@yourself.create/ngx-form-designer 0.0.8 → 0.0.9

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.
@@ -23,6 +23,15 @@ import Quill from 'quill';
23
23
  import { DomSanitizer } from '@angular/platform-browser';
24
24
  import { HttpClient, provideHttpClient } from '@angular/common/http';
25
25
 
26
+ function usesFieldThousandSeparator(field) {
27
+ return field?.useThousandSeparator === true;
28
+ }
29
+ function isFieldReadonly(field) {
30
+ return field?.html5?.readonly === true;
31
+ }
32
+ function isFieldDisabled(field) {
33
+ return field?.html5?.disabled === true;
34
+ }
26
35
  const CURRENT_SCHEMA_VERSION = '1.0.0';
27
36
 
28
37
  /**
@@ -446,8 +455,8 @@ class FormEngine {
446
455
  setValue(fieldName, value) {
447
456
  this.values[fieldName] = value;
448
457
  this.valueSubject.next({ ...this.values }); // Emit new state
449
- // Re-run validation for this field.
450
- this.validateField(fieldName);
458
+ // Rules and dependencies can affect other fields, so revalidate the full form.
459
+ this.validate();
451
460
  }
452
461
  getValues() {
453
462
  return { ...this.values };
@@ -502,7 +511,7 @@ class FormEngine {
502
511
  if (!field)
503
512
  return false;
504
513
  // 1. Dependencies (Legacy)
505
- let enabled = this.evaluateDependencyRules(field, 'enable', 'disable', true);
514
+ let enabled = this.evaluateDependencyRules(field, 'enable', 'disable', !isFieldDisabled(field));
506
515
  // 2. Enterprise Rules
507
516
  enabled = this.evaluateEnterpriseRules(field, 'enable', 'disable', enabled);
508
517
  return enabled;
@@ -535,10 +544,16 @@ class FormEngine {
535
544
  return;
536
545
  const value = this.values[fieldName];
537
546
  const fieldErrors = [];
547
+ const ruleErrors = this.getRuleValidationErrors(field);
538
548
  // Visibility check
539
549
  const isVisible = this.isFieldVisible(field.id);
540
550
  if (!isVisible) {
541
- delete this.errors[fieldName];
551
+ if (ruleErrors.length > 0) {
552
+ this.errors[fieldName] = ruleErrors;
553
+ }
554
+ else {
555
+ delete this.errors[fieldName];
556
+ }
542
557
  return;
543
558
  }
544
559
  const isRequired = this.isFieldRequired(field.id);
@@ -568,30 +583,9 @@ class FormEngine {
568
583
  }
569
584
  }
570
585
  }
571
- // Custom Validation Rules
572
- if (field.validation) {
573
- field.validation.forEach(rule => {
574
- let isValid = true;
575
- if (rule.type === 'builtin') {
576
- if (rule.name === 'email') {
577
- isValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(String(value));
578
- }
579
- }
580
- else if (rule.type === 'expression' && rule.expression) {
581
- try {
582
- const checkFn = new Function('value', 'form', `return ${rule.expression}`);
583
- isValid = checkFn(value, this.values);
584
- }
585
- catch (e) {
586
- console.error('Validation expression error', e);
587
- isValid = false;
588
- }
589
- }
590
- if (!isValid) {
591
- fieldErrors.push(rule.message || 'Validation failed.');
592
- }
593
- });
594
- }
586
+ }
587
+ if (ruleErrors.length > 0) {
588
+ fieldErrors.push(...ruleErrors);
595
589
  }
596
590
  if (fieldErrors.length > 0) {
597
591
  this.errors[fieldName] = fieldErrors;
@@ -600,6 +594,11 @@ class FormEngine {
600
594
  delete this.errors[fieldName];
601
595
  }
602
596
  }
597
+ getRuleValidationErrors(field) {
598
+ return this.getEnterpriseRuleOutcomes(field)
599
+ .filter(({ rule, conditionMet }) => rule.severity === 'error' && !conditionMet && rule.elseAction === undefined)
600
+ .map(({ rule }) => this.getRuleValidationMessage(rule));
601
+ }
603
602
  // Generic rule evaluator
604
603
  evaluateDependencyRules(field, positiveEffect, negativeEffect, startValue) {
605
604
  if (!field.dependencies)
@@ -638,13 +637,31 @@ class FormEngine {
638
637
  if (hasPositiveRules && startValue === true) {
639
638
  currentState = false; // Implicitly hide/disable
640
639
  }
641
- field.rules.forEach((rule) => {
642
- const conditionMet = this.ruleEvaluator.evaluateRule(rule, this.values, this.schema.fields);
643
- const nextAction = conditionMet ? rule.action : rule.elseAction;
640
+ this.getEnterpriseRuleOutcomes(field).forEach(({ nextAction }) => {
644
641
  currentState = this.applyRuleAction(currentState, nextAction, positiveAction, negativeAction);
645
642
  });
646
643
  return currentState;
647
644
  }
645
+ getEnterpriseRuleOutcomes(field) {
646
+ if (!field.rules || field.rules.length === 0) {
647
+ return [];
648
+ }
649
+ return field.rules.map((rule) => {
650
+ const conditionMet = this.ruleEvaluator.evaluateRule(rule, this.values, this.schema.fields);
651
+ return {
652
+ rule,
653
+ conditionMet,
654
+ nextAction: conditionMet ? rule.action : rule.elseAction
655
+ };
656
+ });
657
+ }
658
+ getRuleValidationMessage(rule) {
659
+ const name = rule.name?.trim();
660
+ if (name) {
661
+ return name;
662
+ }
663
+ return `Rule "${rule.action}" failed.`;
664
+ }
648
665
  applyRuleAction(currentState, action, positiveAction, negativeAction) {
649
666
  if (action === positiveAction) {
650
667
  return true;
@@ -4496,6 +4513,7 @@ class FormEventRunner {
4496
4513
  const target = fields.find(f => f.id === action.targetFieldId);
4497
4514
  if (!target)
4498
4515
  return;
4516
+ const previousValue = this.engine.getValue(target.name);
4499
4517
  let val;
4500
4518
  if (action.valueFrom === 'literal') {
4501
4519
  val = action.valueLiteral;
@@ -4510,6 +4528,14 @@ class FormEventRunner {
4510
4528
  }
4511
4529
  }
4512
4530
  this.engine.setValue(target.name, val);
4531
+ if (!Object.is(previousValue, val)) {
4532
+ this.engine.emitUiEvent({
4533
+ fieldId: target.id,
4534
+ fieldName: target.name,
4535
+ type: 'change',
4536
+ value: val
4537
+ });
4538
+ }
4513
4539
  }
4514
4540
  async handleApiAction(action, eventId, evt, fields) {
4515
4541
  if (!this.apiExecutor) {
@@ -5181,6 +5207,7 @@ class JsonFormRendererComponent {
5181
5207
  uploadClient = inject(FILE_UPLOAD_CLIENT, { optional: true }) ?? inject(DefaultFileUploadClient);
5182
5208
  runtimeFieldDataAccessRegistry = inject(RuntimeFieldDataAccessRegistryService);
5183
5209
  dataCatalog = inject(DataCatalog, { optional: true });
5210
+ ruleEvaluationService = inject(RuleEvaluationService);
5184
5211
  constructor(designerState) {
5185
5212
  this.designerState = designerState;
5186
5213
  }
@@ -5435,10 +5462,12 @@ class JsonFormRendererComponent {
5435
5462
  const errors = revalidate
5436
5463
  ? (this.engine?.validate() ?? {})
5437
5464
  : (this.engine?.getErrors() ?? {});
5465
+ const fields = this.buildFieldValidationState(errors);
5438
5466
  return {
5439
5467
  errors: { ...errors },
5440
5468
  isValid: Object.keys(errors).length === 0,
5441
- fields: this.buildFieldValidationState(errors)
5469
+ isSeverityError: Object.values(fields).some(field => field.isSeverityError),
5470
+ fields
5442
5471
  };
5443
5472
  }
5444
5473
  buildFieldValidationState(errors) {
@@ -5459,71 +5488,99 @@ class JsonFormRendererComponent {
5459
5488
  visible,
5460
5489
  required,
5461
5490
  valid: fieldErrors.length === 0,
5491
+ isSeverityError: false,
5462
5492
  errors: fieldErrors,
5463
- validators: this.describeFieldValidators(field, visible, required)
5493
+ validators: this.describeFieldValidators(field, visible, required, fieldErrors)
5464
5494
  };
5495
+ states[field.name].isSeverityError = states[field.name].validators.some(validator => validator.severity === 'error' && validator.valid === false);
5465
5496
  }
5466
5497
  return states;
5467
5498
  }
5468
- describeFieldValidators(field, visible, required) {
5499
+ describeFieldValidators(field, visible, required, fieldErrors) {
5469
5500
  const validators = [];
5470
5501
  const value = this.engine?.getValue(field.name);
5471
5502
  const hasValue = !this.isValidationEmpty(value);
5503
+ const ruleOutcomes = this.getRuleOutcomes(field);
5504
+ const requiredSeverity = ruleOutcomes.some(outcome => outcome.rule.severity === 'error' && outcome.nextAction === 'required');
5472
5505
  if (this.hasRequiredValidation(field) || required) {
5506
+ const message = 'This field is required.';
5473
5507
  validators.push({
5474
5508
  name: 'required',
5475
5509
  source: 'required',
5476
5510
  active: visible && required,
5477
- message: 'This field is required.'
5511
+ valid: !fieldErrors.includes(message),
5512
+ message,
5513
+ ...(requiredSeverity ? { severity: 'error' } : {})
5478
5514
  });
5479
5515
  }
5480
5516
  if (field.html5?.minLength !== undefined) {
5517
+ const message = `Minimum length is ${field.html5.minLength}.`;
5481
5518
  validators.push({
5482
5519
  name: 'minLength',
5483
5520
  source: 'html5',
5484
5521
  active: visible && hasValue,
5485
- value: field.html5.minLength
5522
+ valid: !fieldErrors.includes(message),
5523
+ value: field.html5.minLength,
5524
+ message
5486
5525
  });
5487
5526
  }
5488
5527
  if (field.html5?.maxLength !== undefined) {
5528
+ const message = `Maximum length is ${field.html5.maxLength}.`;
5489
5529
  validators.push({
5490
5530
  name: 'maxLength',
5491
5531
  source: 'html5',
5492
5532
  active: visible && hasValue,
5493
- value: field.html5.maxLength
5533
+ valid: !fieldErrors.includes(message),
5534
+ value: field.html5.maxLength,
5535
+ message
5494
5536
  });
5495
5537
  }
5496
5538
  if (field.html5?.min !== undefined) {
5539
+ const message = `Minimum value is ${field.html5.min}.`;
5497
5540
  validators.push({
5498
5541
  name: 'min',
5499
5542
  source: 'html5',
5500
5543
  active: visible && hasValue,
5501
- value: field.html5.min
5544
+ valid: !fieldErrors.includes(message),
5545
+ value: field.html5.min,
5546
+ message
5502
5547
  });
5503
5548
  }
5504
5549
  if (field.html5?.max !== undefined) {
5550
+ const message = `Maximum value is ${field.html5.max}.`;
5505
5551
  validators.push({
5506
5552
  name: 'max',
5507
5553
  source: 'html5',
5508
5554
  active: visible && hasValue,
5509
- value: field.html5.max
5555
+ valid: !fieldErrors.includes(message),
5556
+ value: field.html5.max,
5557
+ message
5510
5558
  });
5511
5559
  }
5512
5560
  if (field.html5?.pattern) {
5561
+ const message = 'Invalid format.';
5513
5562
  validators.push({
5514
5563
  name: 'pattern',
5515
5564
  source: 'html5',
5516
5565
  active: visible && hasValue,
5517
- value: field.html5.pattern
5566
+ valid: !fieldErrors.includes(message),
5567
+ value: field.html5.pattern,
5568
+ message
5518
5569
  });
5519
5570
  }
5520
- for (const rule of field.validation ?? []) {
5571
+ for (const outcome of ruleOutcomes) {
5572
+ const message = this.getRuleValidationMessage(outcome.rule);
5521
5573
  validators.push({
5522
- name: rule.type === 'builtin' ? (rule.name ?? 'builtin') : 'expression',
5523
- source: 'custom',
5524
- active: visible && hasValue && this.isValidationRuleActive(rule),
5525
- value: rule.type === 'expression' ? rule.expression : rule.name,
5526
- message: rule.message
5574
+ name: outcome.rule.name?.trim() || this.describeRuleName(outcome.rule),
5575
+ source: 'rule',
5576
+ active: true,
5577
+ valid: outcome.conditionMet || outcome.nextAction !== undefined,
5578
+ value: {
5579
+ action: outcome.rule.action,
5580
+ elseAction: outcome.rule.elseAction
5581
+ },
5582
+ message,
5583
+ ...(outcome.rule.severity ? { severity: outcome.rule.severity } : {})
5527
5584
  });
5528
5585
  }
5529
5586
  return validators;
@@ -5542,16 +5599,30 @@ class JsonFormRendererComponent {
5542
5599
  }
5543
5600
  return false;
5544
5601
  }
5545
- isValidationRuleActive(rule) {
5546
- if (!rule.when)
5547
- return true;
5548
- try {
5549
- const checkFn = new Function('form', `return ${rule.when}`);
5550
- return checkFn(this.engine?.getValues() ?? {});
5602
+ getRuleOutcomes(field) {
5603
+ if (!field.rules?.length) {
5604
+ return [];
5551
5605
  }
5552
- catch {
5553
- return false;
5606
+ const schema = this.engine?.getSchema() ?? this.schema;
5607
+ const formValues = this.engine?.getValues() ?? {};
5608
+ return field.rules.map(rule => {
5609
+ const conditionMet = this.ruleEvaluationService.evaluateRule(rule, formValues, schema?.fields ?? []);
5610
+ return {
5611
+ rule,
5612
+ conditionMet,
5613
+ nextAction: conditionMet ? rule.action : rule.elseAction
5614
+ };
5615
+ });
5616
+ }
5617
+ describeRuleName(rule) {
5618
+ return rule.action.charAt(0).toUpperCase() + rule.action.slice(1);
5619
+ }
5620
+ getRuleValidationMessage(rule) {
5621
+ const name = rule.name?.trim();
5622
+ if (name) {
5623
+ return name;
5554
5624
  }
5625
+ return `Rule "${rule.action}" failed.`;
5555
5626
  }
5556
5627
  isValidationEmpty(value) {
5557
5628
  return value === null
@@ -10271,13 +10342,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImpo
10271
10342
  class DynamicPropertiesComponent {
10272
10343
  onPropertyChange;
10273
10344
  designerCtx = inject(DesignerContext);
10274
- validatorTypeOptions = [
10275
- { label: 'Built-in', value: 'builtin' },
10276
- { label: 'Expression', value: 'expression' }
10277
- ];
10278
- builtinValidatorOptions = [
10279
- { label: 'Email', value: 'email' }
10280
- ];
10281
10345
  get properties() {
10282
10346
  if (!this.config)
10283
10347
  return [];
@@ -10609,68 +10673,6 @@ class DynamicPropertiesComponent {
10609
10673
  this.setValue(path, options);
10610
10674
  }
10611
10675
  }
10612
- // Validators editor methods
10613
- addValidator(path) {
10614
- if (this.readOnly)
10615
- return;
10616
- const validators = [...(this.getValue(path) || [])];
10617
- validators.push(this.createDefaultValidationRule());
10618
- this.setValue(path, validators);
10619
- this.handleFieldChange();
10620
- }
10621
- removeValidator(path, index) {
10622
- if (this.readOnly)
10623
- return;
10624
- const validators = [...(this.getValue(path) || [])];
10625
- validators.splice(index, 1);
10626
- this.setValue(path, validators);
10627
- this.handleFieldChange();
10628
- }
10629
- updateValidator(path, index, field, value) {
10630
- if (this.readOnly)
10631
- return;
10632
- const validators = [...(this.getValue(path) || [])];
10633
- const currentRule = validators[index];
10634
- if (!currentRule) {
10635
- return;
10636
- }
10637
- let nextRule = { ...currentRule };
10638
- if (field === 'type') {
10639
- nextRule = value === 'expression'
10640
- ? {
10641
- type: 'expression',
10642
- expression: 'return true;',
10643
- message: 'Validation failed.'
10644
- }
10645
- : this.createDefaultValidationRule();
10646
- }
10647
- else if (field === 'name') {
10648
- nextRule.name = String(value);
10649
- nextRule.message = this.defaultValidationMessage(nextRule);
10650
- }
10651
- else if (field === 'expression') {
10652
- nextRule.expression = String(value);
10653
- }
10654
- else if (field === 'message') {
10655
- nextRule.message = String(value);
10656
- }
10657
- validators[index] = nextRule;
10658
- this.setValue(path, validators);
10659
- this.handleFieldChange();
10660
- }
10661
- createDefaultValidationRule() {
10662
- return {
10663
- type: 'builtin',
10664
- name: 'email',
10665
- message: 'Enter a valid email address.'
10666
- };
10667
- }
10668
- defaultValidationMessage(rule) {
10669
- if (rule.type === 'builtin' && rule.name === 'email') {
10670
- return 'Enter a valid email address.';
10671
- }
10672
- return 'Validation failed.';
10673
- }
10674
10676
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: DynamicPropertiesComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
10675
10677
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.17", type: DynamicPropertiesComponent, isStandalone: true, selector: "app-dynamic-properties", inputs: { onPropertyChange: "onPropertyChange", config: "config", readOnly: "readOnly", includeSections: "includeSections", excludeSections: "excludeSections", allFields: "allFields" }, outputs: { configChange: "configChange" }, usesOnChanges: true, ngImport: i0, template: `
10676
10678
  <div class="dynamic-properties flex flex-col font-sans text-sm">
@@ -10813,51 +10815,6 @@ class DynamicPropertiesComponent {
10813
10815
  </div>
10814
10816
  </ui-field-wrapper>
10815
10817
 
10816
- <!-- Validators Editor -->
10817
- <ui-field-wrapper *ngIf="field.type === 'validators-editor'" [label]="field.label || ''" [helpText]="field.helpText || ''">
10818
- <div class="w-full border border-gray-200 rounded-lg p-3 bg-gray-50">
10819
- <div class="flex items-center justify-between mb-3">
10820
- <span class="text-xs font-semibold text-gray-600 uppercase">{{field.label}}</span>
10821
- <button type="button"
10822
- (click)="addValidator(field.key)"
10823
- class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors">
10824
- +
10825
- </button>
10826
- </div>
10827
- <div class="space-y-2">
10828
- <div *ngFor="let val of getValue(field.key) || []; let i = index" class="flex items-center gap-2 p-2 bg-white rounded border border-gray-200">
10829
- <select [ngModel]="val.type || 'builtin'"
10830
- (ngModelChange)="updateValidator(field.key, i, 'type', $event)"
10831
- class="flex-1 h-8 px-2 text-sm rounded border border-gray-300 bg-white focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none">
10832
- <option *ngFor="let option of validatorTypeOptions" [value]="option.value">{{ option.label }}</option>
10833
- </select>
10834
- <select *ngIf="(val.type || 'builtin') === 'builtin'"
10835
- [ngModel]="val.name || 'email'"
10836
- (ngModelChange)="updateValidator(field.key, i, 'name', $event)"
10837
- class="flex-1 h-8 px-2 text-sm rounded border border-gray-300 bg-white focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none">
10838
- <option *ngFor="let option of builtinValidatorOptions" [value]="option.value">{{ option.label }}</option>
10839
- </select>
10840
- <input *ngIf="val.type === 'expression'"
10841
- type="text"
10842
- [ngModel]="val.expression || ''"
10843
- (ngModelChange)="updateValidator(field.key, i, 'expression', $event)"
10844
- placeholder="return true;"
10845
- class="flex-1 h-8 px-2 text-sm rounded border border-gray-300 bg-white focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none">
10846
- <input type="text"
10847
- [ngModel]="val.message || ''"
10848
- (ngModelChange)="updateValidator(field.key, i, 'message', $event)"
10849
- placeholder="Validation message"
10850
- class="flex-1 h-8 px-2 text-sm rounded border border-gray-300 bg-white focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none">
10851
- <button type="button"
10852
- (click)="removeValidator(field.key, i)"
10853
- class="w-8 h-8 flex items-center justify-center text-red-600 hover:bg-red-50 rounded transition-colors">
10854
- <lucide-icon name="x" class="w-4 h-4"></lucide-icon>
10855
- </button>
10856
- </div>
10857
- </div>
10858
- </div>
10859
- </ui-field-wrapper>
10860
-
10861
10818
  <!-- Field Reference -->
10862
10819
  <ui-field-wrapper *ngIf="field.type === 'field-reference'" [label]="field.label || ''" [helpText]="field.helpText || ''">
10863
10820
  <select [ngModel]="getValue(field.key) || ''"
@@ -11106,51 +11063,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImpo
11106
11063
  </div>
11107
11064
  </ui-field-wrapper>
11108
11065
 
11109
- <!-- Validators Editor -->
11110
- <ui-field-wrapper *ngIf="field.type === 'validators-editor'" [label]="field.label || ''" [helpText]="field.helpText || ''">
11111
- <div class="w-full border border-gray-200 rounded-lg p-3 bg-gray-50">
11112
- <div class="flex items-center justify-between mb-3">
11113
- <span class="text-xs font-semibold text-gray-600 uppercase">{{field.label}}</span>
11114
- <button type="button"
11115
- (click)="addValidator(field.key)"
11116
- class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors">
11117
- +
11118
- </button>
11119
- </div>
11120
- <div class="space-y-2">
11121
- <div *ngFor="let val of getValue(field.key) || []; let i = index" class="flex items-center gap-2 p-2 bg-white rounded border border-gray-200">
11122
- <select [ngModel]="val.type || 'builtin'"
11123
- (ngModelChange)="updateValidator(field.key, i, 'type', $event)"
11124
- class="flex-1 h-8 px-2 text-sm rounded border border-gray-300 bg-white focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none">
11125
- <option *ngFor="let option of validatorTypeOptions" [value]="option.value">{{ option.label }}</option>
11126
- </select>
11127
- <select *ngIf="(val.type || 'builtin') === 'builtin'"
11128
- [ngModel]="val.name || 'email'"
11129
- (ngModelChange)="updateValidator(field.key, i, 'name', $event)"
11130
- class="flex-1 h-8 px-2 text-sm rounded border border-gray-300 bg-white focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none">
11131
- <option *ngFor="let option of builtinValidatorOptions" [value]="option.value">{{ option.label }}</option>
11132
- </select>
11133
- <input *ngIf="val.type === 'expression'"
11134
- type="text"
11135
- [ngModel]="val.expression || ''"
11136
- (ngModelChange)="updateValidator(field.key, i, 'expression', $event)"
11137
- placeholder="return true;"
11138
- class="flex-1 h-8 px-2 text-sm rounded border border-gray-300 bg-white focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none">
11139
- <input type="text"
11140
- [ngModel]="val.message || ''"
11141
- (ngModelChange)="updateValidator(field.key, i, 'message', $event)"
11142
- placeholder="Validation message"
11143
- class="flex-1 h-8 px-2 text-sm rounded border border-gray-300 bg-white focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none">
11144
- <button type="button"
11145
- (click)="removeValidator(field.key, i)"
11146
- class="w-8 h-8 flex items-center justify-center text-red-600 hover:bg-red-50 rounded transition-colors">
11147
- <lucide-icon name="x" class="w-4 h-4"></lucide-icon>
11148
- </button>
11149
- </div>
11150
- </div>
11151
- </div>
11152
- </ui-field-wrapper>
11153
-
11154
11066
  <!-- Field Reference -->
11155
11067
  <ui-field-wrapper *ngIf="field.type === 'field-reference'" [label]="field.label || ''" [helpText]="field.helpText || ''">
11156
11068
  <select [ngModel]="getValue(field.key) || ''"
@@ -15261,7 +15173,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImpo
15261
15173
  type: Output
15262
15174
  }] } });
15263
15175
 
15264
- function parsePathSegments$2(path) {
15176
+ function parsePathSegments$3(path) {
15265
15177
  const segments = [];
15266
15178
  const matcher = /([^[.\]]+)|\[(\d+)\]/g;
15267
15179
  let match;
@@ -15277,14 +15189,14 @@ function parsePathSegments$2(path) {
15277
15189
  }
15278
15190
  return segments;
15279
15191
  }
15280
- function resolvePathValue$1(value, path) {
15192
+ function resolvePathValue$2(value, path) {
15281
15193
  if (!path)
15282
15194
  return value;
15283
15195
  const normalized = path.trim();
15284
15196
  if (!normalized)
15285
15197
  return value;
15286
15198
  let current = value;
15287
- for (const segment of parsePathSegments$2(normalized)) {
15199
+ for (const segment of parsePathSegments$3(normalized)) {
15288
15200
  if (current === null || current === undefined)
15289
15201
  return undefined;
15290
15202
  if (Array.isArray(current)) {
@@ -15301,7 +15213,7 @@ function resolvePathValue$1(value, path) {
15301
15213
  return current;
15302
15214
  }
15303
15215
  function assignPathValue(target, path, value) {
15304
- const segments = parsePathSegments$2(path);
15216
+ const segments = parsePathSegments$3(path);
15305
15217
  if (!segments.length) {
15306
15218
  return;
15307
15219
  }
@@ -15437,7 +15349,6 @@ class DataPanelComponent {
15437
15349
  selectionFieldId;
15438
15350
  selectionMatchPath;
15439
15351
  childRowsPath;
15440
- formatNumericOptionLabels = false;
15441
15352
  optionLabelPrefixPath;
15442
15353
  rootPathOptions = [];
15443
15354
  rowPathOptions = [];
@@ -15679,7 +15590,6 @@ class DataPanelComponent {
15679
15590
  this.selectionFieldId = undefined;
15680
15591
  this.selectionMatchPath = undefined;
15681
15592
  this.childRowsPath = undefined;
15682
- this.formatNumericOptionLabels = false;
15683
15593
  this.optionLabelPrefixPath = undefined;
15684
15594
  this.rootPathOptions = [];
15685
15595
  this.rowPathOptions = [];
@@ -15721,7 +15631,6 @@ class DataPanelComponent {
15721
15631
  this.selectionFieldId = d.selectionFieldId;
15722
15632
  this.selectionMatchPath = d.selectionMatchPath;
15723
15633
  this.childRowsPath = d.childRowsPath;
15724
- this.formatNumericOptionLabels = d.formatNumericOptionLabels === true;
15725
15634
  this.optionLabelPrefixPath = d.optionLabelPrefixPath;
15726
15635
  // Search
15727
15636
  this.searchEnabled = !!d.searchEnabled;
@@ -15774,10 +15683,7 @@ class DataPanelComponent {
15774
15683
  labelKey: this.sourceType === 'source' ? this.labelKey : undefined,
15775
15684
  valueKey: this.sourceType === 'source' ? this.valueKey : undefined,
15776
15685
  rowsPath: this.sourceType === 'source' ? this.normalizedRowsPath() : undefined,
15777
- formatNumericOptionLabels: this.shouldPersistOptionLabelFormatting()
15778
- ? this.formatNumericOptionLabels
15779
- : undefined,
15780
- optionLabelPrefixPath: this.shouldPersistOptionLabelFormatting()
15686
+ optionLabelPrefixPath: this.shouldPersistOptionLabelPrefix()
15781
15687
  ? this.optionLabelPrefixPath
15782
15688
  : undefined,
15783
15689
  rowSelectionMode: this.sourceType === 'source' && this.bindingShape === 'scalar' && this.rowSelectionMode === 'selected'
@@ -15896,9 +15802,6 @@ class DataPanelComponent {
15896
15802
  && this.bindingShape === 'scalar'
15897
15803
  && (this.widgetType === 'text' || this.widgetType === 'number');
15898
15804
  }
15899
- supportsNumericDisplayFormatting() {
15900
- return this.widgetType === 'select' || this.widgetType === 'number';
15901
- }
15902
15805
  displayFormattingTitle() {
15903
15806
  return this.widgetType === 'select' ? 'Amount Display' : 'Field Display';
15904
15807
  }
@@ -15907,15 +15810,10 @@ class DataPanelComponent {
15907
15810
  return 'Format the visible dropdown label without changing the stored option value.';
15908
15811
  }
15909
15812
  if (this.widgetType === 'number') {
15910
- return 'Show a fixed prefix and grouped digits in the visible field value without changing the stored number.';
15813
+ return 'Show a fixed prefix in the visible field value without changing the stored number.';
15911
15814
  }
15912
15815
  return 'Show a fixed prefix beside the editable value without changing the stored field value.';
15913
15816
  }
15914
- numericDisplayFormattingLabel() {
15915
- return this.widgetType === 'select'
15916
- ? 'Show thousand separators for numeric labels'
15917
- : 'Show thousand separators for numeric values';
15918
- }
15919
15817
  showStaticOptionsEditor() {
15920
15818
  return this.sourceType === 'static' && this.widgetType !== 'table' && this.usesOptionMapping();
15921
15819
  }
@@ -15944,12 +15842,12 @@ class DataPanelComponent {
15944
15842
  }
15945
15843
  getPreviewLabel(row) {
15946
15844
  const key = this.labelKey || 'label';
15947
- const value = resolvePathValue$1(row, key);
15845
+ const value = resolvePathValue$2(row, key);
15948
15846
  return value === undefined || value === null || value === '' ? '(no label)' : String(value);
15949
15847
  }
15950
15848
  getPreviewValue(row) {
15951
15849
  const key = this.valueKey || 'value';
15952
- const value = resolvePathValue$1(row, key);
15850
+ const value = resolvePathValue$2(row, key);
15953
15851
  return value === undefined || value === null ? '' : String(value);
15954
15852
  }
15955
15853
  availableRootPaths() {
@@ -15966,7 +15864,7 @@ class DataPanelComponent {
15966
15864
  const sample = this.extractPreviewRows(this.previewRows, this.effectiveRowsPath())[0];
15967
15865
  return sample ? collectArrayPaths(sample) : [];
15968
15866
  }
15969
- shouldPersistOptionLabelFormatting() {
15867
+ shouldPersistOptionLabelPrefix() {
15970
15868
  if (this.widgetType === 'select' && this.usesOptionMapping()) {
15971
15869
  return true;
15972
15870
  }
@@ -15982,7 +15880,7 @@ class DataPanelComponent {
15982
15880
  return normalized ? normalized : 'defaultValue';
15983
15881
  }
15984
15882
  readScalarTargetValue() {
15985
- const resolved = resolvePathValue$1(this.config, this.scalarTargetPath());
15883
+ const resolved = resolvePathValue$2(this.config, this.scalarTargetPath());
15986
15884
  if (resolved !== undefined) {
15987
15885
  return resolved;
15988
15886
  }
@@ -16005,7 +15903,7 @@ class DataPanelComponent {
16005
15903
  }
16006
15904
  const flattened = [];
16007
15905
  for (const row of rows) {
16008
- const resolved = resolvePathValue$1(row, normalizedPath);
15906
+ const resolved = resolvePathValue$2(row, normalizedPath);
16009
15907
  if (Array.isArray(resolved)) {
16010
15908
  flattened.push(...resolved);
16011
15909
  continue;
@@ -16297,15 +16195,6 @@ class DataPanelComponent {
16297
16195
  <div class="text-xs font-semibold uppercase tracking-wide text-gray-500">{{ displayFormattingTitle() }}</div>
16298
16196
  <div class="mt-1 text-[11px] text-gray-500">{{ displayFormattingDescription() }}</div>
16299
16197
 
16300
- <label class="mt-3 flex items-center gap-2 text-sm text-gray-700" *ngIf="supportsNumericDisplayFormatting()">
16301
- <input
16302
- type="checkbox"
16303
- [checked]="formatNumericOptionLabels"
16304
- (change)="formatNumericOptionLabels = $any($event.target).checked; emitChange()"
16305
- class="rounded border-gray-300 text-blue-600 focus:ring-blue-500">
16306
- <span>{{ numericDisplayFormattingLabel() }}</span>
16307
- </label>
16308
-
16309
16198
  <div class="mt-3 flex flex-col gap-1">
16310
16199
  <label class="text-xs font-medium text-gray-500">Prefix Key</label>
16311
16200
  <input
@@ -16916,15 +16805,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImpo
16916
16805
  <div class="text-xs font-semibold uppercase tracking-wide text-gray-500">{{ displayFormattingTitle() }}</div>
16917
16806
  <div class="mt-1 text-[11px] text-gray-500">{{ displayFormattingDescription() }}</div>
16918
16807
 
16919
- <label class="mt-3 flex items-center gap-2 text-sm text-gray-700" *ngIf="supportsNumericDisplayFormatting()">
16920
- <input
16921
- type="checkbox"
16922
- [checked]="formatNumericOptionLabels"
16923
- (change)="formatNumericOptionLabels = $any($event.target).checked; emitChange()"
16924
- class="rounded border-gray-300 text-blue-600 focus:ring-blue-500">
16925
- <span>{{ numericDisplayFormattingLabel() }}</span>
16926
- </label>
16927
-
16928
16808
  <div class="mt-3 flex flex-col gap-1">
16929
16809
  <label class="text-xs font-medium text-gray-500">Prefix Key</label>
16930
16810
  <input
@@ -17866,7 +17746,7 @@ class RulesPanelComponent {
17866
17746
  <div class="flex flex-col gap-2">
17867
17747
  <div class="flex items-center gap-2 bg-amber-50 p-2 rounded-md border border-amber-100">
17868
17748
  <span class="text-xs font-semibold text-amber-700 uppercase">Severity</span>
17869
- <span class="text-xs text-amber-800 italic">Saved for backend validation only.</span>
17749
+ <span class="text-xs text-amber-800 italic">Error severity is emitted in runtime validation; warn stays informational.</span>
17870
17750
  </div>
17871
17751
 
17872
17752
  <div class="flex items-center gap-2">
@@ -17988,7 +17868,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImpo
17988
17868
  <div class="flex flex-col gap-2">
17989
17869
  <div class="flex items-center gap-2 bg-amber-50 p-2 rounded-md border border-amber-100">
17990
17870
  <span class="text-xs font-semibold text-amber-700 uppercase">Severity</span>
17991
- <span class="text-xs text-amber-800 italic">Saved for backend validation only.</span>
17871
+ <span class="text-xs text-amber-800 italic">Error severity is emitted in runtime validation; warn stays informational.</span>
17992
17872
  </div>
17993
17873
 
17994
17874
  <div class="flex items-center gap-2">
@@ -20255,13 +20135,13 @@ async function updateFieldSettings(state, args, widgetDefs = []) {
20255
20135
  updates[key] = value;
20256
20136
  }
20257
20137
  }
20138
+ const html5Patch = {};
20258
20139
  if (typeof a['disabled'] === 'boolean') {
20259
- updates.disabled = a['disabled'];
20140
+ html5Patch['disabled'] = a['disabled'];
20260
20141
  }
20261
20142
  if (typeof a['readonly'] === 'boolean') {
20262
- updates.readonly = a['readonly'];
20143
+ html5Patch['readonly'] = a['readonly'];
20263
20144
  }
20264
- const html5Patch = {};
20265
20145
  const html5Keys = ['required', 'min', 'max', 'step', 'minLength', 'maxLength', 'pattern'];
20266
20146
  for (const key of html5Keys) {
20267
20147
  if (typeof a[key] !== 'undefined') {
@@ -22071,7 +21951,7 @@ const DATA_SOURCES_TEMPLATE = {
22071
21951
  "type": "text",
22072
21952
  "label": "Selected country code",
22073
21953
  "placeholder": "Auto-filled",
22074
- "readonly": true,
21954
+ "html5": { "readonly": true },
22075
21955
  "helpText": "Updated by an event binding on the Country field."
22076
21956
  },
22077
21957
  {
@@ -22477,8 +22357,8 @@ const TRANSACTION_KYC_REVIEW_TEMPLATE = {
22477
22357
  "name": "clientDisplay",
22478
22358
  "type": "text",
22479
22359
  "label": "Client",
22480
- "readonly": true,
22481
22360
  "html5": {
22361
+ "readonly": true,
22482
22362
  "required": false
22483
22363
  },
22484
22364
  "rules": [
@@ -22524,7 +22404,7 @@ const TRANSACTION_KYC_REVIEW_TEMPLATE = {
22524
22404
  "type": "text",
22525
22405
  "label": "Balance",
22526
22406
  "defaultValue": "USD 24,580.00",
22527
- "readonly": true,
22407
+ "html5": { "readonly": true },
22528
22408
  "rules": [
22529
22409
  {
22530
22410
  "id": "rule_balance_visible",
@@ -23292,7 +23172,7 @@ const JOURNEY_BRANCHING_TEMPLATE_STEP_TWO = {
23292
23172
  "name": "summaryCountryCode",
23293
23173
  "type": "text",
23294
23174
  "label": "Country Code",
23295
- "readonly": true,
23175
+ "html5": { "readonly": true },
23296
23176
  "dataConfig": {
23297
23177
  "type": "source",
23298
23178
  "datasourceId": "journey_branch_ds_country_profile",
@@ -23497,7 +23377,7 @@ const JOURNEY_BRANCHING_TEMPLATE_STEP_THREE = {
23497
23377
  "name": "summaryCurrency",
23498
23378
  "type": "text",
23499
23379
  "label": "Currency",
23500
- "readonly": true,
23380
+ "html5": { "readonly": true },
23501
23381
  "dataConfig": {
23502
23382
  "type": "source",
23503
23383
  "datasourceId": "journey_branch_ds_country_profile",
@@ -23510,7 +23390,7 @@ const JOURNEY_BRANCHING_TEMPLATE_STEP_THREE = {
23510
23390
  "name": "summaryTimezone",
23511
23391
  "type": "text",
23512
23392
  "label": "Timezone",
23513
- "readonly": true,
23393
+ "html5": { "readonly": true },
23514
23394
  "dataConfig": {
23515
23395
  "type": "source",
23516
23396
  "datasourceId": "journey_branch_ds_country_profile",
@@ -31742,7 +31622,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImpo
31742
31622
  }]
31743
31623
  }] });
31744
31624
 
31745
- function parsePathSegments$1(path) {
31625
+ function parsePathSegments$2(path) {
31746
31626
  const segments = [];
31747
31627
  const matcher = /([^[.\]]+)|\[(\d+)\]/g;
31748
31628
  let match;
@@ -31758,14 +31638,14 @@ function parsePathSegments$1(path) {
31758
31638
  }
31759
31639
  return segments;
31760
31640
  }
31761
- function resolvePathValue(value, path) {
31641
+ function resolvePathValue$1(value, path) {
31762
31642
  if (!path)
31763
31643
  return value;
31764
31644
  const normalizedPath = path.trim();
31765
31645
  if (!normalizedPath)
31766
31646
  return value;
31767
31647
  let current = value;
31768
- for (const segment of parsePathSegments$1(normalizedPath)) {
31648
+ for (const segment of parsePathSegments$2(normalizedPath)) {
31769
31649
  if (current === null || current === undefined) {
31770
31650
  return undefined;
31771
31651
  }
@@ -31789,7 +31669,7 @@ function hasPathSyntax(path) {
31789
31669
  return false;
31790
31670
  return path.includes('.') || path.includes('[');
31791
31671
  }
31792
- function valuesMatch(left, right) {
31672
+ function valuesMatch$1(left, right) {
31793
31673
  if (Object.is(left, right))
31794
31674
  return true;
31795
31675
  if (left === undefined || left === null || right === undefined || right === null)
@@ -31845,8 +31725,8 @@ class DataProvider {
31845
31725
  if (query.sort && query.sort.length > 0) {
31846
31726
  rows = [...rows].sort((a, b) => {
31847
31727
  for (const s of query.sort) {
31848
- const valA = resolvePathValue(a, s.column);
31849
- const valB = resolvePathValue(b, s.column);
31728
+ const valA = resolvePathValue$1(a, s.column);
31729
+ const valB = resolvePathValue$1(b, s.column);
31850
31730
  if (valA === valB)
31851
31731
  continue;
31852
31732
  const result = compareSortableValues(valA, valB);
@@ -32006,12 +31886,12 @@ class DefaultDataProvider extends DataProvider {
32006
31886
  }
32007
31887
  const row = context.row;
32008
31888
  const resolvedPath = cfg.valueKey;
32009
- const resolvedValue = resolvePathValue(row, resolvedPath);
31889
+ const resolvedValue = resolvePathValue$1(row, resolvedPath);
32010
31890
  if (resolvedPath && resolvedValue !== undefined) {
32011
31891
  return resolvedValue;
32012
31892
  }
32013
- if (resolvePathValue(row, 'value') !== undefined) {
32014
- return resolvePathValue(row, 'value');
31893
+ if (resolvePathValue$1(row, 'value') !== undefined) {
31894
+ return resolvePathValue$1(row, 'value');
32015
31895
  }
32016
31896
  return row;
32017
31897
  }
@@ -32028,7 +31908,7 @@ class DefaultDataProvider extends DataProvider {
32028
31908
  for (const candidate of [context.row, context.parentRow, context.sourceRow]) {
32029
31909
  if (!candidate)
32030
31910
  continue;
32031
- const resolved = resolvePathValue(candidate, prefixPath);
31911
+ const resolved = resolvePathValue$1(candidate, prefixPath);
32032
31912
  const value = toDisplayText(resolved);
32033
31913
  if (value) {
32034
31914
  return value;
@@ -32131,7 +32011,7 @@ class DefaultDataProvider extends DataProvider {
32131
32011
  }
32132
32012
  const flattened = [];
32133
32013
  for (const row of rows) {
32134
- const resolved = resolvePathValue(row, normalizedPath);
32014
+ const resolved = resolvePathValue$1(row, normalizedPath);
32135
32015
  if (Array.isArray(resolved)) {
32136
32016
  for (const entry of resolved) {
32137
32017
  flattened.push({
@@ -32159,7 +32039,7 @@ class DefaultDataProvider extends DataProvider {
32159
32039
  }
32160
32040
  const flattened = [];
32161
32041
  for (const row of rows) {
32162
- const resolved = resolvePathValue(row, normalizedPath);
32042
+ const resolved = resolvePathValue$1(row, normalizedPath);
32163
32043
  if (Array.isArray(resolved)) {
32164
32044
  for (const entry of resolved) {
32165
32045
  flattened.push(this.toRowRecord(entry));
@@ -32187,7 +32067,7 @@ class DefaultDataProvider extends DataProvider {
32187
32067
  if (selectorValue === undefined || selectorValue === null || selectorValue === '') {
32188
32068
  return undefined;
32189
32069
  }
32190
- return rows.find(row => valuesMatch(resolvePathValue(row, cfg.selectionMatchPath), selectorValue));
32070
+ return rows.find(row => valuesMatch$1(resolvePathValue$1(row, cfg.selectionMatchPath), selectorValue));
32191
32071
  }
32192
32072
  selectOptionContext(contexts, cfg, engine) {
32193
32073
  if (contexts.length === 0)
@@ -32204,7 +32084,7 @@ class DefaultDataProvider extends DataProvider {
32204
32084
  if (selectorValue === undefined || selectorValue === null || selectorValue === '') {
32205
32085
  return undefined;
32206
32086
  }
32207
- return contexts.find(context => valuesMatch(resolvePathValue(context.row, cfg.selectionMatchPath), selectorValue));
32087
+ return contexts.find(context => valuesMatch$1(resolvePathValue$1(context.row, cfg.selectionMatchPath), selectorValue));
32208
32088
  }
32209
32089
  applyRowFilters(rows, filters, engine) {
32210
32090
  if (!filters || filters.length === 0)
@@ -32221,7 +32101,7 @@ class DefaultDataProvider extends DataProvider {
32221
32101
  const expected = this.resolveFilterValue(filter, engine);
32222
32102
  if (expected === undefined)
32223
32103
  return true;
32224
- const actual = resolvePathValue(row, filter.column);
32104
+ const actual = resolvePathValue$1(row, filter.column);
32225
32105
  const val = expected;
32226
32106
  switch (filter.op) {
32227
32107
  case 'eq': return actual === val;
@@ -32293,17 +32173,16 @@ class DefaultDataProvider extends DataProvider {
32293
32173
  }
32294
32174
  mapRowToOption(row, labelKey, valueKey) {
32295
32175
  return {
32296
- label: String(resolvePathValue(row, labelKey) ?? ''),
32297
- value: this.toOptionValue(resolvePathValue(row, valueKey))
32176
+ label: String(resolvePathValue$1(row, labelKey) ?? ''),
32177
+ value: this.toOptionValue(resolvePathValue$1(row, valueKey))
32298
32178
  };
32299
32179
  }
32300
32180
  mapContextToOption(context, labelKey, valueKey, cfg) {
32301
- const rawLabel = String(resolvePathValue(context.row, labelKey) ?? '');
32302
32181
  const prefix = this.resolveOptionLabelPrefix(context, cfg);
32303
- const label = this.formatOptionLabel(rawLabel, cfg);
32304
32182
  return {
32305
- label: prefix ? `${prefix} ${label}` : label,
32306
- value: this.toOptionValue(resolvePathValue(context.row, valueKey))
32183
+ label: String(resolvePathValue$1(context.row, labelKey) ?? ''),
32184
+ displayPrefix: prefix || undefined,
32185
+ value: this.toOptionValue(resolvePathValue$1(context.row, valueKey))
32307
32186
  };
32308
32187
  }
32309
32188
  resolveOptionLabelPrefix(context, cfg) {
@@ -32314,7 +32193,7 @@ class DefaultDataProvider extends DataProvider {
32314
32193
  for (const candidate of [context.row, context.parentRow, context.sourceRow]) {
32315
32194
  if (!candidate)
32316
32195
  continue;
32317
- const resolved = resolvePathValue(candidate, prefixPath);
32196
+ const resolved = resolvePathValue$1(candidate, prefixPath);
32318
32197
  const value = toDisplayText(resolved);
32319
32198
  if (value) {
32320
32199
  return value;
@@ -32322,29 +32201,6 @@ class DefaultDataProvider extends DataProvider {
32322
32201
  }
32323
32202
  return '';
32324
32203
  }
32325
- formatOptionLabel(label, cfg) {
32326
- if (!cfg.formatNumericOptionLabels) {
32327
- return label;
32328
- }
32329
- const trimmed = label.trim();
32330
- if (!trimmed) {
32331
- return label;
32332
- }
32333
- const normalized = trimmed.replace(/,/g, '');
32334
- if (!/^-?\d+(\.\d+)?$/.test(normalized)) {
32335
- return label;
32336
- }
32337
- const numericValue = Number(normalized);
32338
- if (!Number.isFinite(numericValue)) {
32339
- return label;
32340
- }
32341
- const fractionPart = normalized.split('.')[1];
32342
- return new Intl.NumberFormat(undefined, {
32343
- useGrouping: true,
32344
- minimumFractionDigits: fractionPart?.length ?? 0,
32345
- maximumFractionDigits: fractionPart?.length ?? 0
32346
- }).format(numericValue);
32347
- }
32348
32204
  async getRuntimeOptions(field, engine) {
32349
32205
  if (!engine)
32350
32206
  return undefined;
@@ -32503,6 +32359,60 @@ function getHeadingClass(level) {
32503
32359
  }
32504
32360
  }
32505
32361
 
32362
+ function parsePathSegments$1(path) {
32363
+ const segments = [];
32364
+ const matcher = /([^[.\]]+)|\[(\d+)\]/g;
32365
+ let match;
32366
+ while ((match = matcher.exec(path)) !== null) {
32367
+ const [, property, index] = match;
32368
+ if (property) {
32369
+ segments.push(property);
32370
+ continue;
32371
+ }
32372
+ if (index !== undefined) {
32373
+ segments.push(index);
32374
+ }
32375
+ }
32376
+ return segments;
32377
+ }
32378
+ function resolvePathValue(value, path) {
32379
+ if (!path)
32380
+ return value;
32381
+ const normalizedPath = path.trim();
32382
+ if (!normalizedPath)
32383
+ return value;
32384
+ let current = value;
32385
+ for (const segment of parsePathSegments$1(normalizedPath)) {
32386
+ if (current === null || current === undefined) {
32387
+ return undefined;
32388
+ }
32389
+ if (Array.isArray(current)) {
32390
+ const index = Number(segment);
32391
+ if (!Number.isInteger(index)) {
32392
+ return undefined;
32393
+ }
32394
+ current = current[index];
32395
+ continue;
32396
+ }
32397
+ if (typeof current !== 'object') {
32398
+ return undefined;
32399
+ }
32400
+ current = current[segment];
32401
+ }
32402
+ return current;
32403
+ }
32404
+ function valuesMatch(left, right) {
32405
+ if (Object.is(left, right))
32406
+ return true;
32407
+ if (left === undefined || left === null || right === undefined || right === null)
32408
+ return false;
32409
+ const leftIsPrimitive = typeof left === 'string' || typeof left === 'number' || typeof left === 'boolean';
32410
+ const rightIsPrimitive = typeof right === 'string' || typeof right === 'number' || typeof right === 'boolean';
32411
+ if (leftIsPrimitive && rightIsPrimitive) {
32412
+ return String(left) === String(right);
32413
+ }
32414
+ return false;
32415
+ }
32506
32416
  class TextFieldWidgetComponent {
32507
32417
  _config;
32508
32418
  hasReceivedConfig = false;
@@ -32552,7 +32462,7 @@ class TextFieldWidgetComponent {
32552
32462
  return this.config?.type === 'color';
32553
32463
  }
32554
32464
  usesFormattedNumberInput() {
32555
- return this.config?.type === 'number' && this.config?.dataConfig?.formatNumericOptionLabels === true;
32465
+ return this.config?.type === 'number' && usesFieldThousandSeparator(this.config);
32556
32466
  }
32557
32467
  hasDisplayPrefix() {
32558
32468
  return !this.isTextarea() && !this.isColorField() && this.displayPrefix.trim().length > 0;
@@ -32609,7 +32519,7 @@ class TextFieldWidgetComponent {
32609
32519
  const datasourceId = this.config?.dataConfig?.datasourceId;
32610
32520
  if (!datasourceId || update.datasourceId !== datasourceId)
32611
32521
  return;
32612
- void this.refreshValueFromDataSource();
32522
+ void this.refreshValueFromDataSource(true);
32613
32523
  });
32614
32524
  }
32615
32525
  this.engine.valueChanges$
@@ -32617,7 +32527,7 @@ class TextFieldWidgetComponent {
32617
32527
  .subscribe(values => {
32618
32528
  this.syncEnabledState();
32619
32529
  if (this.haveDependencyValuesChanged(values)) {
32620
- void this.refreshValueFromDataSource();
32530
+ void this.refreshValueFromDataSource(true);
32621
32531
  }
32622
32532
  });
32623
32533
  // Listen for submit attempts to show validation errors
@@ -32721,7 +32631,7 @@ class TextFieldWidgetComponent {
32721
32631
  return this.engine ? this.engine.isFieldVisible(this.config.id) : true;
32722
32632
  }
32723
32633
  get enabled() {
32724
- if (this.config?.disabled)
32634
+ if (isFieldDisabled(this.config))
32725
32635
  return false;
32726
32636
  return this.engine ? this.engine.isFieldEnabled(this.config.id) : true;
32727
32637
  }
@@ -32747,14 +32657,23 @@ class TextFieldWidgetComponent {
32747
32657
  this.control.disable({ emitEvent: false });
32748
32658
  }
32749
32659
  }
32750
- async refreshValueFromDataSource() {
32660
+ async refreshValueFromDataSource(clearWhenMissing = false) {
32751
32661
  if (!this.dataProvider || !this.config.dataConfig)
32752
32662
  return;
32753
32663
  try {
32664
+ if (clearWhenMissing && !(await this.hasSelectedRowMatch())) {
32665
+ this.clearResolvedValue();
32666
+ return;
32667
+ }
32754
32668
  const val = await this.dataProvider.getValue(this.config, this.engine);
32755
32669
  this.displayPrefix = await this.dataProvider.getValueDisplayPrefix(this.config, this.engine);
32756
- if (val === undefined || val === null)
32670
+ if (val === undefined || val === null) {
32671
+ if (!clearWhenMissing) {
32672
+ return;
32673
+ }
32674
+ this.clearResolvedValue();
32757
32675
  return;
32676
+ }
32758
32677
  this.control.setValue(val, { emitEvent: false });
32759
32678
  this.syncFormattedNumberValue();
32760
32679
  if (this.engine) {
@@ -32765,12 +32684,48 @@ class TextFieldWidgetComponent {
32765
32684
  // Ignore failed datasource refreshes; field remains editable.
32766
32685
  }
32767
32686
  }
32687
+ clearResolvedValue() {
32688
+ this.displayPrefix = '';
32689
+ this.control.setValue(null, { emitEvent: false });
32690
+ this.syncFormattedNumberValue();
32691
+ if (this.engine) {
32692
+ this.engine.setValue(this.config.name, null);
32693
+ }
32694
+ }
32695
+ async hasSelectedRowMatch() {
32696
+ const cfg = this.config.dataConfig;
32697
+ if (!cfg
32698
+ || cfg.rowSelectionMode !== 'selected'
32699
+ || !cfg.selectionFieldId
32700
+ || !cfg.selectionMatchPath
32701
+ || !this.engine) {
32702
+ return true;
32703
+ }
32704
+ const selectorField = this.engine.getSchema().fields.find(field => field.id === cfg.selectionFieldId);
32705
+ if (!selectorField) {
32706
+ return false;
32707
+ }
32708
+ const selectorValue = this.engine.getValue(selectorField.name);
32709
+ if (selectorValue === undefined || selectorValue === null || selectorValue === '') {
32710
+ return false;
32711
+ }
32712
+ const rows = await this.dataProvider.getList(this.config, this.engine);
32713
+ return rows.some(row => valuesMatch(resolvePathValue(row, cfg.selectionMatchPath), selectorValue));
32714
+ }
32768
32715
  getDataConfigSignature(config) {
32769
32716
  if (!config)
32770
32717
  return '';
32771
32718
  const dataConfig = config.dataConfig;
32772
32719
  if (!dataConfig) {
32773
- return `${config.id}::no-data-config`;
32720
+ return [
32721
+ config.id,
32722
+ config.name,
32723
+ config.type,
32724
+ String(config.html5?.readonly ?? ''),
32725
+ String(config.html5?.disabled ?? ''),
32726
+ String(config.useThousandSeparator ?? ''),
32727
+ 'no-data-config'
32728
+ ].join('::');
32774
32729
  }
32775
32730
  const sourceKey = dataConfig.type === 'source' || dataConfig.type === 'global' || dataConfig.type === 'api'
32776
32731
  ? String(dataConfig.datasourceId ?? '')
@@ -32778,16 +32733,18 @@ class TextFieldWidgetComponent {
32778
32733
  return [
32779
32734
  config.id,
32780
32735
  config.name,
32736
+ String(config.html5?.readonly ?? ''),
32737
+ String(config.html5?.disabled ?? ''),
32781
32738
  dataConfig.type ?? '',
32782
32739
  sourceKey,
32783
32740
  String(dataConfig.valueKey ?? ''),
32784
32741
  String(dataConfig.rowsPath ?? ''),
32785
- String(dataConfig.formatNumericOptionLabels ?? ''),
32786
32742
  String(dataConfig.optionLabelPrefixPath ?? ''),
32787
32743
  String(dataConfig.optionLabelPrefixFieldId ?? ''),
32788
32744
  String(dataConfig.rowSelectionMode ?? ''),
32789
32745
  String(dataConfig.selectionFieldId ?? ''),
32790
32746
  String(dataConfig.selectionMatchPath ?? ''),
32747
+ String(config.useThousandSeparator ?? ''),
32791
32748
  (dataConfig.filters ?? []).map(filter => `${filter.column}:${filter.valueFrom ?? ''}:${filter.fieldId ?? ''}`).join('|')
32792
32749
  ].join('::');
32793
32750
  }
@@ -32922,7 +32879,7 @@ class TextFieldWidgetComponent {
32922
32879
  (blur)="onBlur()"
32923
32880
  (mouseenter)="onMouseEnter()"
32924
32881
  (mouseleave)="onMouseLeave()"
32925
- [readonly]="config.readonly"
32882
+ [readonly]="config.html5?.readonly"
32926
32883
  [attr.aria-required]="required"
32927
32884
  [attr.aria-invalid]="!!error"
32928
32885
  [attr.aria-describedby]="getAriaDescribedBy()"
@@ -32943,7 +32900,7 @@ class TextFieldWidgetComponent {
32943
32900
  (blur)="onBlur()"
32944
32901
  (mouseenter)="onMouseEnter()"
32945
32902
  (mouseleave)="onMouseLeave()"
32946
- [readonly]="config.readonly"
32903
+ [readonly]="config.html5?.readonly"
32947
32904
  [attr.aria-required]="required"
32948
32905
  [attr.aria-invalid]="!!error"
32949
32906
  [attr.aria-describedby]="getAriaDescribedBy()"
@@ -32955,7 +32912,7 @@ class TextFieldWidgetComponent {
32955
32912
  [cpAlphaChannel]="'disabled'"
32956
32913
  [cpOutputFormat]="'hex'"
32957
32914
  [cpFallbackColor]="'#000000'"
32958
- [cpDisabled]="config.readonly || !enabled"
32915
+ [cpDisabled]="config.html5?.readonly || !enabled"
32959
32916
  (colorPickerChange)="onColorPickerChange($event)">
32960
32917
  } @else if (usesFormattedNumberInput()) {
32961
32918
  @if (hasDisplayPrefix()) {
@@ -32977,7 +32934,7 @@ class TextFieldWidgetComponent {
32977
32934
  (blur)="onBlur()"
32978
32935
  (mouseenter)="onMouseEnter()"
32979
32936
  (mouseleave)="onMouseLeave()"
32980
- [readonly]="config.readonly"
32937
+ [readonly]="config.html5?.readonly"
32981
32938
  [disabled]="control.disabled"
32982
32939
  [attr.aria-required]="required"
32983
32940
  [attr.aria-invalid]="!!error"
@@ -33007,7 +32964,7 @@ class TextFieldWidgetComponent {
33007
32964
  (blur)="onBlur()"
33008
32965
  (mouseenter)="onMouseEnter()"
33009
32966
  (mouseleave)="onMouseLeave()"
33010
- [readonly]="config.readonly"
32967
+ [readonly]="config.html5?.readonly"
33011
32968
  [attr.aria-required]="required"
33012
32969
  [attr.aria-invalid]="!!error"
33013
32970
  [attr.aria-describedby]="getAriaDescribedBy()"
@@ -33065,7 +33022,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImpo
33065
33022
  (blur)="onBlur()"
33066
33023
  (mouseenter)="onMouseEnter()"
33067
33024
  (mouseleave)="onMouseLeave()"
33068
- [readonly]="config.readonly"
33025
+ [readonly]="config.html5?.readonly"
33069
33026
  [attr.aria-required]="required"
33070
33027
  [attr.aria-invalid]="!!error"
33071
33028
  [attr.aria-describedby]="getAriaDescribedBy()"
@@ -33086,7 +33043,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImpo
33086
33043
  (blur)="onBlur()"
33087
33044
  (mouseenter)="onMouseEnter()"
33088
33045
  (mouseleave)="onMouseLeave()"
33089
- [readonly]="config.readonly"
33046
+ [readonly]="config.html5?.readonly"
33090
33047
  [attr.aria-required]="required"
33091
33048
  [attr.aria-invalid]="!!error"
33092
33049
  [attr.aria-describedby]="getAriaDescribedBy()"
@@ -33098,7 +33055,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImpo
33098
33055
  [cpAlphaChannel]="'disabled'"
33099
33056
  [cpOutputFormat]="'hex'"
33100
33057
  [cpFallbackColor]="'#000000'"
33101
- [cpDisabled]="config.readonly || !enabled"
33058
+ [cpDisabled]="config.html5?.readonly || !enabled"
33102
33059
  (colorPickerChange)="onColorPickerChange($event)">
33103
33060
  } @else if (usesFormattedNumberInput()) {
33104
33061
  @if (hasDisplayPrefix()) {
@@ -33120,7 +33077,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImpo
33120
33077
  (blur)="onBlur()"
33121
33078
  (mouseenter)="onMouseEnter()"
33122
33079
  (mouseleave)="onMouseLeave()"
33123
- [readonly]="config.readonly"
33080
+ [readonly]="config.html5?.readonly"
33124
33081
  [disabled]="control.disabled"
33125
33082
  [attr.aria-required]="required"
33126
33083
  [attr.aria-invalid]="!!error"
@@ -33150,7 +33107,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImpo
33150
33107
  (blur)="onBlur()"
33151
33108
  (mouseenter)="onMouseEnter()"
33152
33109
  (mouseleave)="onMouseLeave()"
33153
- [readonly]="config.readonly"
33110
+ [readonly]="config.html5?.readonly"
33154
33111
  [attr.aria-required]="required"
33155
33112
  [attr.aria-invalid]="!!error"
33156
33113
  [attr.aria-describedby]="getAriaDescribedBy()"
@@ -33263,12 +33220,12 @@ class FileUploadWidgetComponent {
33263
33220
  return this.engine ? this.engine.isFieldVisible(this.config.id) : true;
33264
33221
  }
33265
33222
  get enabled() {
33266
- if (this.config?.disabled)
33223
+ if (isFieldDisabled(this.config))
33267
33224
  return false;
33268
33225
  return this.engine ? this.engine.isFieldEnabled(this.config.id) : true;
33269
33226
  }
33270
33227
  get isDisabled() {
33271
- return !this.enabled || !!this.config?.readonly;
33228
+ return !this.enabled || isFieldReadonly(this.config);
33272
33229
  }
33273
33230
  get required() {
33274
33231
  return this.engine ? this.engine.isFieldRequired(this.config.id) : !!this.config?.html5?.required;
@@ -33759,7 +33716,7 @@ class SelectWidgetComponent {
33759
33716
  this.runtimeOptionsLoaded = false;
33760
33717
  this.currentSearchTerm = '';
33761
33718
  this.setOptionsFromRaw([]);
33762
- void this.loadOptions(this.currentSearchTerm);
33719
+ void this.loadOptions(this.currentSearchTerm, false, true);
33763
33720
  });
33764
33721
  }
33765
33722
  }
@@ -33902,7 +33859,7 @@ class SelectWidgetComponent {
33902
33859
  return this.engine ? this.engine.isFieldVisible(this.config.id) : true;
33903
33860
  }
33904
33861
  get enabled() {
33905
- if (this.config?.disabled)
33862
+ if (isFieldDisabled(this.config))
33906
33863
  return false;
33907
33864
  return this.engine ? this.engine.isFieldEnabled(this.config.id) : true;
33908
33865
  }
@@ -33959,7 +33916,7 @@ class SelectWidgetComponent {
33959
33916
  event.preventDefault();
33960
33917
  }
33961
33918
  onPillClick() {
33962
- if (this.config.readonly) {
33919
+ if (isFieldReadonly(this.config)) {
33963
33920
  return;
33964
33921
  }
33965
33922
  this.onClick();
@@ -34026,7 +33983,7 @@ class SelectWidgetComponent {
34026
33983
  assign('--fd-select-icon-color', controlStyles['iconColor'] ?? controlStyles['color']);
34027
33984
  return vars;
34028
33985
  }
34029
- async loadOptions(term = '', isSearch = false) {
33986
+ async loadOptions(term = '', isSearch = false, clearStaleSelection = false) {
34030
33987
  if (!this.config)
34031
33988
  return;
34032
33989
  const reqId = ++this.requestId;
@@ -34044,8 +34001,17 @@ class SelectWidgetComponent {
34044
34001
  opts = await this.dataProvider.getOptions(this.config, this.engine);
34045
34002
  }
34046
34003
  if (this.requestId === reqId) {
34047
- this.setOptionsFromRaw(this.withSelectedValueFallbackOptions(opts));
34048
- this.syncStoredFieldLabel(this.control.value);
34004
+ const selectedValue = this.resolveSelectedValueForFallback();
34005
+ const shouldClearStaleSelection = clearStaleSelection
34006
+ && this.hasMeaningfulValue(selectedValue)
34007
+ && !this.hasOptionValue(opts, this.toComparableOptionValue(selectedValue));
34008
+ this.setOptionsFromRaw(shouldClearStaleSelection ? opts : this.withSelectedValueFallbackOptions(opts));
34009
+ if (shouldClearStaleSelection) {
34010
+ this.control.setValue(null);
34011
+ }
34012
+ else {
34013
+ this.syncStoredFieldLabel(this.control.value);
34014
+ }
34049
34015
  this.loading = false;
34050
34016
  this.loadError = null;
34051
34017
  this.cdr.markForCheck();
@@ -34152,6 +34118,9 @@ class SelectWidgetComponent {
34152
34118
  config.id,
34153
34119
  config.name,
34154
34120
  config.type,
34121
+ String(config.html5?.readonly ?? ''),
34122
+ String(config.html5?.disabled ?? ''),
34123
+ String(config.useThousandSeparator ?? ''),
34155
34124
  'no-data-config',
34156
34125
  fieldStaticOptions
34157
34126
  ].join('::');
@@ -34164,11 +34133,13 @@ class SelectWidgetComponent {
34164
34133
  config.id,
34165
34134
  config.name,
34166
34135
  config.type,
34136
+ String(config.html5?.readonly ?? ''),
34137
+ String(config.html5?.disabled ?? ''),
34167
34138
  dataConfig.type ?? '',
34168
34139
  String(dataConfig.datasourceId ?? ''),
34169
34140
  String(dataConfig.labelKey ?? ''),
34170
34141
  String(dataConfig.valueKey ?? ''),
34171
- String(dataConfig.formatNumericOptionLabels ?? ''),
34142
+ String(config.useThousandSeparator ?? ''),
34172
34143
  String(dataConfig.optionLabelPrefixFieldId ?? ''),
34173
34144
  String(dataConfig.searchEnabled ?? ''),
34174
34145
  String(dataConfig.optionsLimit ?? ''),
@@ -34221,15 +34192,18 @@ class SelectWidgetComponent {
34221
34192
  applyDisplayFormatting() {
34222
34193
  this.options = this.rawOptions.map(option => ({
34223
34194
  ...option,
34224
- label: this.formatOptionLabel(option.label)
34195
+ label: this.formatOptionLabel(option)
34225
34196
  }));
34226
34197
  }
34227
- formatOptionLabel(label) {
34228
- const formattedLabel = this.config.dataConfig?.formatNumericOptionLabels
34198
+ formatOptionLabel(option) {
34199
+ const label = option.label;
34200
+ const formattedLabel = usesFieldThousandSeparator(this.config)
34229
34201
  ? this.formatNumericLabel(label)
34230
34202
  : label;
34231
- const prefix = this.resolveOptionLabelPrefix();
34232
- return prefix ? `${prefix} ${formattedLabel}` : formattedLabel;
34203
+ const prefixParts = [option.displayPrefix, this.resolveOptionLabelPrefix()]
34204
+ .map(value => value?.trim())
34205
+ .filter((value) => !!value);
34206
+ return prefixParts.length > 0 ? `${prefixParts.join(' ')} ${formattedLabel}` : formattedLabel;
34233
34207
  }
34234
34208
  resolveOptionLabelPrefix() {
34235
34209
  const prefixFieldId = this.config.dataConfig?.optionLabelPrefixFieldId;
@@ -34312,6 +34286,12 @@ class SelectWidgetComponent {
34312
34286
  hasOptionValue(options, value) {
34313
34287
  return options.some(option => Object.is(option.value, value) || String(option.value) === String(value));
34314
34288
  }
34289
+ toComparableOptionValue(value) {
34290
+ if (typeof value === 'number' && Number.isFinite(value)) {
34291
+ return value;
34292
+ }
34293
+ return String(value ?? '');
34294
+ }
34315
34295
  resolveSelectedValueForFallback() {
34316
34296
  const controlValue = this.control.value;
34317
34297
  if (this.hasMeaningfulValue(controlValue)) {
@@ -34356,7 +34336,7 @@ class SelectWidgetComponent {
34356
34336
  if (this.config.dataConfig?.optionLabelPrefixFieldId) {
34357
34337
  return this.resolveOptionLabelPrefix().length === 0;
34358
34338
  }
34359
- return this.config.dataConfig?.formatNumericOptionLabels !== true;
34339
+ return !usesFieldThousandSeparator(this.config);
34360
34340
  }
34361
34341
  setStoredFieldLabel(label) {
34362
34342
  if (!this.engine || typeof this.engine.setFieldLabel !== 'function') {
@@ -34394,8 +34374,8 @@ class SelectWidgetComponent {
34394
34374
  [loadingText]="'Loading options...'"
34395
34375
  [notFoundText]="loadError || 'No options found'"
34396
34376
  [searchable]="isSearchable()"
34397
- [clearable]="!required && !config.readonly && !hasPillLabel()"
34398
- [readonly]="config.readonly"
34377
+ [clearable]="!required && !config.html5?.readonly && !hasPillLabel()"
34378
+ [readonly]="config.html5?.readonly"
34399
34379
  [labelForId]="fieldId"
34400
34380
  [inputAttrs]="getInputAttributes()"
34401
34381
  [typeahead]="searchTerms$"
@@ -34421,7 +34401,7 @@ class SelectWidgetComponent {
34421
34401
  class="fd-select-pill"
34422
34402
  [ngStyle]="getPillStyles()"
34423
34403
  data-fd-select-pill
34424
- [disabled]="config.readonly"
34404
+ [disabled]="config.html5?.readonly"
34425
34405
  (mousedown)="onPillMouseDown($event)"
34426
34406
  (click)="onPillClick()">
34427
34407
  <span class="fd-select-pill__label">{{ pillLabel() }}</span>
@@ -34474,8 +34454,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImpo
34474
34454
  [loadingText]="'Loading options...'"
34475
34455
  [notFoundText]="loadError || 'No options found'"
34476
34456
  [searchable]="isSearchable()"
34477
- [clearable]="!required && !config.readonly && !hasPillLabel()"
34478
- [readonly]="config.readonly"
34457
+ [clearable]="!required && !config.html5?.readonly && !hasPillLabel()"
34458
+ [readonly]="config.html5?.readonly"
34479
34459
  [labelForId]="fieldId"
34480
34460
  [inputAttrs]="getInputAttributes()"
34481
34461
  [typeahead]="searchTerms$"
@@ -34501,7 +34481,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImpo
34501
34481
  class="fd-select-pill"
34502
34482
  [ngStyle]="getPillStyles()"
34503
34483
  data-fd-select-pill
34504
- [disabled]="config.readonly"
34484
+ [disabled]="config.html5?.readonly"
34505
34485
  (mousedown)="onPillMouseDown($event)"
34506
34486
  (click)="onPillClick()">
34507
34487
  <span class="fd-select-pill__label">{{ pillLabel() }}</span>
@@ -34596,7 +34576,7 @@ class SearchWidgetComponent {
34596
34576
  return this.engine ? this.engine.isFieldVisible(this.config.id) : true;
34597
34577
  }
34598
34578
  get enabled() {
34599
- if (this.config?.disabled)
34579
+ if (isFieldDisabled(this.config))
34600
34580
  return false;
34601
34581
  return this.engine ? this.engine.isFieldEnabled(this.config.id) : true;
34602
34582
  }
@@ -34659,7 +34639,7 @@ class SearchWidgetComponent {
34659
34639
  const datasourceId = this.config?.dataConfig?.datasourceId;
34660
34640
  if (!datasourceId || update.datasourceId !== datasourceId)
34661
34641
  return;
34662
- void this.refreshForCurrentQuery();
34642
+ void this.refreshForCurrentQuery(true);
34663
34643
  });
34664
34644
  }
34665
34645
  this.engine.valueChanges$
@@ -34803,7 +34783,7 @@ class SearchWidgetComponent {
34803
34783
  this.queryControl.disable({ emitEvent: false });
34804
34784
  }
34805
34785
  }
34806
- async refreshForCurrentQuery() {
34786
+ async refreshForCurrentQuery(clearStaleSelection = false) {
34807
34787
  const query = this.queryControl.value ?? '';
34808
34788
  if (!this.shouldQuery(query)) {
34809
34789
  this.options = [];
@@ -34813,7 +34793,7 @@ class SearchWidgetComponent {
34813
34793
  this.cdr.markForCheck();
34814
34794
  return;
34815
34795
  }
34816
- await this.loadOptions(query);
34796
+ await this.loadOptions(query, clearStaleSelection);
34817
34797
  }
34818
34798
  async handleQueryChange(query) {
34819
34799
  if (this.selectingOption) {
@@ -34833,7 +34813,7 @@ class SearchWidgetComponent {
34833
34813
  }
34834
34814
  await this.loadOptions(normalizedQuery);
34835
34815
  }
34836
- async loadOptions(term) {
34816
+ async loadOptions(term, clearStaleSelection = false) {
34837
34817
  const requestId = ++this.requestId;
34838
34818
  this.loading = true;
34839
34819
  this.loadError = null;
@@ -34847,6 +34827,15 @@ class SearchWidgetComponent {
34847
34827
  return;
34848
34828
  }
34849
34829
  this.options = this.normalizeOptions(options);
34830
+ if (clearStaleSelection && this.isSelectedOptionState() && !this.hasOptionValue(this.options, this.control.value)) {
34831
+ this.queryControl.setValue('', { emitEvent: false });
34832
+ this.control.setValue(null);
34833
+ this.loading = false;
34834
+ this.loadError = null;
34835
+ this.activeOptionIndex = -1;
34836
+ this.cdr.markForCheck();
34837
+ return;
34838
+ }
34850
34839
  this.activeOptionIndex = this.options.length > 0 ? 0 : -1;
34851
34840
  this.loading = false;
34852
34841
  this.loadError = null;
@@ -34878,6 +34867,31 @@ class SearchWidgetComponent {
34878
34867
  const minChars = this.getMinChars();
34879
34868
  return (query ?? '').trim().length >= minChars;
34880
34869
  }
34870
+ isSelectedOptionState() {
34871
+ if (!this.hasMeaningfulValue(this.control.value)) {
34872
+ return false;
34873
+ }
34874
+ const storedLabel = this.getStoredFieldLabel();
34875
+ if (typeof storedLabel === 'string' && storedLabel.length > 0) {
34876
+ return true;
34877
+ }
34878
+ return String(this.control.value ?? '') !== String(this.queryControl.value ?? '');
34879
+ }
34880
+ hasOptionValue(options, value) {
34881
+ if (!this.hasMeaningfulValue(value)) {
34882
+ return false;
34883
+ }
34884
+ return options.some(option => Object.is(option.value, value) || String(option.value) === String(value));
34885
+ }
34886
+ hasMeaningfulValue(value) {
34887
+ if (value === undefined || value === null)
34888
+ return false;
34889
+ if (typeof value === 'string')
34890
+ return value.length > 0;
34891
+ if (Array.isArray(value))
34892
+ return value.length > 0;
34893
+ return true;
34894
+ }
34881
34895
  getMinChars() {
34882
34896
  return this.toSafeNonNegativeInt(this.config?.['searchMinChars'], 1);
34883
34897
  }
@@ -34945,7 +34959,7 @@ class SearchWidgetComponent {
34945
34959
  type="search"
34946
34960
  [placeholder]="config.placeholder || 'Search...'"
34947
34961
  [formControl]="queryControl"
34948
- [readonly]="config.readonly"
34962
+ [readonly]="config.html5?.readonly"
34949
34963
  [attr.aria-required]="required"
34950
34964
  [attr.aria-invalid]="!!error"
34951
34965
  [attr.aria-describedby]="getAriaDescribedBy()"
@@ -35024,7 +35038,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImpo
35024
35038
  type="search"
35025
35039
  [placeholder]="config.placeholder || 'Search...'"
35026
35040
  [formControl]="queryControl"
35027
- [readonly]="config.readonly"
35041
+ [readonly]="config.html5?.readonly"
35028
35042
  [attr.aria-required]="required"
35029
35043
  [attr.aria-invalid]="!!error"
35030
35044
  [attr.aria-describedby]="getAriaDescribedBy()"
@@ -35311,7 +35325,7 @@ class TreeSelectWidgetComponent {
35311
35325
  expandRecursive(filtered);
35312
35326
  }
35313
35327
  toggleDropdown(e) {
35314
- if (this.config.disabled || this.config.readonly)
35328
+ if (isFieldDisabled(this.config) || isFieldReadonly(this.config))
35315
35329
  return;
35316
35330
  this.engine?.emitUiEvent({ fieldId: this.config.id, fieldName: this.config.name, type: 'click' });
35317
35331
  if (this.isOpen) {
@@ -35423,8 +35437,8 @@ class TreeSelectWidgetComponent {
35423
35437
  <!-- Dropdown Trigger -->
35424
35438
  <div class="relative group" (click)="toggleDropdown($event)">
35425
35439
  <div class="flex items-center justify-between w-full rounded-md border text-sm px-3 py-2 cursor-pointer transition-colors"
35426
- [class.bg-gray-50]="config.disabled"
35427
- [class.cursor-not-allowed]="config.disabled"
35440
+ [class.bg-gray-50]="config.html5?.disabled"
35441
+ [class.cursor-not-allowed]="config.html5?.disabled"
35428
35442
  [class.border-gray-300]="!isInvalid && !isOpen"
35429
35443
  [class.border-blue-500]="isOpen"
35430
35444
  [class.ring-2]="isOpen"
@@ -35527,8 +35541,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImpo
35527
35541
  <!-- Dropdown Trigger -->
35528
35542
  <div class="relative group" (click)="toggleDropdown($event)">
35529
35543
  <div class="flex items-center justify-between w-full rounded-md border text-sm px-3 py-2 cursor-pointer transition-colors"
35530
- [class.bg-gray-50]="config.disabled"
35531
- [class.cursor-not-allowed]="config.disabled"
35544
+ [class.bg-gray-50]="config.html5?.disabled"
35545
+ [class.cursor-not-allowed]="config.html5?.disabled"
35532
35546
  [class.border-gray-300]="!isInvalid && !isOpen"
35533
35547
  [class.border-blue-500]="isOpen"
35534
35548
  [class.ring-2]="isOpen"
@@ -35778,7 +35792,7 @@ class RadioWidgetComponent {
35778
35792
  return this.engine ? this.engine.isFieldVisible(this.config.id) : true;
35779
35793
  }
35780
35794
  get enabled() {
35781
- if (this.config?.disabled)
35795
+ if (isFieldDisabled(this.config))
35782
35796
  return false;
35783
35797
  return this.engine ? this.engine.isFieldEnabled(this.config.id) : true;
35784
35798
  }
@@ -36094,7 +36108,7 @@ class CheckboxGroupWidgetComponent {
36094
36108
  return this.engine ? this.engine.isFieldVisible(this.config.id) : true;
36095
36109
  }
36096
36110
  get enabled() {
36097
- if (this.config?.disabled)
36111
+ if (isFieldDisabled(this.config))
36098
36112
  return false;
36099
36113
  return this.engine ? this.engine.isFieldEnabled(this.config.id) : true;
36100
36114
  }
@@ -36330,7 +36344,7 @@ class CheckboxWidgetComponent {
36330
36344
  return this.engine ? this.engine.isFieldVisible(this.config.id) : true;
36331
36345
  }
36332
36346
  get enabled() {
36333
- if (this.config?.disabled)
36347
+ if (isFieldDisabled(this.config))
36334
36348
  return false;
36335
36349
  return this.engine ? this.engine.isFieldEnabled(this.config.id) : true;
36336
36350
  }
@@ -36564,7 +36578,7 @@ class RepeatableGroupWidgetComponent {
36564
36578
  return this.engine ? this.engine.isFieldVisible(this.config.id) : true;
36565
36579
  }
36566
36580
  get enabled() {
36567
- if (this.config?.disabled)
36581
+ if (isFieldDisabled(this.config))
36568
36582
  return false;
36569
36583
  return this.engine ? this.engine.isFieldEnabled(this.config.id) : true;
36570
36584
  }
@@ -37899,8 +37913,8 @@ const COMMON_GROUP_FIELDS = [
37899
37913
  { key: 'groupKey', type: 'text', label: 'Group Key', placeholder: 'documents', helpText: 'Group fields into one object on submit/preview (uses the field key inside the group).' }
37900
37914
  ];
37901
37915
  const COMMON_INTERACTIVITY_FIELDS = [
37902
- { key: 'readonly', type: 'checkbox', label: 'Read Only' },
37903
- { key: 'disabled', type: 'checkbox', label: 'Disabled' }
37916
+ { key: 'html5.readonly', type: 'checkbox', label: 'Read Only' },
37917
+ { key: 'html5.disabled', type: 'checkbox', label: 'Disabled' }
37904
37918
  ];
37905
37919
  const COMMON_BASIC_FIELDS = [
37906
37920
  { key: 'name', type: 'text', label: 'Field Key (Name)' },
@@ -37960,16 +37974,10 @@ const BASE_REQUIRED_FIELD = {
37960
37974
  label: 'Required',
37961
37975
  helpText: 'Base required state; Rules can override this dynamically.'
37962
37976
  };
37963
- const CUSTOM_VALIDATION_RULES_FIELD = {
37964
- key: 'validation',
37965
- type: 'validators-editor',
37966
- label: 'Custom Validation Rules',
37967
- helpText: 'Use these for field-level validation checks. Use the Rules tab for conditional behaviour.'
37968
- };
37969
37977
  function createValidationSection(...fields) {
37970
37978
  return {
37971
37979
  label: 'Validation',
37972
- fields: [...fields, CUSTOM_VALIDATION_RULES_FIELD]
37980
+ fields
37973
37981
  };
37974
37982
  }
37975
37983
  const TEXT_VALIDATION_SECTION = createValidationSection(BASE_REQUIRED_FIELD, { key: 'html5.minLength', type: 'number', label: 'Min Length' }, { key: 'html5.maxLength', type: 'number', label: 'Max Length' }, { key: 'html5.pattern', type: 'text', label: 'Pattern (regex)' });
@@ -37979,6 +37987,28 @@ const TIME_VALIDATION_SECTION = createValidationSection(BASE_REQUIRED_FIELD, { k
37979
37987
  const DATETIME_VALIDATION_SECTION = createValidationSection(BASE_REQUIRED_FIELD, { key: 'html5.min', type: 'text', label: 'Earliest (YYYY-MM-DDTHH:MM)' }, { key: 'html5.max', type: 'text', label: 'Latest (YYYY-MM-DDTHH:MM)' });
37980
37988
  const SELECT_VALIDATION_SECTION = createValidationSection(BASE_REQUIRED_FIELD);
37981
37989
  const FILE_VALIDATION_SECTION = createValidationSection(BASE_REQUIRED_FIELD);
37990
+ const NUMBER_DISPLAY_SECTION = {
37991
+ label: 'Display',
37992
+ fields: [
37993
+ {
37994
+ key: 'useThousandSeparator',
37995
+ type: 'checkbox',
37996
+ label: 'Show thousand separators',
37997
+ helpText: 'Formats grouped digits in the visible field value without changing the stored number.'
37998
+ }
37999
+ ]
38000
+ };
38001
+ const SELECT_DISPLAY_SECTION = {
38002
+ label: 'Display',
38003
+ fields: [
38004
+ {
38005
+ key: 'useThousandSeparator',
38006
+ type: 'checkbox',
38007
+ label: 'Show thousand separators for numeric labels',
38008
+ helpText: 'Formats numeric option labels without changing the stored option value.'
38009
+ }
38010
+ ]
38011
+ };
37982
38012
  const BUTTON_VARIANT_OPTIONS = [
37983
38013
  { label: 'Primary (Blue)', value: 'primary' },
37984
38014
  { label: 'Secondary (Outline)', value: 'secondary' }
@@ -38187,6 +38217,7 @@ const FIELD_WIDGETS = [
38187
38217
  ]
38188
38218
  },
38189
38219
  NUMBER_VALIDATION_SECTION,
38220
+ NUMBER_DISPLAY_SECTION,
38190
38221
  { label: 'Appearance', fields: COMMON_APPEARANCE_FIELDS },
38191
38222
  ...COMMON_STYLE_SECTIONS
38192
38223
  ]
@@ -38648,6 +38679,7 @@ const FIELD_WIDGETS = [
38648
38679
  },
38649
38680
  // Options are now handled in Data Tab
38650
38681
  SELECT_VALIDATION_SECTION,
38682
+ SELECT_DISPLAY_SECTION,
38651
38683
  { label: 'Appearance', fields: COMMON_APPEARANCE_FIELDS },
38652
38684
  ...COMMON_STYLE_SECTIONS
38653
38685
  ]
@@ -41463,5 +41495,5 @@ function provideHttpDataSourceClient(config) {
41463
41495
  * Generated bundle index. Do not edit.
41464
41496
  */
41465
41497
 
41466
- export { APPEARANCE_FIELDS, AiToolRegistryService, CONTENT_TEXT_CLASS, CONTENT_TEXT_MUTED_CLASS, CORE_DESIGNER_PLUGINS, CURRENT_SCHEMA_VERSION, DATA_PROVIDER, DATA_SOURCE_CLIENT, DEFAULT_TEMPLATE_LIBRARY, DEFAULT_WEBSITE_SECTIONS, DEFAULT_WIDGET_PACKS, DESIGNER_PLUGINS, DESIGNER_SECTIONS, DataCatalog, DataPanelComponent, DataProvider, DefaultDataProvider, DefaultDataSourceClient, DefaultFileUploadClient, DesignerContext, DesignerStateService, DynamicPropertiesComponent, EFFECTS_FIELDS, EMAIL_SAFE_STYLE_SECTIONS, EMAIL_WIDGETS, EmailRendererComponent, EventsPanelComponent, EventsWorkspaceComponent, FIELD_CHOICE_LABEL_CLASS, FIELD_CHOICE_SURFACE_CLASS, FIELD_CONTAINER_CLASS, FIELD_ERROR_CLASS, FIELD_HELP_CLASS, FIELD_LABEL_CLASS, FIELD_OPTION_CLASS, FIELD_REQUIRED_CLASS, FIELD_RESULTS_PANEL_CLASS, FIELD_WIDGETS, FILE_UPLOAD_CARD_CLASS, FILE_UPLOAD_CLIENT, FORM_DESIGNER_AI_TOOL_PLUGIN, FULL_WEB_STYLE_SECTIONS, FieldPaletteComponent, FormDesignerAiFeatureStateService, FormDesignerShellComponent, FormEngine, FormJourneyStateService, FormJourneyViewerComponent, FormViewerComponent, GlobalDataManagerComponent, HTTP_DATA_SOURCE_CLIENT_CONFIG, HttpDataSourceClient, InMemoryDataCatalogService, InspectorAdvancedSectionComponent, InspectorBackgroundsSectionComponent, InspectorBordersSectionComponent, InspectorEffectsSectionComponent, InspectorLayoutSectionComponent, InspectorPositionSectionComponent, InspectorSizeSectionComponent, InspectorSpacingSectionComponent, InspectorSpinInputComponent, InspectorTypographySectionComponent, JsonFormDesignerComponent, JsonFormRendererComponent, LAYOUT_FIELDS, LayoutCanvasComponent, LayoutNodeComponent, OUTLINED_FIELD_BORDER_WIDTH, OUTLINED_FIELD_IDLE_BORDER_COLOR, OUTLINED_FIELD_INTERACTIVE_BORDER_COLOR, OUTLINED_FIELD_INVALID_BORDER_COLOR, PAGE_WIDGETS, PropertiesPanelComponent, RulesPanelComponent, RuntimeFieldDataAccessRegistryService, SELECT_THEME_HOOKS, SELECT_THEME_TOKENS, SPACING_BOX_MODEL_FIELD, SPACING_MARGIN_FIELDS, SPACING_PADDING_FIELDS, STANDARD_FORM_STYLE_SECTIONS, STYLE_APPEARANCE_SECTION, STYLE_EFFECTS_SECTION, STYLE_LAYOUT_SECTION, STYLE_SECTIONS, STYLE_SPACING_SECTION, STYLE_TRANSFORM_SECTION, STYLE_TYPOGRAPHY_SECTION, TABLE_CELL_CLASS, TABLE_EMPTY_CLASS, TABLE_GRID_CLASS, TABLE_HEAD_CELL_CLASS, TABLE_HEAD_CLASS, TABLE_ICON_BUTTON_CLASS, TABLE_ROOT_CLASS, TABLE_ROW_CLASS, TABLE_TOOLBAR_CLASS, TRANSFORM_FIELDS, TYPOGRAPHY_FIELDS, ThemeService, UiAccordionComponent, UiBoxModelComponent, UiColorSwatchComponent, UiDimensionComponent, UiEdgeBoxComponent, UiFieldWrapperComponent, UiInputComponent, UiRangeNumberComponent, UiSelectIconComponent, UiTabComponent, UiTabsComponent, WIDGET_DEFINITIONS, WIDGET_ID_SEPARATOR, WebsiteBrickStudioComponent, WebsiteDesignerShellComponent, WebsitePreviewShellComponent, WebsiteProjectService, WidgetDefinitionResolverService, WidgetInspectorComponent, appendSectionToSchema, buildWidgetId, checkSchemaForApiOnlySources, createDefaultWebsiteBrick, createDefaultWebsiteBricks, createDefaultWebsiteTheme, createEmptySchema, createFormEngine, createFormJourneyPage, createFormJourneyProject, createPluginContext, createWebsiteBrick, createWebsitePage, createWebsiteProject, defineWidget, flattenPluginWidgets, getButtonClass, getChoiceControlClass, getEffectiveDataConfig, getFileInputClass, getFileUploadShellClass, getHeadingClass, getOutlinedFieldInlineStyle, getTextControlClass, getWidgetsForFlavor, hasWrapperSurfaceStyles, inferSchema, isFormJourneyProject, isWidgetVisibleInPalette, mergeAndNormalize, normalizeRuntimeOptions, normalizeStyle$1 as normalizeStyle, normalizeToJourney, parseCsv, parseJsonArray, parseSchema, provideDesignerPlugins, provideFormDesignerAngaiFeature, provideHttpDataSourceClient, serializeSchema, slugifyId, splitControlSurfaceStyles, stripEmbeddedSourceData, unwrapSinglePageJourney };
41498
+ export { APPEARANCE_FIELDS, AiToolRegistryService, CONTENT_TEXT_CLASS, CONTENT_TEXT_MUTED_CLASS, CORE_DESIGNER_PLUGINS, CURRENT_SCHEMA_VERSION, DATA_PROVIDER, DATA_SOURCE_CLIENT, DEFAULT_TEMPLATE_LIBRARY, DEFAULT_WEBSITE_SECTIONS, DEFAULT_WIDGET_PACKS, DESIGNER_PLUGINS, DESIGNER_SECTIONS, DataCatalog, DataPanelComponent, DataProvider, DefaultDataProvider, DefaultDataSourceClient, DefaultFileUploadClient, DesignerContext, DesignerStateService, DynamicPropertiesComponent, EFFECTS_FIELDS, EMAIL_SAFE_STYLE_SECTIONS, EMAIL_WIDGETS, EmailRendererComponent, EventsPanelComponent, EventsWorkspaceComponent, FIELD_CHOICE_LABEL_CLASS, FIELD_CHOICE_SURFACE_CLASS, FIELD_CONTAINER_CLASS, FIELD_ERROR_CLASS, FIELD_HELP_CLASS, FIELD_LABEL_CLASS, FIELD_OPTION_CLASS, FIELD_REQUIRED_CLASS, FIELD_RESULTS_PANEL_CLASS, FIELD_WIDGETS, FILE_UPLOAD_CARD_CLASS, FILE_UPLOAD_CLIENT, FORM_DESIGNER_AI_TOOL_PLUGIN, FULL_WEB_STYLE_SECTIONS, FieldPaletteComponent, FormDesignerAiFeatureStateService, FormDesignerShellComponent, FormEngine, FormJourneyStateService, FormJourneyViewerComponent, FormViewerComponent, GlobalDataManagerComponent, HTTP_DATA_SOURCE_CLIENT_CONFIG, HttpDataSourceClient, InMemoryDataCatalogService, InspectorAdvancedSectionComponent, InspectorBackgroundsSectionComponent, InspectorBordersSectionComponent, InspectorEffectsSectionComponent, InspectorLayoutSectionComponent, InspectorPositionSectionComponent, InspectorSizeSectionComponent, InspectorSpacingSectionComponent, InspectorSpinInputComponent, InspectorTypographySectionComponent, JsonFormDesignerComponent, JsonFormRendererComponent, LAYOUT_FIELDS, LayoutCanvasComponent, LayoutNodeComponent, OUTLINED_FIELD_BORDER_WIDTH, OUTLINED_FIELD_IDLE_BORDER_COLOR, OUTLINED_FIELD_INTERACTIVE_BORDER_COLOR, OUTLINED_FIELD_INVALID_BORDER_COLOR, PAGE_WIDGETS, PropertiesPanelComponent, RulesPanelComponent, RuntimeFieldDataAccessRegistryService, SELECT_THEME_HOOKS, SELECT_THEME_TOKENS, SPACING_BOX_MODEL_FIELD, SPACING_MARGIN_FIELDS, SPACING_PADDING_FIELDS, STANDARD_FORM_STYLE_SECTIONS, STYLE_APPEARANCE_SECTION, STYLE_EFFECTS_SECTION, STYLE_LAYOUT_SECTION, STYLE_SECTIONS, STYLE_SPACING_SECTION, STYLE_TRANSFORM_SECTION, STYLE_TYPOGRAPHY_SECTION, TABLE_CELL_CLASS, TABLE_EMPTY_CLASS, TABLE_GRID_CLASS, TABLE_HEAD_CELL_CLASS, TABLE_HEAD_CLASS, TABLE_ICON_BUTTON_CLASS, TABLE_ROOT_CLASS, TABLE_ROW_CLASS, TABLE_TOOLBAR_CLASS, TRANSFORM_FIELDS, TYPOGRAPHY_FIELDS, ThemeService, UiAccordionComponent, UiBoxModelComponent, UiColorSwatchComponent, UiDimensionComponent, UiEdgeBoxComponent, UiFieldWrapperComponent, UiInputComponent, UiRangeNumberComponent, UiSelectIconComponent, UiTabComponent, UiTabsComponent, WIDGET_DEFINITIONS, WIDGET_ID_SEPARATOR, WebsiteBrickStudioComponent, WebsiteDesignerShellComponent, WebsitePreviewShellComponent, WebsiteProjectService, WidgetDefinitionResolverService, WidgetInspectorComponent, appendSectionToSchema, buildWidgetId, checkSchemaForApiOnlySources, createDefaultWebsiteBrick, createDefaultWebsiteBricks, createDefaultWebsiteTheme, createEmptySchema, createFormEngine, createFormJourneyPage, createFormJourneyProject, createPluginContext, createWebsiteBrick, createWebsitePage, createWebsiteProject, defineWidget, flattenPluginWidgets, getButtonClass, getChoiceControlClass, getEffectiveDataConfig, getFileInputClass, getFileUploadShellClass, getHeadingClass, getOutlinedFieldInlineStyle, getTextControlClass, getWidgetsForFlavor, hasWrapperSurfaceStyles, inferSchema, isFieldDisabled, isFieldReadonly, isFormJourneyProject, isWidgetVisibleInPalette, mergeAndNormalize, normalizeRuntimeOptions, normalizeStyle$1 as normalizeStyle, normalizeToJourney, parseCsv, parseJsonArray, parseSchema, provideDesignerPlugins, provideFormDesignerAngaiFeature, provideHttpDataSourceClient, serializeSchema, slugifyId, splitControlSurfaceStyles, stripEmbeddedSourceData, unwrapSinglePageJourney, usesFieldThousandSeparator };
41467
41499
  //# sourceMappingURL=uch-web-ngx-form-designer.mjs.map