@fuentis/phoenix-ui 0.0.9-alpha.551 → 0.0.9-alpha.552

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.
@@ -8079,29 +8079,49 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
8079
8079
  }] });
8080
8080
 
8081
8081
  class ReadOnlyInputV2Component {
8082
+ /** Field metadata (type, key, options, labels, etc.) */
8082
8083
  field;
8084
+ /** Parent FormGroup that contains the control referenced by field.configuration.key */
8083
8085
  form;
8086
+ /** Used to automatically unsubscribe from valueChanges on destroy */
8084
8087
  dr = inject(DestroyRef);
8088
+ /**
8089
+ * Internal reactive signal holding the current control value.
8090
+ * We keep a local signal to optimize OnPush change detection and computed selectors.
8091
+ */
8085
8092
  _v = signal(null, ...(ngDevMode ? [{ debugName: "_v" }] : []));
8086
8093
  ngOnInit() {
8094
+ // Initial sync of the control value into the local signal
8087
8095
  this.sync();
8096
+ // Keep local signal in sync with the form control value
8088
8097
  this.ctrl()?.valueChanges
8089
8098
  .pipe(takeUntilDestroyed(this.dr))
8090
8099
  .subscribe(() => this.sync());
8091
8100
  }
8101
+ /** Control key resolved from MetaFieldConfig */
8092
8102
  get key() {
8093
8103
  return this.field?.configuration?.key ?? '';
8094
8104
  }
8105
+ /** Meta field type (TEXT, DATE, ASSIGN, UPLOAD, etc.) */
8095
8106
  get type() {
8096
8107
  return this.field?.configuration?.type ?? 'TEXT';
8097
8108
  }
8109
+ /** Shortcut to the underlying FormControl */
8098
8110
  ctrl() {
8099
8111
  return this.form.get(this.key);
8100
8112
  }
8113
+ /**
8114
+ * Synchronizes the FormControl value into the local signal.
8115
+ * This keeps computed properties reactive and OnPush-friendly.
8116
+ */
8101
8117
  sync() {
8102
8118
  this._v.set(this.ctrl()?.value ?? null);
8103
8119
  }
8104
8120
  // ---------- helpers ----------
8121
+ /**
8122
+ * Resolves language suffix used by backend DTOs (labelKeyValEn/De/Sr).
8123
+ * This is used to pick the correct localized label for option objects.
8124
+ */
8105
8125
  getLangSuffix() {
8106
8126
  const lang = (localStorage.getItem('language') ?? 'en').toLowerCase();
8107
8127
  if (lang.startsWith('de'))
@@ -8110,12 +8130,19 @@ class ReadOnlyInputV2Component {
8110
8130
  return 'Sr';
8111
8131
  return 'En';
8112
8132
  }
8113
- /** bool prikaz (switch/checkbox) */
8133
+ /**
8134
+ * Boolean renderer (for SWITCH / CHECKBOX).
8135
+ * Returns boolean value or null if the value is not a boolean.
8136
+ */
8114
8137
  bool = computed(() => {
8115
8138
  const v = this._v();
8116
8139
  return typeof v === 'boolean' ? v : null;
8117
8140
  }, ...(ngDevMode ? [{ debugName: "bool" }] : []));
8118
- /** DATE podrška: Date ili ISO */
8141
+ /**
8142
+ * DATE renderer.
8143
+ * Supports both Date instances and ISO-like string values.
8144
+ * Returns a valid Date object or null if parsing fails.
8145
+ */
8119
8146
  dateValue = computed(() => {
8120
8147
  const v = this._v();
8121
8148
  if (!v)
@@ -8125,7 +8152,11 @@ class ReadOnlyInputV2Component {
8125
8152
  const d = new Date(String(v));
8126
8153
  return isNaN(d.getTime()) ? null : d;
8127
8154
  }, ...(ngDevMode ? [{ debugName: "dateValue" }] : []));
8128
- /** START_DUE_DATE: {startDate,endDate} (Date ili string) */
8155
+ /**
8156
+ * START_DUE_DATE renderer.
8157
+ * Expects shape: { startDate, endDate } (Date or string).
8158
+ * Returns normalized Date objects or null if invalid.
8159
+ */
8129
8160
  startDue = computed(() => {
8130
8161
  const v = this._v();
8131
8162
  if (!v || typeof v !== 'object')
@@ -8141,7 +8172,10 @@ class ReadOnlyInputV2Component {
8141
8172
  endDate: ed && !isNaN(ed.getTime()) ? ed : null,
8142
8173
  };
8143
8174
  }, ...(ngDevMode ? [{ debugName: "startDue" }] : []));
8144
- /** ASSIGN: očekuje {name,function,email,phone} */
8175
+ /**
8176
+ * ASSIGN renderer.
8177
+ * Normalizes various backend DTO shapes into a unified view model.
8178
+ */
8145
8179
  assign = computed(() => {
8146
8180
  const v = this._v();
8147
8181
  if (!v || typeof v !== 'object')
@@ -8153,7 +8187,10 @@ class ReadOnlyInputV2Component {
8153
8187
  phone: v?.phone ?? null,
8154
8188
  };
8155
8189
  }, ...(ngDevMode ? [{ debugName: "assign" }] : []));
8156
- /** UPLOAD: {fileName,size,type} ili slično */
8190
+ /**
8191
+ * UPLOAD renderer.
8192
+ * Supports common upload DTO shapes (fileName/name, size, type).
8193
+ */
8157
8194
  upload = computed(() => {
8158
8195
  const v = this._v();
8159
8196
  if (!v || typeof v !== 'object')
@@ -8164,16 +8201,21 @@ class ReadOnlyInputV2Component {
8164
8201
  type: v?.type ?? null,
8165
8202
  };
8166
8203
  }, ...(ngDevMode ? [{ debugName: "upload" }] : []));
8167
- /** SS_OPTION i SS_OPTION_OBJECT_BASED: raw ili object */
8204
+ /**
8205
+ * Single-select option label resolver (SS_OPTION / SS_OPTION_OBJECT_BASED).
8206
+ * - If value is primitive -> find matching option in configuration.options
8207
+ * - If value is object -> resolve best display label using common DTO fields
8208
+ */
8168
8209
  ssLabel = computed(() => {
8169
8210
  const v = this._v();
8170
8211
  if (v === null || v === undefined || v === '')
8171
8212
  return null;
8172
- // raw value (npr 'RS') -> nađi u options
8213
+ // Primitive value: resolve label from options list
8173
8214
  if (typeof v !== 'object') {
8174
8215
  const opt = (this.field.configuration.options ?? []).find((x) => x?.value === v);
8175
8216
  return opt?.label ?? String(v);
8176
8217
  }
8218
+ // Object value: resolve localized label from DTO
8177
8219
  const suffix = this.getLangSuffix();
8178
8220
  const key = `labelKeyVal${suffix}`;
8179
8221
  return (v?.label ??
@@ -8183,7 +8225,10 @@ class ReadOnlyInputV2Component {
8183
8225
  v?.value ??
8184
8226
  null);
8185
8227
  }, ...(ngDevMode ? [{ debugName: "ssLabel" }] : []));
8186
- /** MS_OPTION: array raw ili array objekata */
8228
+ /**
8229
+ * Multi-select option label resolver (MS_OPTION).
8230
+ * Joins multiple labels into a single string with truncation after maxItems.
8231
+ */
8187
8232
  msLabel = computed(() => {
8188
8233
  const v = this._v();
8189
8234
  if (!Array.isArray(v) || v.length === 0)
@@ -8201,7 +8246,10 @@ class ReadOnlyInputV2Component {
8201
8246
  ? labels.join(', ')
8202
8247
  : `${labels.slice(0, maxItems).join(', ')} ...`;
8203
8248
  }, ...(ngDevMode ? [{ debugName: "msLabel" }] : []));
8204
- /** general fallback text (siguran za sve tipove) */
8249
+ /**
8250
+ * Generic fallback display value.
8251
+ * Ensures that every field type has a safe string representation.
8252
+ */
8205
8253
  displayText = computed(() => {
8206
8254
  const v = this._v();
8207
8255
  if (v === null || v === undefined || v === '')
@@ -8217,7 +8265,7 @@ class ReadOnlyInputV2Component {
8217
8265
  if (this.type === 'ASSIGN' || this.type === 'ASSIGN_ASSET') {
8218
8266
  return this.assign()?.name ?? null;
8219
8267
  }
8220
- // textarea/editor su često html
8268
+ // Text areas and editors often contain HTML
8221
8269
  if (this.type === 'TEXT_AREA' || this.type === 'TEXT_EDITOR') {
8222
8270
  return typeof v === 'string' ? v : String(v);
8223
8271
  }
@@ -8248,37 +8296,92 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
8248
8296
  }] } });
8249
8297
 
8250
8298
  class MetaAssignResponsibleV2Component {
8251
- /** legacy meta-config data for dialog table */
8252
- items = []; // umesto control.configuration.items
8299
+ /**
8300
+ * List of available assignees used as table data inside the selection dialog.
8301
+ * This replaces legacy `control.configuration.items` access and keeps the CVA independent
8302
+ * from meta-form internals.
8303
+ */
8304
+ items = [];
8305
+ /**
8306
+ * Translation key for the dialog header title.
8307
+ * Kept as an input so different contexts can reuse this field with a custom title.
8308
+ */
8253
8309
  dialogHeaderKey = 'LABELS.ASSIGN_RESPONSIBLE';
8254
8310
  translate = inject(TranslateService);
8255
8311
  dialog = inject(DialogService);
8312
+ /**
8313
+ * Currently selected assignee value bound to the parent form control.
8314
+ * We store the full object (row) because downstream fields often need more than uuid.
8315
+ */
8256
8316
  value = null;
8317
+ /**
8318
+ * Disabled state propagated from Angular forms via `setDisabledState`.
8319
+ * (If you later add a field-level `@Input() disable`, combine them like in other CVAs.)
8320
+ */
8257
8321
  disabled = false;
8322
+ /**
8323
+ * CVA callback invoked when the value changes (selection/clear).
8324
+ */
8258
8325
  onChange = () => { };
8326
+ /**
8327
+ * CVA callback invoked when the control is marked as touched (user interaction).
8328
+ * Standard: call on meaningful interactions (open dialog, select, clear).
8329
+ */
8259
8330
  onTouched = () => { };
8331
+ /**
8332
+ * Called by Angular forms when the model value changes programmatically.
8333
+ * Keep it side-effect free: do not call `onChange`/`onTouched` from here.
8334
+ */
8260
8335
  writeValue(value) {
8261
8336
  this.value = value ?? null;
8262
8337
  }
8338
+ /**
8339
+ * Registers the callback that should be called when the component updates the value.
8340
+ */
8263
8341
  registerOnChange(fn) {
8264
8342
  this.onChange = fn;
8265
8343
  }
8344
+ /**
8345
+ * Registers the callback that should be called when the control becomes "touched".
8346
+ */
8266
8347
  registerOnTouched(fn) {
8267
8348
  this.onTouched = fn;
8268
8349
  }
8350
+ /**
8351
+ * Receives disabled state from Angular forms and updates local state.
8352
+ */
8269
8353
  setDisabledState(isDisabled) {
8270
8354
  this.disabled = isDisabled;
8271
8355
  }
8356
+ /**
8357
+ * Clears current assignee value.
8358
+ * - emits `null`
8359
+ * - marks as touched (user action)
8360
+ */
8272
8361
  clear() {
8273
8362
  if (this.disabled)
8274
8363
  return;
8364
+ // prevent redundant emits
8365
+ if (this.value === null) {
8366
+ this.onTouched();
8367
+ return;
8368
+ }
8275
8369
  this.value = null;
8276
8370
  this.onChange(null);
8277
8371
  this.onTouched();
8278
8372
  }
8373
+ /**
8374
+ * Opens object selection dialog for choosing a new assignee.
8375
+ * The dialog renders a generic table and returns the selected row on close.
8376
+ *
8377
+ * Standard for CVA:
8378
+ * - mark as touched when user opens the picker (interaction started)
8379
+ * - emit onChange only when a row is actually selected
8380
+ */
8279
8381
  openDialog() {
8280
8382
  if (this.disabled)
8281
8383
  return;
8384
+ this.onTouched();
8282
8385
  const ref = this.dialog.open(ObjectItemDialogComponent, {
8283
8386
  header: this.translate.instant(this.dialogHeaderKey),
8284
8387
  width: '700px',
@@ -8299,8 +8402,13 @@ class MetaAssignResponsibleV2Component {
8299
8402
  ref?.onClose.subscribe((response) => {
8300
8403
  if (!response)
8301
8404
  return;
8405
+ // prevent redundant emits (same selection)
8406
+ const same = (this.value?.uuid ?? null) === (response?.uuid ?? null) &&
8407
+ JSON.stringify(this.value ?? null) === JSON.stringify(response ?? null);
8302
8408
  this.value = response;
8303
- this.onChange(response);
8409
+ if (!same) {
8410
+ this.onChange(response);
8411
+ }
8304
8412
  this.onTouched();
8305
8413
  });
8306
8414
  }
@@ -8461,32 +8569,107 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
8461
8569
  }] } });
8462
8570
 
8463
8571
  class MetaColorPickerV2Component {
8464
- /** optional: parent-level disable (pored reactive disable) */
8572
+ /**
8573
+ * Optional external disable flag (field-level disable coming from meta config).
8574
+ * This is combined with the disabled state coming from Angular Forms (CVA).
8575
+ */
8465
8576
  disable = false;
8466
- /** current value (hex string, npr "#ff00aa") */
8577
+ cdr = inject(ChangeDetectorRef);
8578
+ /**
8579
+ * Currently selected color value.
8580
+ * Expected format: HEX string (e.g. "#ff00aa") or null.
8581
+ */
8467
8582
  value = null;
8583
+ /**
8584
+ * CVA callback invoked when the value changes.
8585
+ */
8468
8586
  onChange = () => { };
8587
+ /**
8588
+ * CVA callback invoked when the control is marked as touched.
8589
+ * Standard: call on blur / close, not on every value change.
8590
+ */
8469
8591
  onTouched = () => { };
8592
+ /**
8593
+ * Disabled state coming from Angular Forms (ControlValueAccessor).
8594
+ */
8470
8595
  isDisabled = false;
8596
+ /**
8597
+ * Final disabled state combining:
8598
+ * - form-level disabled state (CVA)
8599
+ * - field-level disable flag (input)
8600
+ */
8601
+ get disabled() {
8602
+ return this.disable || this.isDisabled;
8603
+ }
8604
+ /**
8605
+ * Writes a new value from the parent form control into the component.
8606
+ * Keep it idempotent and UI-safe.
8607
+ */
8471
8608
  writeValue(v) {
8472
- this.value = v ?? null;
8609
+ this.value = this.normalizeHex(v);
8610
+ this.cdr.markForCheck();
8473
8611
  }
8612
+ /**
8613
+ * Registers callback that is triggered when the value changes.
8614
+ */
8474
8615
  registerOnChange(fn) {
8475
8616
  this.onChange = fn;
8476
8617
  }
8618
+ /**
8619
+ * Registers callback that is triggered when the control is touched.
8620
+ */
8477
8621
  registerOnTouched(fn) {
8478
8622
  this.onTouched = fn;
8479
8623
  }
8624
+ /**
8625
+ * Receives disabled state from Angular Forms and updates local state.
8626
+ */
8480
8627
  setDisabledState(isDisabled) {
8481
8628
  this.isDisabled = isDisabled;
8629
+ this.cdr.markForCheck();
8482
8630
  }
8483
- // PrimeNG emituje event, ali nama treba value
8631
+ /**
8632
+ * Handler for PrimeNG color picker change event.
8633
+ * Propagates the currently selected color value to the parent form control.
8634
+ *
8635
+ * Note: we intentionally do NOT call onTouched here (our standard is: touched on blur).
8636
+ */
8484
8637
  onPickerChange() {
8485
- this.onChange(this.value);
8638
+ if (this.disabled)
8639
+ return;
8640
+ const next = this.normalizeHex(this.value);
8641
+ // prevent redundant emits (useful when PrimeNG fires multiple times)
8642
+ if (next === this.value) {
8643
+ this.cdr.markForCheck();
8644
+ return;
8645
+ }
8646
+ this.value = next;
8647
+ this.onChange(next);
8648
+ this.cdr.markForCheck();
8649
+ }
8650
+ /**
8651
+ * Marks control as touched when user leaves the component.
8652
+ * (Matches "touched on blur" CVA guideline.)
8653
+ */
8654
+ handleBlur() {
8655
+ if (this.disabled)
8656
+ return;
8486
8657
  this.onTouched();
8487
8658
  }
8488
- get disabled() {
8489
- return this.disable || this.isDisabled;
8659
+ /**
8660
+ * Normalizes incoming values to a safe HEX string or null.
8661
+ * - accepts "#RRGGBB" / "#RGB" / "RRGGBB"
8662
+ * - returns null for empty/invalid inputs
8663
+ */
8664
+ normalizeHex(v) {
8665
+ if (v === null || v === undefined)
8666
+ return null;
8667
+ const s = String(v).trim();
8668
+ if (!s)
8669
+ return null;
8670
+ const withHash = s.startsWith('#') ? s : `#${s}`;
8671
+ const ok = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(withHash);
8672
+ return ok ? withHash.toLowerCase() : null;
8490
8673
  }
8491
8674
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: MetaColorPickerV2Component, deps: [], target: i0.ɵɵFactoryTarget.Component });
8492
8675
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.16", type: MetaColorPickerV2Component, isStandalone: true, selector: "phoenix-meta-color-picker-v2", inputs: { disable: "disable" }, providers: [
@@ -8496,28 +8679,26 @@ class MetaColorPickerV2Component {
8496
8679
  multi: true,
8497
8680
  },
8498
8681
  ], ngImport: i0, template: `
8499
-
8500
- <p-colorPicker
8501
- class="color-swatch"
8502
- [(ngModel)]="value"
8503
- (onChange)="onPickerChange()"
8504
- (onBlur)="onTouched()"
8505
- [disabled]="disabled"
8506
- [appendTo]="'body'"
8682
+ <p-colorPicker
8683
+ class="color-swatch"
8684
+ [(ngModel)]="value"
8685
+ (onChange)="onPickerChange()"
8686
+ (onBlur)="handleBlur()"
8687
+ [disabled]="disabled"
8688
+ [appendTo]="'body'"
8507
8689
  ></p-colorPicker>
8508
8690
  `, isInline: true, styles: [":host ::ng-deep .color-swatch.p-colorpicker{width:35px;height:35px;padding:0!important;border:none!important;background:transparent!important;box-shadow:none!important}:host ::ng-deep .color-swatch .p-colorpicker-preview{width:35px!important;height:35px!important;border:none!important;border-radius:6px!important;box-shadow:none!important}:host ::ng-deep .color-swatch .p-colorpicker-preview:focus,:host ::ng-deep .color-swatch .p-colorpicker-preview:focus-visible{outline:none!important;box-shadow:none!important}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2$3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2$3.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: ColorPickerModule }, { kind: "component", type: i2$8.ColorPicker, selector: "p-colorPicker, p-colorpicker, p-color-picker", inputs: ["styleClass", "inline", "format", "tabindex", "inputId", "autoZIndex", "showTransitionOptions", "hideTransitionOptions", "autofocus", "defaultColor", "appendTo"], outputs: ["onChange", "onShow", "onHide"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
8509
8691
  }
8510
8692
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: MetaColorPickerV2Component, decorators: [{
8511
8693
  type: Component,
8512
8694
  args: [{ selector: 'phoenix-meta-color-picker-v2', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [CommonModule, FormsModule, ColorPickerModule], template: `
8513
-
8514
- <p-colorPicker
8515
- class="color-swatch"
8516
- [(ngModel)]="value"
8517
- (onChange)="onPickerChange()"
8518
- (onBlur)="onTouched()"
8519
- [disabled]="disabled"
8520
- [appendTo]="'body'"
8695
+ <p-colorPicker
8696
+ class="color-swatch"
8697
+ [(ngModel)]="value"
8698
+ (onChange)="onPickerChange()"
8699
+ (onBlur)="handleBlur()"
8700
+ [disabled]="disabled"
8701
+ [appendTo]="'body'"
8521
8702
  ></p-colorPicker>
8522
8703
  `, providers: [
8523
8704
  {
@@ -8531,50 +8712,137 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
8531
8712
  }] } });
8532
8713
 
8533
8714
  class MetaCheckboxColorPickerV2Component {
8715
+ /**
8716
+ * 2D array of color values used to render the color grid in the popover.
8717
+ * Example:
8718
+ * [
8719
+ * ['#ff0000', '#00ff00', '#0000ff'],
8720
+ * ['#facc15', '#22c55e', '#0ea5e9']
8721
+ * ]
8722
+ */
8534
8723
  options = [];
8724
+ /**
8725
+ * External disable flag (e.g. meta-form field-level disable).
8726
+ * This is combined with CVA disabled state.
8727
+ */
8535
8728
  disable = false;
8536
8729
  cdr = inject(ChangeDetectorRef);
8730
+ /**
8731
+ * Currently selected color value bound to the parent form control.
8732
+ */
8537
8733
  value = null;
8734
+ /**
8735
+ * Color currently focused/hovered in the UI.
8736
+ * Used only for visual outline highlight in the picker grid.
8737
+ */
8538
8738
  focusedColor = null;
8739
+ /**
8740
+ * Disabled state coming from Angular Forms (ControlValueAccessor).
8741
+ */
8539
8742
  isDisabled = false;
8743
+ /**
8744
+ * CVA callback invoked when the value changes.
8745
+ */
8540
8746
  onChange = () => { };
8747
+ /**
8748
+ * CVA callback invoked when the control is marked as touched.
8749
+ * Standard: touched on "open/blur/close" actions, not necessarily on hover.
8750
+ */
8541
8751
  onTouched = () => { };
8542
- // ----- CVA -----
8752
+ /**
8753
+ * Final disabled state combining:
8754
+ * - form-level disabled state (CVA)
8755
+ * - field-level disable flag (input)
8756
+ */
8757
+ get disabled() {
8758
+ return this.disable || this.isDisabled;
8759
+ }
8760
+ // ----- ControlValueAccessor implementation -----
8761
+ /**
8762
+ * Writes a new value from the parent form into the component.
8763
+ * Keeps internal value and focusedColor in sync for correct UI outline.
8764
+ */
8543
8765
  writeValue(v) {
8544
- this.value = v ?? null;
8766
+ this.value = this.normalizeColor(v);
8545
8767
  this.focusedColor = this.value;
8546
8768
  this.cdr.markForCheck();
8547
8769
  }
8770
+ /**
8771
+ * Registers callback that is triggered when the value changes.
8772
+ */
8548
8773
  registerOnChange(fn) {
8549
8774
  this.onChange = fn;
8550
8775
  }
8776
+ /**
8777
+ * Registers callback that is triggered when the control is touched.
8778
+ */
8551
8779
  registerOnTouched(fn) {
8552
8780
  this.onTouched = fn;
8553
8781
  }
8782
+ /**
8783
+ * Receives disabled state from Angular Forms and updates local state.
8784
+ */
8554
8785
  setDisabledState(isDisabled) {
8555
8786
  this.isDisabled = isDisabled;
8556
8787
  this.cdr.markForCheck();
8557
8788
  }
8558
- get disabled() {
8559
- return this.disable || this.isDisabled;
8560
- }
8561
8789
  // ----- UI handlers -----
8790
+ /**
8791
+ * Toggles the popover visibility when the selected-color button is clicked.
8792
+ * We mark as touched because user interacted with the control.
8793
+ */
8562
8794
  toggle(popover, ev) {
8563
8795
  if (this.disabled)
8564
8796
  return;
8565
8797
  this.onTouched();
8566
8798
  popover.toggle(ev);
8567
8799
  }
8800
+ /**
8801
+ * Handles color selection from the grid:
8802
+ * - updates internal value
8803
+ * - propagates value to parent form (onChange)
8804
+ * - marks control as touched (selection is a meaningful interaction)
8805
+ * - closes the popover
8806
+ */
8568
8807
  select(color, popover) {
8569
8808
  if (this.disabled)
8570
8809
  return;
8571
- this.focusedColor = color;
8572
- this.value = color;
8573
- this.onChange(color);
8810
+ const next = this.normalizeColor(color);
8811
+ // prevent redundant emits
8812
+ if (next === this.value) {
8813
+ this.focusedColor = next;
8814
+ this.onTouched();
8815
+ this.cdr.markForCheck();
8816
+ popover.hide();
8817
+ return;
8818
+ }
8819
+ this.focusedColor = next;
8820
+ this.value = next;
8821
+ this.onChange(next);
8574
8822
  this.onTouched();
8575
8823
  this.cdr.markForCheck();
8576
8824
  popover.hide();
8577
8825
  }
8826
+ /**
8827
+ * Normalizes incoming values to a safe CSS color string or null.
8828
+ * For this component we keep it permissive:
8829
+ * - accepts "#RRGGBB" / "#RGB"
8830
+ * - also allows any non-empty string (in case someone passes "red" or "var(--x)")
8831
+ * - returns null for empty values
8832
+ */
8833
+ normalizeColor(v) {
8834
+ if (v === null || v === undefined)
8835
+ return null;
8836
+ const s = String(v).trim();
8837
+ if (!s)
8838
+ return null;
8839
+ // normalize common hex values (with/without '#')
8840
+ const withHash = s.startsWith('#') ? s : `#${s}`;
8841
+ if (/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(withHash))
8842
+ return withHash.toLowerCase();
8843
+ // fallback: allow CSS colors (e.g. "red") or CSS vars
8844
+ return s;
8845
+ }
8578
8846
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: MetaCheckboxColorPickerV2Component, deps: [], target: i0.ɵɵFactoryTarget.Component });
8579
8847
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.16", type: MetaCheckboxColorPickerV2Component, isStandalone: true, selector: "phoenix-meta-checkbox-color-picker-v2", inputs: { options: "options", disable: "disable" }, providers: [
8580
8848
  {
@@ -8660,50 +8928,154 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
8660
8928
  }] } });
8661
8929
 
8662
8930
  class MetaStartDueDateV2Component {
8663
- /** opcioni data-cy prefix ako ti treba (nije obavezno) */
8931
+ /**
8932
+ * Optional data-cy attribute prefix for e2e testing.
8933
+ * Can be used by parent components to uniquely identify this field in tests.
8934
+ */
8664
8935
  dataCy;
8665
- disabled = false;
8936
+ /**
8937
+ * Optional parent-level disable flag (in addition to reactive-form disable).
8938
+ * Use this when the field must be disabled due to page/business logic.
8939
+ */
8940
+ disable = false;
8941
+ cdr = inject(ChangeDetectorRef);
8942
+ /**
8943
+ * Disabled state coming from Angular Forms (ControlValueAccessor).
8944
+ * When true, both date pickers are non-interactive.
8945
+ */
8946
+ isDisabled = false;
8947
+ /**
8948
+ * Local UI state for the selected start date.
8949
+ * Normalized to Date or null for PrimeNG DatePicker compatibility.
8950
+ */
8666
8951
  startDate = null;
8952
+ /**
8953
+ * Local UI state for the selected end date.
8954
+ * Normalized to Date or null for PrimeNG DatePicker compatibility.
8955
+ */
8667
8956
  endDate = null;
8957
+ /**
8958
+ * CVA callback invoked when the composite value changes.
8959
+ */
8668
8960
  onChange = () => { };
8961
+ /**
8962
+ * CVA callback invoked when the control is marked as touched.
8963
+ * IMPORTANT: We call this on blur (not on every change) to match standard CVA behavior.
8964
+ */
8669
8965
  onTouched = () => { };
8966
+ /**
8967
+ * Effective disabled state used by the template.
8968
+ * Combines reactive form disable + parent-level disable.
8969
+ */
8970
+ get disabled() {
8971
+ return this.disable || this.isDisabled;
8972
+ }
8973
+ /**
8974
+ * Writes a new value from the parent form control into the component.
8975
+ * Incoming values are normalized to Date instances for the UI layer.
8976
+ *
8977
+ * NOTE: "YYYY-MM-DD" strings are parsed as local dates to avoid timezone day-shifts.
8978
+ */
8670
8979
  writeValue(v) {
8671
- const sd = v?.startDate ? new Date(v.startDate) : null;
8672
- const ed = v?.endDate ? new Date(v.endDate) : null;
8673
- this.startDate = sd && !isNaN(sd.getTime()) ? sd : null;
8674
- this.endDate = ed && !isNaN(ed.getTime()) ? ed : null;
8980
+ this.startDate = this.parseToDate(v?.startDate ?? null);
8981
+ this.endDate = this.parseToDate(v?.endDate ?? null);
8982
+ // OnPush: ensure UI reflects external value writes (patchValue/setValue)
8983
+ this.cdr.markForCheck();
8675
8984
  }
8985
+ /**
8986
+ * Registers the callback that should be called when the value changes.
8987
+ */
8676
8988
  registerOnChange(fn) {
8677
8989
  this.onChange = fn;
8678
8990
  }
8991
+ /**
8992
+ * Registers the callback that should be called when the control is touched.
8993
+ */
8679
8994
  registerOnTouched(fn) {
8680
8995
  this.onTouched = fn;
8681
8996
  }
8997
+ /**
8998
+ * Receives disabled state from Angular Forms and updates local state.
8999
+ */
8682
9000
  setDisabledState(isDisabled) {
8683
- this.disabled = isDisabled;
9001
+ this.isDisabled = isDisabled;
9002
+ // OnPush: reflect disabled state changes immediately
9003
+ this.cdr.markForCheck();
8684
9004
  }
9005
+ /**
9006
+ * Handler used by PrimeNG DatePicker blur events.
9007
+ * Marks the control as touched without emitting a value change.
9008
+ */
9009
+ handleBlur() {
9010
+ if (this.disabled)
9011
+ return;
9012
+ this.onTouched();
9013
+ }
9014
+ /**
9015
+ * Handler for start date change coming from the DatePicker.
9016
+ * Updates local state and propagates the composite value.
9017
+ */
8685
9018
  onStartChange(d) {
9019
+ if (this.disabled)
9020
+ return;
8686
9021
  this.startDate = d ?? null;
8687
- this.emit();
9022
+ this.emitChange();
8688
9023
  }
9024
+ /**
9025
+ * Handler for end date change coming from the DatePicker.
9026
+ * Updates local state and propagates the composite value.
9027
+ */
8689
9028
  onEndChange(d) {
9029
+ if (this.disabled)
9030
+ return;
8690
9031
  this.endDate = d ?? null;
8691
- this.emit();
9032
+ this.emitChange();
8692
9033
  }
8693
- emit() {
8694
- this.onTouched();
8695
- // ako su oba null -> tretiraj kao null (čistije za required)
9034
+ /**
9035
+ * Emits the composite value to the parent form control.
9036
+ * - Emits `null` when both dates are empty (cleaner semantics for required/bothDates validators).
9037
+ * - Otherwise emits the `{ startDate, endDate }` object (partial values allowed; validator decides).
9038
+ */
9039
+ emitChange() {
8696
9040
  if (!this.startDate && !this.endDate) {
8697
9041
  this.onChange(null);
9042
+ this.cdr.markForCheck();
8698
9043
  return;
8699
9044
  }
8700
9045
  this.onChange({
8701
9046
  startDate: this.startDate,
8702
9047
  endDate: this.endDate,
8703
9048
  });
9049
+ this.cdr.markForCheck();
9050
+ }
9051
+ /**
9052
+ * Parses Date | string safely into a Date instance for the UI.
9053
+ * Important: "YYYY-MM-DD" is treated as a local date (prevents timezone day-shift).
9054
+ */
9055
+ parseToDate(v) {
9056
+ if (!v)
9057
+ return null;
9058
+ if (v instanceof Date) {
9059
+ return isNaN(v.getTime()) ? null : v;
9060
+ }
9061
+ const s = String(v).trim();
9062
+ if (!s)
9063
+ return null;
9064
+ // Date-only string -> create LOCAL date (avoid UTC shifting)
9065
+ const m = s.match(/^(\d{4})-(\d{2})-(\d{2})$/);
9066
+ if (m) {
9067
+ const y = Number(m[1]);
9068
+ const mo = Number(m[2]) - 1;
9069
+ const d = Number(m[3]);
9070
+ const local = new Date(y, mo, d);
9071
+ return isNaN(local.getTime()) ? null : local;
9072
+ }
9073
+ // ISO / other -> fallback
9074
+ const dt = new Date(s);
9075
+ return isNaN(dt.getTime()) ? null : dt;
8704
9076
  }
8705
9077
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: MetaStartDueDateV2Component, deps: [], target: i0.ɵɵFactoryTarget.Component });
8706
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.16", type: MetaStartDueDateV2Component, isStandalone: true, selector: "phoenix-meta-start-due-date-v2", inputs: { dataCy: "dataCy" }, providers: [
9078
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.16", type: MetaStartDueDateV2Component, isStandalone: true, selector: "phoenix-meta-start-due-date-v2", inputs: { dataCy: "dataCy", disable: "disable" }, providers: [
8707
9079
  {
8708
9080
  provide: NG_VALUE_ACCESSOR,
8709
9081
  useExisting: forwardRef(() => MetaStartDueDateV2Component),
@@ -8722,6 +9094,7 @@ class MetaStartDueDateV2Component {
8722
9094
  [disabled]="disabled"
8723
9095
  [ngModel]="startDate"
8724
9096
  (ngModelChange)="onStartChange($event)"
9097
+ (onBlur)="handleBlur()"
8725
9098
  appendTo="body"
8726
9099
  ></p-datepicker>
8727
9100
  </div>
@@ -8740,16 +9113,17 @@ class MetaStartDueDateV2Component {
8740
9113
  [disabled]="disabled"
8741
9114
  [ngModel]="endDate"
8742
9115
  (ngModelChange)="onEndChange($event)"
9116
+ (onBlur)="handleBlur()"
8743
9117
  appendTo="body"
8744
9118
  ></p-datepicker>
8745
9119
  </div>
8746
9120
  </div>
8747
9121
  </div>
8748
- `, isInline: true, styles: [":host{display:block}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2$3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2$3.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: TranslateModule }, { kind: "ngmodule", type: DatePickerModule }, { kind: "component", type: i2$7.DatePicker, selector: "p-datePicker, p-datepicker, p-date-picker", inputs: ["iconDisplay", "styleClass", "inputStyle", "inputId", "inputStyleClass", "placeholder", "ariaLabelledBy", "ariaLabel", "iconAriaLabel", "dateFormat", "multipleSeparator", "rangeSeparator", "inline", "showOtherMonths", "selectOtherMonths", "showIcon", "icon", "readonlyInput", "shortYearCutoff", "hourFormat", "timeOnly", "stepHour", "stepMinute", "stepSecond", "showSeconds", "showOnFocus", "showWeek", "startWeekFromFirstDayOfYear", "showClear", "dataType", "selectionMode", "maxDateCount", "showButtonBar", "todayButtonStyleClass", "clearButtonStyleClass", "autofocus", "autoZIndex", "baseZIndex", "panelStyleClass", "panelStyle", "keepInvalid", "hideOnDateTimeSelect", "touchUI", "timeSeparator", "focusTrap", "showTransitionOptions", "hideTransitionOptions", "tabindex", "minDate", "maxDate", "disabledDates", "disabledDays", "showTime", "responsiveOptions", "numberOfMonths", "firstDayOfWeek", "view", "defaultDate", "appendTo"], outputs: ["onFocus", "onBlur", "onClose", "onSelect", "onClear", "onInput", "onTodayClick", "onClearClick", "onMonthChange", "onYearChange", "onClickOutside", "onShow"] }, { kind: "pipe", type: i3$2.TranslatePipe, name: "translate" }] });
9122
+ `, isInline: true, styles: [":host{display:block}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2$3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2$3.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: TranslateModule }, { kind: "ngmodule", type: DatePickerModule }, { kind: "component", type: i2$7.DatePicker, selector: "p-datePicker, p-datepicker, p-date-picker", inputs: ["iconDisplay", "styleClass", "inputStyle", "inputId", "inputStyleClass", "placeholder", "ariaLabelledBy", "ariaLabel", "iconAriaLabel", "dateFormat", "multipleSeparator", "rangeSeparator", "inline", "showOtherMonths", "selectOtherMonths", "showIcon", "icon", "readonlyInput", "shortYearCutoff", "hourFormat", "timeOnly", "stepHour", "stepMinute", "stepSecond", "showSeconds", "showOnFocus", "showWeek", "startWeekFromFirstDayOfYear", "showClear", "dataType", "selectionMode", "maxDateCount", "showButtonBar", "todayButtonStyleClass", "clearButtonStyleClass", "autofocus", "autoZIndex", "baseZIndex", "panelStyleClass", "panelStyle", "keepInvalid", "hideOnDateTimeSelect", "touchUI", "timeSeparator", "focusTrap", "showTransitionOptions", "hideTransitionOptions", "tabindex", "minDate", "maxDate", "disabledDates", "disabledDays", "showTime", "responsiveOptions", "numberOfMonths", "firstDayOfWeek", "view", "defaultDate", "appendTo"], outputs: ["onFocus", "onBlur", "onClose", "onSelect", "onClear", "onInput", "onTodayClick", "onClearClick", "onMonthChange", "onYearChange", "onClickOutside", "onShow"] }, { kind: "pipe", type: i3$2.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
8749
9123
  }
8750
9124
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: MetaStartDueDateV2Component, decorators: [{
8751
9125
  type: Component,
8752
- args: [{ selector: 'phoenix-meta-start-due-date-v2', standalone: true, imports: [CommonModule, FormsModule, TranslateModule, DatePickerModule], providers: [
9126
+ args: [{ selector: 'phoenix-meta-start-due-date-v2', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [CommonModule, FormsModule, TranslateModule, DatePickerModule], providers: [
8753
9127
  {
8754
9128
  provide: NG_VALUE_ACCESSOR,
8755
9129
  useExisting: forwardRef(() => MetaStartDueDateV2Component),
@@ -8768,6 +9142,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
8768
9142
  [disabled]="disabled"
8769
9143
  [ngModel]="startDate"
8770
9144
  (ngModelChange)="onStartChange($event)"
9145
+ (onBlur)="handleBlur()"
8771
9146
  appendTo="body"
8772
9147
  ></p-datepicker>
8773
9148
  </div>
@@ -8786,6 +9161,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
8786
9161
  [disabled]="disabled"
8787
9162
  [ngModel]="endDate"
8788
9163
  (ngModelChange)="onEndChange($event)"
9164
+ (onBlur)="handleBlur()"
8789
9165
  appendTo="body"
8790
9166
  ></p-datepicker>
8791
9167
  </div>
@@ -8794,18 +9170,35 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
8794
9170
  `, styles: [":host{display:block}\n"] }]
8795
9171
  }], propDecorators: { dataCy: [{
8796
9172
  type: Input
9173
+ }], disable: [{
9174
+ type: Input
8797
9175
  }] } });
8798
9176
 
8799
9177
  class MetaFormFieldV2Component {
9178
+ /** Metadata definition of the field (type, key, options, styles, flags, etc.) */
8800
9179
  field;
9180
+ /** Parent FormGroup that contains the FormControl for this field */
8801
9181
  form;
8802
- /** External "page read-only" state; when true, we render read-only fields (like V1). */
9182
+ /**
9183
+ * Page-level read-only flag.
9184
+ * When true, the component renders ReadOnlyInputV2Component instead of editable controls.
9185
+ */
8803
9186
  readOnly = false;
8804
- // ako hoćeš global readOnly/disable (npr. parent toggles)
9187
+ /**
9188
+ * Global disable flag (e.g. parent dialog toggles entire form disabled).
9189
+ * This is merged with field-level disable configuration.
9190
+ */
8805
9191
  disableForm = false;
9192
+ /** Used to manually trigger change detection for OnPush strategy */
8806
9193
  cdr = inject(ChangeDetectorRef);
9194
+ /** Used to automatically unsubscribe from value/status streams on destroy */
8807
9195
  dr = inject(DestroyRef);
9196
+ /** Translation service for validation and display labels */
8808
9197
  translate = inject(TranslateService);
9198
+ /**
9199
+ * Exposed enum-like mapping of MetaFieldType for template usage.
9200
+ * Keeps templates readable and avoids magic strings.
9201
+ */
8809
9202
  MetaFieldType = Object.freeze({
8810
9203
  TEXT: 'TEXT',
8811
9204
  NUMBER: 'NUMBER',
@@ -8831,61 +9224,95 @@ class MetaFormFieldV2Component {
8831
9224
  LINKS_DATA: 'LINKS_DATA',
8832
9225
  SLOT: 'SLOT',
8833
9226
  });
9227
+ /** Control key resolved from MetaFieldConfig */
8834
9228
  get key() {
8835
9229
  return this.field?.configuration?.key ?? '';
8836
9230
  }
9231
+ /** Field type resolved from MetaFieldConfig */
8837
9232
  get type() {
8838
9233
  return this.field?.configuration?.type ?? 'TEXT';
8839
9234
  }
9235
+ /** Column width class for grid layout (falls back to default if not provided) */
8840
9236
  get colClass() {
8841
- return this.field?.hidden ? 'p-0' : (this.field?.style.colWidth ?? 'col-12 md:col-6');
9237
+ return this.field?.hidden
9238
+ ? 'p-0'
9239
+ : (this.field?.style.colWidth ?? 'col-12 md:col-6');
8842
9240
  }
8843
9241
  ngOnInit() {
8844
9242
  const ctrl = this.ctrl();
8845
9243
  if (!ctrl)
8846
9244
  return;
9245
+ /**
9246
+ * Subscribe to both valueChanges and statusChanges so the component:
9247
+ * - re-renders when user changes the value
9248
+ * - re-renders when validation state changes (touched/dirty/errors)
9249
+ */
8847
9250
  merge(ctrl.valueChanges, ctrl.statusChanges)
8848
9251
  .pipe(takeUntilDestroyed(this.dr))
8849
9252
  .subscribe(() => this.cdr.markForCheck());
8850
9253
  }
9254
+ /** Human-friendly label defined in metadata (already localized key) */
8851
9255
  userFriendlyMessage() {
8852
9256
  return this.field?.userFriendlyMessage ?? null;
8853
9257
  }
9258
+ /** Optional placeholder i18n key defined in metadata */
8854
9259
  placeholderKey() {
8855
9260
  return this.field?.configuration?.placeholderKey ?? null;
8856
9261
  }
9262
+ /**
9263
+ * Resolves final read-only state for this field:
9264
+ * - page-level readOnly OR field-level readOnly
9265
+ */
8857
9266
  isReadOnly() {
8858
9267
  return !!this.readOnly || !!this.field?.readOnly;
8859
9268
  }
9269
+ /**
9270
+ * Resolves final disabled state for this field:
9271
+ * - page-level disable OR field-level disable
9272
+ */
8860
9273
  isDisabled() {
8861
9274
  return !!this.disableForm || !!this.field?.disable;
8862
9275
  }
9276
+ /** Shortcut to underlying FormControl */
8863
9277
  ctrl() {
8864
9278
  return this.form.get(this.key);
8865
9279
  }
8866
- /** Used by read-only renderer. Keep it simple, same as V1 behavior. */
9280
+ /**
9281
+ * Minimal value formatter for legacy read-only rendering.
9282
+ * Kept intentionally simple to match V1 behavior.
9283
+ */
8867
9284
  displayValue() {
8868
9285
  const v = this.ctrl()?.value;
8869
9286
  if (v === null || v === undefined)
8870
9287
  return '';
8871
9288
  if (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean')
8872
9289
  return v;
8873
- // common DTO shapes (e.g. assign, option objects, upload)
9290
+ // Common DTO shapes (assign, option objects, uploads, etc.)
8874
9291
  if (typeof v === 'object') {
8875
9292
  return v.label ?? v.name ?? v.fileName ?? JSON.stringify(v);
8876
9293
  }
8877
9294
  return String(v);
8878
9295
  }
9296
+ /**
9297
+ * Determines whether validation error should be displayed.
9298
+ * Errors are shown only after user interaction (touched or dirty).
9299
+ */
8879
9300
  showError() {
8880
9301
  const c = this.ctrl();
8881
9302
  return !!c && (c.touched || c.dirty) && !!c.errors;
8882
9303
  }
8883
- /** mapira error key (sync + submit-only async) */
9304
+ /**
9305
+ * Maps control error object to a normalized error key.
9306
+ * Supports:
9307
+ * - Angular built-in validators
9308
+ * - Phoenix custom validators
9309
+ * - Submit-only async validators
9310
+ */
8884
9311
  errorKey() {
8885
9312
  const c = this.ctrl();
8886
9313
  if (!c?.errors)
8887
9314
  return null;
8888
- // sync (Angular built-ins)
9315
+ // Angular built-in validators
8889
9316
  if (c.errors['required'])
8890
9317
  return 'required';
8891
9318
  if (c.errors['minlength'])
@@ -8900,27 +9327,31 @@ class MetaFormFieldV2Component {
8900
9327
  return 'min';
8901
9328
  if (c.errors['max'])
8902
9329
  return 'max';
8903
- // custom validators (Phoenix)
9330
+ // Phoenix custom validators
8904
9331
  if (c.errors['dangerousChars'])
8905
9332
  return 'dangerousChars';
8906
9333
  if (c.errors['timeperiod'])
8907
9334
  return 'timeperiod';
8908
9335
  if (c.errors['invalidDate'])
8909
- return 'invalidDate'; // ako negde postoji
9336
+ return 'invalidDate';
8910
9337
  if (c.errors['dueDate'])
8911
9338
  return 'dueDate';
8912
9339
  if (c.errors['bothDates'])
8913
9340
  return 'bothDates';
8914
- // submit-only async
9341
+ // Submit-only async validators
8915
9342
  if (c.errors['unique'])
8916
9343
  return 'unique';
8917
9344
  if (c.errors['uniqueEntry'])
8918
9345
  return 'uniqueEntry';
8919
9346
  if (c.errors['custom'])
8920
9347
  return 'custom';
9348
+ // Fallback: return first error key
8921
9349
  return Object.keys(c.errors)[0] ?? null;
8922
9350
  }
8923
- /** minimal: posle prevežeš na i18n ključeve */
9351
+ /**
9352
+ * Resolves translated error message based on errorKey().
9353
+ * This is the single place responsible for validation message UX.
9354
+ */
8924
9355
  errorText() {
8925
9356
  const c = this.ctrl();
8926
9357
  const k = this.errorKey();
@@ -8942,11 +9373,12 @@ class MetaFormFieldV2Component {
8942
9373
  case 'dangerousChars':
8943
9374
  return this.translate.instant('VALIDATION_MESSAGE.NO_SPECIAL_CHARS_ALLOWED');
8944
9375
  case 'custom':
8945
- // legacy ponašanje: custom može biti string ključ poruke
9376
+ // Legacy behavior: custom error can already be a translation key
8946
9377
  return this.translate.instant(c.errors?.['custom']);
8947
9378
  case 'uniqueEntry':
8948
- // legacy: uniqueEntry je već tekst (ili već prevedeno)
8949
- return c.errors?.['uniqueEntry'] ?? this.translate.instant('VALIDATION_MESSAGE.VALUE_IS_ALREADY_IN_USE');
9379
+ // Legacy behavior: uniqueEntry may already be a translated string
9380
+ return (c.errors?.['uniqueEntry'] ??
9381
+ this.translate.instant('VALIDATION_MESSAGE.VALUE_IS_ALREADY_IN_USE'));
8950
9382
  case 'unique':
8951
9383
  return this.translate.instant('VALIDATION_MESSAGE.VALUE_IS_ALREADY_IN_USE');
8952
9384
  case 'timeperiod':
@@ -8961,7 +9393,7 @@ class MetaFormFieldV2Component {
8961
9393
  upperValue: c.errors?.['max']?.max,
8962
9394
  });
8963
9395
  case 'pattern': {
8964
- // isti spec-case kao u starom InlineFieldError
9396
+ // Special-case URL pattern handling (legacy InlineFieldError behavior)
8965
9397
  const re = '^(https?://)?([\\da-z.-]+)\\.([a-z.]{2,6})[/\\w .-]*/?$';
8966
9398
  const requiredPattern = c.errors?.['pattern']?.requiredPattern;
8967
9399
  if (requiredPattern === re) {
@@ -8973,42 +9405,47 @@ class MetaFormFieldV2Component {
8973
9405
  return this.translate.instant('VALIDATION_MESSAGE.INVALID_VALUE');
8974
9406
  }
8975
9407
  }
9408
+ /**
9409
+ * Lightweight text formatter for simple read-only display use cases.
9410
+ * This is used mainly for inline displays and summary UIs.
9411
+ */
8976
9412
  valueText() {
8977
9413
  const c = this.ctrl();
8978
9414
  const v = c?.value;
8979
9415
  if (v === null || v === undefined || v === '')
8980
9416
  return '--';
8981
- // SS_OPTION value object {label,value} ili raw
9417
+ // Single-select option: resolve label from options
8982
9418
  if (this.type === 'SS_OPTION') {
8983
9419
  const opts = this.field?.configuration?.options ?? [];
8984
9420
  if (typeof v !== 'object') {
8985
9421
  const hit = opts.find((o) => o?.value === v);
8986
9422
  const label = hit?.label ?? v;
8987
- // ako je label i18n key
8988
9423
  return this.translate.instant(label);
8989
9424
  }
8990
- // ako je objekat
9425
+ // Object value fallback
8991
9426
  const label = v.label ?? v.value;
8992
9427
  return this.translate.instant(label);
8993
9428
  }
8994
- // DATE
9429
+ // Date formatting
8995
9430
  if (this.type === 'DATE' && v instanceof Date) {
8996
9431
  return v.toLocaleDateString();
8997
9432
  }
8998
- // TEXT_EDITOR / TEXT_AREA: strip html (minimalno)
9433
+ // Text editor / textarea: strip basic HTML tags for compact display
8999
9434
  if (this.type === 'TEXT_EDITOR' || this.type === 'TEXT_AREA') {
9000
9435
  return String(v).replace(/<[^>]*>/g, '').trim() || '--';
9001
9436
  }
9002
9437
  return String(v);
9003
9438
  }
9004
9439
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: MetaFormFieldV2Component, deps: [], target: i0.ɵɵFactoryTarget.Component });
9005
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.16", type: MetaFormFieldV2Component, isStandalone: true, selector: "phoenix-meta-form-field-v2", inputs: { field: "field", form: "form", readOnly: "readOnly", disableForm: "disableForm" }, ngImport: i0, template: "<div [formGroup]=\"form\">\n @if (!field.hidden) {\n <div class=\"meta-field flex flex-column gap-2\" [style.order]=\"field.order ?? null\"\n [attr.data-cy]=\"'meta-field-' + key\">\n\n @if (userFriendlyMessage()) {\n <label class=\"meta-label\" [attr.for]=\"key\">\n {{ userFriendlyMessage()! | translate }}\n @if (field.mandatory) { <span class=\"meta-required\">*</span> }\n </label>\n }\n\n <!-- READ ONLY (page-level ili field-level) -->\n @if (isReadOnly()) {\n <phoenix-read-only-input-v2 [field]=\"field\" [form]=\"form\"></phoenix-read-only-input-v2>\n } @else {\n\n @switch (type) {\n\n @case (MetaFieldType.TEXT) {\n <input pInputText [id]=\"key\" [formControlName]=\"key\"\n [attr.placeholder]=\"placeholderKey() ? (placeholderKey()! | translate) : null\" [readonly]=\"isReadOnly()\">\n }\n\n @case (MetaFieldType.PASSWORD) {\n <!-- Ako nema\u0161 legacy komponentu, mo\u017Ee\u0161 samo input type=\"password\" -->\n <phoenix-meta-password-feild [disable]=\"isDisabled()\" [formControlName]=\"key\" [control]=\"field\" [parentForm]=\"form\">\n </phoenix-meta-password-feild>\n }\n\n @case (MetaFieldType.TEXT_AREA) {\n <textarea pTextarea class=\"meta-textarea\" [id]=\"key\" [formControlName]=\"key\" fluid [autoResize]=\"false\" rows=\"5\"\n [readonly]=\"isReadOnly()\" [attr.placeholder]=\"placeholderKey() ? (placeholderKey()! | translate) : null\">\n </textarea>\n }\n\n @case (MetaFieldType.NUMBER) {\n <p-inputNumber [inputId]=\"key\" [formControlName]=\"key\">\n </p-inputNumber>\n }\n\n @case (MetaFieldType.DATE) {\n <p-datepicker [inputId]=\"key\" [formControlName]=\"key\" [showIcon]=\"true\">\n </p-datepicker>\n }\n\n @case (MetaFieldType.SS_OPTION) {\n <p-select [inputId]=\"key\" [options]=\"field.configuration.options ?? []\" optionLabel=\"label\" optionValue=\"value\"\n [formControlName]=\"key\" [showClear]=\"false\">\n </p-select>\n }\n\n @case (MetaFieldType.SS_OPTION_OBJECT_BASED) {\n <p-select [inputId]=\"key\" [options]=\"field.configuration.options ?? []\" optionLabel=\"label\" [formControlName]=\"key\"\n [showClear]=\"true\">\n </p-select>\n }\n\n @case (MetaFieldType.MS_OPTION) {\n <p-multiselect [inputId]=\"key\" [options]=\"field.configuration.options ?? []\" optionLabel=\"label\" optionValue=\"value\"\n [formControlName]=\"key\" [showClear]=\"false\" display=\"chip\">\n </p-multiselect>\n }\n\n @case (MetaFieldType.CHECKBOX) {\n <div class=\"flex align-items-center gap-2\">\n <p-checkbox [inputId]=\"key\" [binary]=\"true\" [formControlName]=\"key\">\n </p-checkbox>\n\n @if (placeholderKey()) {\n <label [attr.for]=\"key\" class=\"meta-inline-label\">\n {{ placeholderKey()! | translate }}\n </label>\n }\n </div>\n }\n\n <!-- advanced: preko postoje\u0107ih komponenti -->\n @case (MetaFieldType.TIMEPERIOD) {\n <phoenix-meta-timeperiod [formControlName]=\"key\" [control]=\"field\" [parentForm]=\"form\"></phoenix-meta-timeperiod>\n }\n\n @case (MetaFieldType.CURRENCY) {\n <phoenix-meta-currency [disable]=\"isDisabled()\" [formControlName]=\"key\" [control]=\"field\"\n [parentForm]=\"form\"></phoenix-meta-currency>\n }\n\n @case (MetaFieldType.START_DUE_DATE) {\n <phoenix-meta-start-due-date-v2\n [formControlName]=\"key\"\n [attr.data-cy]=\"'start-due-' + key\">\n </phoenix-meta-start-due-date-v2>\n }\n\n @case (MetaFieldType.TEXT_EDITOR) {\n <phoenix-meta-text-editor [disable]=\"isDisabled()\" [formControlName]=\"key\" [control]=\"field\"\n [parentForm]=\"form\"></phoenix-meta-text-editor>\n }\n\n @case (MetaFieldType.CHECKBOX_COLOR) {\n <phoenix-meta-checkbox-color-picker-v2\n [formControlName]=\"key\"\n [options]=\"(field.configuration.extra?.['colorGrid'] ?? [])\"\n [disable]=\"isDisabled()\">\n </phoenix-meta-checkbox-color-picker-v2>\n }\n\n @case (MetaFieldType.SWITCH) {\n <phoenix-meta-switch [disable]=\"isDisabled()\" [formControlName]=\"key\" [control]=\"field\"\n [parentForm]=\"form\"></phoenix-meta-switch>\n }\n\n @case (MetaFieldType.SELECT_BUTTON) {\n <phoenix-meta-select-button [disable]=\"isDisabled()\" [formControlName]=\"key\" [control]=\"field\"\n [parentForm]=\"form\"></phoenix-meta-select-button>\n }\n\n @case (MetaFieldType.ASSIGN) {\n <phoenix-meta-assign-responsible-v2\n [formControlName]=\"key\"\n [items]=\"(field.configuration.extra?.['items'] ?? [])\"\n [dialogHeaderKey]=\"(field.configuration.extra?.['dialogHeaderKey'] ?? 'LABELS.ASSIGN_RESPONSIBLE')\"\n ></phoenix-meta-assign-responsible-v2>\n }\n\n <!-- @case (MetaFieldType.ASSIGN_ASSET) {\n <phoenix-meta-assign-asset [disable]=\"isDisabled()\" [formControlName]=\"key\" [control]=\"field\"\n [parentForm]=\"form\"></phoenix-meta-assign-asset>\n } -->\n\n @case (MetaFieldType.COLOR) {\n <phoenix-meta-color-picker-v2\n [formControlName]=\"key\"\n [disable]=\"isDisabled()\">\n </phoenix-meta-color-picker-v2>\n }\n\n @case (MetaFieldType.UPLOAD) {\n <phoenix-meta-upload [disable]=\"isDisabled()\" [formControlName]=\"key\" [control]=\"field\"\n [parentForm]=\"form\"></phoenix-meta-upload>\n }\n\n @case (MetaFieldType.UPLOAD_DRAG_DROP) {\n <phoenix-meta-upload-dragdrop [disable]=\"isDisabled()\" [formControlName]=\"key\" [control]=\"field\"\n [parentForm]=\"form\"></phoenix-meta-upload-dragdrop>\n }\n\n @case (MetaFieldType.LINKS_DATA) {\n <!-- <input pInputText [id]=\"key\" [formControlName]=\"key\" [readonly]=\"true\"> -->\n }\n\n @case (MetaFieldType.SLOT) { }\n\n @default {\n <input pInputText [id]=\"key\" [formControlName]=\"key\">\n }\n }\n }\n\n\n @if (!readOnly && showError()) {\n <small class=\"p-error block mt-1\">\n <i class=\"pi pi-info-circle mr-1\"></i>{{ errorText() }}\n </small>\n }\n </div>\n }\n</div>", styles: [".meta-field{width:100%}.meta-required{margin-left:4px;color:#ef4444}.meta-textarea{resize:none!important}.meta-inline-label{opacity:.9}.p-inputtext.ng-invalid.ng-dirty{border-color:var(--p-inputtext-border-color)!important}.p-select.ng-invalid.ng-dirty{border-color:var(--p-select-border-color)!important}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i2$3.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: i2$3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2$3.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i2$3.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i2$3.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "ngmodule", type: TranslateModule }, { kind: "ngmodule", type:
9006
- // PrimeNG 20
9440
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.16", type: MetaFormFieldV2Component, isStandalone: true, selector: "phoenix-meta-form-field-v2", inputs: { field: "field", form: "form", readOnly: "readOnly", disableForm: "disableForm" }, ngImport: i0, template: "<div [formGroup]=\"form\">\n @if (!field.hidden) {\n <div class=\"meta-field flex flex-column gap-2\" [style.order]=\"field.order ?? null\"\n [attr.data-cy]=\"'meta-field-' + key\">\n\n @if (userFriendlyMessage()) {\n <label class=\"meta-label\" [attr.for]=\"key\">\n {{ userFriendlyMessage()! | translate }}\n @if (field.mandatory) { <span class=\"meta-required\">*</span> }\n </label>\n }\n\n <!-- READ ONLY (page-level ili field-level) -->\n @if (isReadOnly()) {\n <phoenix-read-only-input-v2 [field]=\"field\" [form]=\"form\"></phoenix-read-only-input-v2>\n } @else {\n\n @switch (type) {\n\n @case (MetaFieldType.TEXT) {\n <input pInputText [id]=\"key\" [formControlName]=\"key\"\n [attr.placeholder]=\"placeholderKey() ? (placeholderKey()! | translate) : null\" [readonly]=\"isReadOnly()\">\n }\n\n @case (MetaFieldType.PASSWORD) {\n <phoenix-meta-password-feild [disable]=\"isDisabled()\" [formControlName]=\"key\" [control]=\"field\" [parentForm]=\"form\">\n </phoenix-meta-password-feild>\n }\n\n @case (MetaFieldType.TEXT_AREA) {\n <textarea pTextarea class=\"meta-textarea\" [id]=\"key\" [formControlName]=\"key\" fluid [autoResize]=\"false\" rows=\"5\"\n [readonly]=\"isReadOnly()\" [attr.placeholder]=\"placeholderKey() ? (placeholderKey()! | translate) : null\">\n </textarea>\n }\n\n @case (MetaFieldType.NUMBER) {\n <p-inputNumber [inputId]=\"key\" [formControlName]=\"key\">\n </p-inputNumber>\n }\n\n @case (MetaFieldType.DATE) {\n <p-datepicker [inputId]=\"key\" [formControlName]=\"key\" [showIcon]=\"true\">\n </p-datepicker>\n }\n\n @case (MetaFieldType.SS_OPTION) {\n <p-select [inputId]=\"key\" [options]=\"field.configuration.options ?? []\" optionLabel=\"label\" optionValue=\"value\"\n [formControlName]=\"key\" [showClear]=\"false\">\n </p-select>\n }\n\n @case (MetaFieldType.SS_OPTION_OBJECT_BASED) {\n <p-select [inputId]=\"key\" [options]=\"field.configuration.options ?? []\" optionLabel=\"label\" [formControlName]=\"key\"\n [showClear]=\"true\">\n </p-select>\n }\n\n @case (MetaFieldType.MS_OPTION) {\n <p-multiselect [inputId]=\"key\" [options]=\"field.configuration.options ?? []\" optionLabel=\"label\" optionValue=\"value\"\n [formControlName]=\"key\" [showClear]=\"false\" display=\"chip\">\n </p-multiselect>\n }\n\n @case (MetaFieldType.CHECKBOX) {\n <div class=\"flex align-items-center gap-2\">\n <p-checkbox [inputId]=\"key\" [binary]=\"true\" [formControlName]=\"key\">\n </p-checkbox>\n\n @if (placeholderKey()) {\n <label [attr.for]=\"key\" class=\"meta-inline-label\">\n {{ placeholderKey()! | translate }}\n </label>\n }\n </div>\n }\n\n <!-- advanced: preko postoje\u0107ih komponenti -->\n @case (MetaFieldType.TIMEPERIOD) {\n <phoenix-meta-timeperiod [formControlName]=\"key\" [control]=\"field\" [parentForm]=\"form\"></phoenix-meta-timeperiod>\n }\n\n @case (MetaFieldType.CURRENCY) {\n <phoenix-meta-currency [disable]=\"isDisabled()\" [formControlName]=\"key\" [control]=\"field\"\n [parentForm]=\"form\"></phoenix-meta-currency>\n }\n\n @case (MetaFieldType.START_DUE_DATE) {\n <phoenix-meta-start-due-date-v2\n [formControlName]=\"key\"\n [attr.data-cy]=\"'start-due-' + key\">\n </phoenix-meta-start-due-date-v2>\n }\n\n @case (MetaFieldType.TEXT_EDITOR) {\n <phoenix-meta-text-editor [disable]=\"isDisabled()\" [formControlName]=\"key\" [control]=\"field\"\n [parentForm]=\"form\"></phoenix-meta-text-editor>\n }\n\n @case (MetaFieldType.CHECKBOX_COLOR) {\n <phoenix-meta-checkbox-color-picker-v2\n [formControlName]=\"key\"\n [options]=\"(field.configuration.extra?.['colorGrid'] ?? [])\"\n [disable]=\"isDisabled()\">\n </phoenix-meta-checkbox-color-picker-v2>\n }\n\n @case (MetaFieldType.SWITCH) {\n <phoenix-meta-switch [disable]=\"isDisabled()\" [formControlName]=\"key\" [control]=\"field\"\n [parentForm]=\"form\"></phoenix-meta-switch>\n }\n\n @case (MetaFieldType.SELECT_BUTTON) {\n <phoenix-meta-select-button [disable]=\"isDisabled()\" [formControlName]=\"key\" [control]=\"field\"\n [parentForm]=\"form\"></phoenix-meta-select-button>\n }\n\n @case (MetaFieldType.ASSIGN) {\n <phoenix-meta-assign-responsible-v2\n [formControlName]=\"key\"\n [items]=\"(field.configuration.extra?.['items'] ?? [])\"\n [dialogHeaderKey]=\"(field.configuration.extra?.['dialogHeaderKey'] ?? 'LABELS.ASSIGN_RESPONSIBLE')\"\n ></phoenix-meta-assign-responsible-v2>\n }\n\n <!-- @case (MetaFieldType.ASSIGN_ASSET) {\n <phoenix-meta-assign-asset [disable]=\"isDisabled()\" [formControlName]=\"key\" [control]=\"field\"\n [parentForm]=\"form\"></phoenix-meta-assign-asset>\n } -->\n\n @case (MetaFieldType.COLOR) {\n <phoenix-meta-color-picker-v2\n [formControlName]=\"key\"\n [disable]=\"isDisabled()\">\n </phoenix-meta-color-picker-v2>\n }\n\n @case (MetaFieldType.UPLOAD) {\n <phoenix-meta-upload [disable]=\"isDisabled()\" [formControlName]=\"key\" [control]=\"field\"\n [parentForm]=\"form\"></phoenix-meta-upload>\n }\n\n @case (MetaFieldType.UPLOAD_DRAG_DROP) {\n <phoenix-meta-upload-dragdrop [disable]=\"isDisabled()\" [formControlName]=\"key\" [control]=\"field\"\n [parentForm]=\"form\"></phoenix-meta-upload-dragdrop>\n }\n\n @case (MetaFieldType.LINKS_DATA) {\n <!-- <input pInputText [id]=\"key\" [formControlName]=\"key\" [readonly]=\"true\"> -->\n }\n\n @case (MetaFieldType.SLOT) { }\n\n @default {\n <input pInputText [id]=\"key\" [formControlName]=\"key\">\n }\n }\n }\n\n\n @if (!readOnly && showError()) {\n <small class=\"p-error block mt-1\">\n <i class=\"pi pi-info-circle mr-1\"></i>{{ errorText() }}\n </small>\n }\n </div>\n }\n</div>", styles: [".meta-field{width:100%}.meta-required{margin-left:4px;color:#ef4444}.meta-textarea{resize:none!important}.meta-inline-label{opacity:.9}.p-inputtext.ng-invalid.ng-dirty{border-color:var(--p-inputtext-border-color)!important}.p-select.ng-invalid.ng-dirty{border-color:var(--p-select-border-color)!important}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i2$3.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: i2$3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2$3.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i2$3.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i2$3.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "ngmodule", type: TranslateModule }, { kind: "ngmodule", type:
9441
+ // PrimeNG 20 base inputs
9007
9442
  InputTextModule }, { kind: "directive", type: i3$4.InputText, selector: "[pInputText]", inputs: ["hostName", "ptInputText", "pSize", "variant", "fluid", "invalid"] }, { kind: "ngmodule", type: TextareaModule }, { kind: "directive", type: i3$8.Textarea, selector: "[pTextarea], [pInputTextarea]", inputs: ["autoResize", "pSize", "variant", "fluid", "invalid"], outputs: ["onResize"] }, { kind: "ngmodule", type: InputNumberModule }, { kind: "component", type: i3$6.InputNumber, selector: "p-inputNumber, p-inputnumber, p-input-number", inputs: ["showButtons", "format", "buttonLayout", "inputId", "styleClass", "placeholder", "tabindex", "title", "ariaLabelledBy", "ariaDescribedBy", "ariaLabel", "ariaRequired", "autocomplete", "incrementButtonClass", "decrementButtonClass", "incrementButtonIcon", "decrementButtonIcon", "readonly", "allowEmpty", "locale", "localeMatcher", "mode", "currency", "currencyDisplay", "useGrouping", "minFractionDigits", "maxFractionDigits", "prefix", "suffix", "inputStyle", "inputStyleClass", "showClear", "autofocus"], outputs: ["onInput", "onFocus", "onBlur", "onKeyDown", "onClear"] }, { kind: "ngmodule", type: CheckboxModule }, { kind: "component", type: i4.Checkbox, selector: "p-checkbox, p-checkBox, p-check-box", inputs: ["hostName", "value", "binary", "ariaLabelledBy", "ariaLabel", "tabindex", "inputId", "inputStyle", "styleClass", "inputClass", "indeterminate", "formControl", "checkboxIcon", "readonly", "autofocus", "trueValue", "falseValue", "variant", "size"], outputs: ["onChange", "onFocus", "onBlur"] }, { kind: "ngmodule", type: MultiSelectModule }, { kind: "component", type: i10.MultiSelect, selector: "p-multiSelect, p-multiselect, p-multi-select", inputs: ["id", "ariaLabel", "styleClass", "panelStyle", "panelStyleClass", "inputId", "readonly", "group", "filter", "filterPlaceHolder", "filterLocale", "overlayVisible", "tabindex", "dataKey", "ariaLabelledBy", "displaySelectedLabel", "maxSelectedLabels", "selectionLimit", "selectedItemsLabel", "showToggleAll", "emptyFilterMessage", "emptyMessage", "resetFilterOnHide", "dropdownIcon", "chipIcon", "optionLabel", "optionValue", "optionDisabled", "optionGroupLabel", "optionGroupChildren", "showHeader", "filterBy", "scrollHeight", "lazy", "virtualScroll", "loading", "virtualScrollItemSize", "loadingIcon", "virtualScrollOptions", "overlayOptions", "ariaFilterLabel", "filterMatchMode", "tooltip", "tooltipPosition", "tooltipPositionStyle", "tooltipStyleClass", "autofocusFilter", "display", "autocomplete", "showClear", "autofocus", "placeholder", "options", "filterValue", "selectAll", "focusOnHover", "filterFields", "selectOnFocus", "autoOptionFocus", "highlightOnSelect", "size", "variant", "fluid", "appendTo"], outputs: ["onChange", "onFilter", "onFocus", "onBlur", "onClick", "onClear", "onPanelShow", "onPanelHide", "onLazyLoad", "onRemove", "onSelectAllChange"] }, { kind: "ngmodule", type: SelectModule }, { kind: "component", type: i3$5.Select, selector: "p-select", inputs: ["id", "scrollHeight", "filter", "panelStyle", "styleClass", "panelStyleClass", "readonly", "editable", "tabindex", "placeholder", "loadingIcon", "filterPlaceholder", "filterLocale", "inputId", "dataKey", "filterBy", "filterFields", "autofocus", "resetFilterOnHide", "checkmark", "dropdownIcon", "loading", "optionLabel", "optionValue", "optionDisabled", "optionGroupLabel", "optionGroupChildren", "group", "showClear", "emptyFilterMessage", "emptyMessage", "lazy", "virtualScroll", "virtualScrollItemSize", "virtualScrollOptions", "overlayOptions", "ariaFilterLabel", "ariaLabel", "ariaLabelledBy", "filterMatchMode", "tooltip", "tooltipPosition", "tooltipPositionStyle", "tooltipStyleClass", "focusOnHover", "selectOnFocus", "autoOptionFocus", "autofocusFilter", "filterValue", "options", "appendTo"], outputs: ["onChange", "onFilter", "onFocus", "onBlur", "onClick", "onShow", "onHide", "onClear", "onLazyLoad"] }, { kind: "ngmodule", type: DatePickerModule }, { kind: "component", type: i2$7.DatePicker, selector: "p-datePicker, p-datepicker, p-date-picker", inputs: ["iconDisplay", "styleClass", "inputStyle", "inputId", "inputStyleClass", "placeholder", "ariaLabelledBy", "ariaLabel", "iconAriaLabel", "dateFormat", "multipleSeparator", "rangeSeparator", "inline", "showOtherMonths", "selectOtherMonths", "showIcon", "icon", "readonlyInput", "shortYearCutoff", "hourFormat", "timeOnly", "stepHour", "stepMinute", "stepSecond", "showSeconds", "showOnFocus", "showWeek", "startWeekFromFirstDayOfYear", "showClear", "dataType", "selectionMode", "maxDateCount", "showButtonBar", "todayButtonStyleClass", "clearButtonStyleClass", "autofocus", "autoZIndex", "baseZIndex", "panelStyleClass", "panelStyle", "keepInvalid", "hideOnDateTimeSelect", "touchUI", "timeSeparator", "focusTrap", "showTransitionOptions", "hideTransitionOptions", "tabindex", "minDate", "maxDate", "disabledDates", "disabledDays", "showTime", "responsiveOptions", "numberOfMonths", "firstDayOfWeek", "view", "defaultDate", "appendTo"], outputs: ["onFocus", "onBlur", "onClose", "onSelect", "onClear", "onInput", "onTodayClick", "onClearClick", "onMonthChange", "onYearChange", "onClickOutside", "onShow"] }, { kind: "ngmodule", type: MessageModule }, { kind: "component", type:
9008
- // optional advanced
9009
- MetaTimeperiodComponent, selector: "phoenix-meta-timeperiod", inputs: ["control", "parentForm"] }, { kind: "component", type: MetaCurrencyComponent, selector: "phoenix-meta-currency" }, { kind: "component", type: MetaStartDueDateV2Component, selector: "phoenix-meta-start-due-date-v2", inputs: ["dataCy"] }, { kind: "component", type: MetaTextEditorComponent, selector: "phoenix-meta-text-editor", inputs: ["previewMode", "hideLabel"] }, { kind: "component", type: MetaCheckboxColorPickerV2Component, selector: "phoenix-meta-checkbox-color-picker-v2", inputs: ["options", "disable"] }, { kind: "component", type: MetaSwitchComponent, selector: "phoenix-meta-switch" }, { kind: "component", type: MetaSelectButtonComponent, selector: "phoenix-meta-select-button" }, { kind: "component", type: MetaAssignResponsibleV2Component, selector: "phoenix-meta-assign-responsible-v2", inputs: ["items", "dialogHeaderKey"] }, { kind: "component", type:
9443
+ // Advanced / custom Phoenix fields
9444
+ MetaTimeperiodComponent, selector: "phoenix-meta-timeperiod", inputs: ["control", "parentForm"] }, { kind: "component", type: MetaCurrencyComponent, selector: "phoenix-meta-currency" }, { kind: "component", type: MetaStartDueDateV2Component, selector: "phoenix-meta-start-due-date-v2", inputs: ["dataCy", "disable"] }, { kind: "component", type: MetaTextEditorComponent, selector: "phoenix-meta-text-editor", inputs: ["previewMode", "hideLabel"] }, { kind: "component", type: MetaCheckboxColorPickerV2Component, selector: "phoenix-meta-checkbox-color-picker-v2", inputs: ["options", "disable"] }, { kind: "component", type: MetaSwitchComponent, selector: "phoenix-meta-switch" }, { kind: "component", type: MetaSelectButtonComponent, selector: "phoenix-meta-select-button" }, { kind: "component", type: MetaAssignResponsibleV2Component, selector: "phoenix-meta-assign-responsible-v2", inputs: ["items", "dialogHeaderKey"] }, { kind: "component", type:
9010
9445
  // MetaAssignAssetComponent,
9011
- MetaPasswordFeildComponent, selector: "phoenix-meta-password-feild" }, { kind: "component", type: MetaColorPickerV2Component, selector: "phoenix-meta-color-picker-v2", inputs: ["disable"] }, { kind: "component", type: MetaUploadComponent, selector: "phoenix-meta-upload" }, { kind: "component", type: MetaUploadComponentDragDrop, selector: "phoenix-meta-upload-dragdrop" }, { kind: "component", type: ReadOnlyInputV2Component, selector: "phoenix-read-only-input-v2", inputs: ["field", "form"] }, { kind: "pipe", type: i3$2.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
9446
+ MetaPasswordFeildComponent, selector: "phoenix-meta-password-feild" }, { kind: "component", type: MetaColorPickerV2Component, selector: "phoenix-meta-color-picker-v2", inputs: ["disable"] }, { kind: "component", type: MetaUploadComponent, selector: "phoenix-meta-upload" }, { kind: "component", type: MetaUploadComponentDragDrop, selector: "phoenix-meta-upload-dragdrop" }, { kind: "component", type:
9447
+ // Read-only renderer used when page or field is in read-only mode
9448
+ ReadOnlyInputV2Component, selector: "phoenix-read-only-input-v2", inputs: ["field", "form"] }, { kind: "pipe", type: i3$2.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
9012
9449
  }
9013
9450
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: MetaFormFieldV2Component, decorators: [{
9014
9451
  type: Component,
@@ -9016,7 +9453,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
9016
9453
  CommonModule,
9017
9454
  ReactiveFormsModule,
9018
9455
  TranslateModule,
9019
- // PrimeNG 20
9456
+ // PrimeNG 20 base inputs
9020
9457
  InputTextModule,
9021
9458
  TextareaModule,
9022
9459
  InputNumberModule,
@@ -9025,7 +9462,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
9025
9462
  SelectModule,
9026
9463
  DatePickerModule,
9027
9464
  MessageModule,
9028
- // optional advanced
9465
+ // Advanced / custom Phoenix fields
9029
9466
  MetaTimeperiodComponent,
9030
9467
  MetaCurrencyComponent,
9031
9468
  MetaStartDueDateV2Component,
@@ -9039,8 +9476,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
9039
9476
  MetaColorPickerV2Component,
9040
9477
  MetaUploadComponent,
9041
9478
  MetaUploadComponentDragDrop,
9479
+ // Read-only renderer used when page or field is in read-only mode
9042
9480
  ReadOnlyInputV2Component,
9043
- ], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div [formGroup]=\"form\">\n @if (!field.hidden) {\n <div class=\"meta-field flex flex-column gap-2\" [style.order]=\"field.order ?? null\"\n [attr.data-cy]=\"'meta-field-' + key\">\n\n @if (userFriendlyMessage()) {\n <label class=\"meta-label\" [attr.for]=\"key\">\n {{ userFriendlyMessage()! | translate }}\n @if (field.mandatory) { <span class=\"meta-required\">*</span> }\n </label>\n }\n\n <!-- READ ONLY (page-level ili field-level) -->\n @if (isReadOnly()) {\n <phoenix-read-only-input-v2 [field]=\"field\" [form]=\"form\"></phoenix-read-only-input-v2>\n } @else {\n\n @switch (type) {\n\n @case (MetaFieldType.TEXT) {\n <input pInputText [id]=\"key\" [formControlName]=\"key\"\n [attr.placeholder]=\"placeholderKey() ? (placeholderKey()! | translate) : null\" [readonly]=\"isReadOnly()\">\n }\n\n @case (MetaFieldType.PASSWORD) {\n <!-- Ako nema\u0161 legacy komponentu, mo\u017Ee\u0161 samo input type=\"password\" -->\n <phoenix-meta-password-feild [disable]=\"isDisabled()\" [formControlName]=\"key\" [control]=\"field\" [parentForm]=\"form\">\n </phoenix-meta-password-feild>\n }\n\n @case (MetaFieldType.TEXT_AREA) {\n <textarea pTextarea class=\"meta-textarea\" [id]=\"key\" [formControlName]=\"key\" fluid [autoResize]=\"false\" rows=\"5\"\n [readonly]=\"isReadOnly()\" [attr.placeholder]=\"placeholderKey() ? (placeholderKey()! | translate) : null\">\n </textarea>\n }\n\n @case (MetaFieldType.NUMBER) {\n <p-inputNumber [inputId]=\"key\" [formControlName]=\"key\">\n </p-inputNumber>\n }\n\n @case (MetaFieldType.DATE) {\n <p-datepicker [inputId]=\"key\" [formControlName]=\"key\" [showIcon]=\"true\">\n </p-datepicker>\n }\n\n @case (MetaFieldType.SS_OPTION) {\n <p-select [inputId]=\"key\" [options]=\"field.configuration.options ?? []\" optionLabel=\"label\" optionValue=\"value\"\n [formControlName]=\"key\" [showClear]=\"false\">\n </p-select>\n }\n\n @case (MetaFieldType.SS_OPTION_OBJECT_BASED) {\n <p-select [inputId]=\"key\" [options]=\"field.configuration.options ?? []\" optionLabel=\"label\" [formControlName]=\"key\"\n [showClear]=\"true\">\n </p-select>\n }\n\n @case (MetaFieldType.MS_OPTION) {\n <p-multiselect [inputId]=\"key\" [options]=\"field.configuration.options ?? []\" optionLabel=\"label\" optionValue=\"value\"\n [formControlName]=\"key\" [showClear]=\"false\" display=\"chip\">\n </p-multiselect>\n }\n\n @case (MetaFieldType.CHECKBOX) {\n <div class=\"flex align-items-center gap-2\">\n <p-checkbox [inputId]=\"key\" [binary]=\"true\" [formControlName]=\"key\">\n </p-checkbox>\n\n @if (placeholderKey()) {\n <label [attr.for]=\"key\" class=\"meta-inline-label\">\n {{ placeholderKey()! | translate }}\n </label>\n }\n </div>\n }\n\n <!-- advanced: preko postoje\u0107ih komponenti -->\n @case (MetaFieldType.TIMEPERIOD) {\n <phoenix-meta-timeperiod [formControlName]=\"key\" [control]=\"field\" [parentForm]=\"form\"></phoenix-meta-timeperiod>\n }\n\n @case (MetaFieldType.CURRENCY) {\n <phoenix-meta-currency [disable]=\"isDisabled()\" [formControlName]=\"key\" [control]=\"field\"\n [parentForm]=\"form\"></phoenix-meta-currency>\n }\n\n @case (MetaFieldType.START_DUE_DATE) {\n <phoenix-meta-start-due-date-v2\n [formControlName]=\"key\"\n [attr.data-cy]=\"'start-due-' + key\">\n </phoenix-meta-start-due-date-v2>\n }\n\n @case (MetaFieldType.TEXT_EDITOR) {\n <phoenix-meta-text-editor [disable]=\"isDisabled()\" [formControlName]=\"key\" [control]=\"field\"\n [parentForm]=\"form\"></phoenix-meta-text-editor>\n }\n\n @case (MetaFieldType.CHECKBOX_COLOR) {\n <phoenix-meta-checkbox-color-picker-v2\n [formControlName]=\"key\"\n [options]=\"(field.configuration.extra?.['colorGrid'] ?? [])\"\n [disable]=\"isDisabled()\">\n </phoenix-meta-checkbox-color-picker-v2>\n }\n\n @case (MetaFieldType.SWITCH) {\n <phoenix-meta-switch [disable]=\"isDisabled()\" [formControlName]=\"key\" [control]=\"field\"\n [parentForm]=\"form\"></phoenix-meta-switch>\n }\n\n @case (MetaFieldType.SELECT_BUTTON) {\n <phoenix-meta-select-button [disable]=\"isDisabled()\" [formControlName]=\"key\" [control]=\"field\"\n [parentForm]=\"form\"></phoenix-meta-select-button>\n }\n\n @case (MetaFieldType.ASSIGN) {\n <phoenix-meta-assign-responsible-v2\n [formControlName]=\"key\"\n [items]=\"(field.configuration.extra?.['items'] ?? [])\"\n [dialogHeaderKey]=\"(field.configuration.extra?.['dialogHeaderKey'] ?? 'LABELS.ASSIGN_RESPONSIBLE')\"\n ></phoenix-meta-assign-responsible-v2>\n }\n\n <!-- @case (MetaFieldType.ASSIGN_ASSET) {\n <phoenix-meta-assign-asset [disable]=\"isDisabled()\" [formControlName]=\"key\" [control]=\"field\"\n [parentForm]=\"form\"></phoenix-meta-assign-asset>\n } -->\n\n @case (MetaFieldType.COLOR) {\n <phoenix-meta-color-picker-v2\n [formControlName]=\"key\"\n [disable]=\"isDisabled()\">\n </phoenix-meta-color-picker-v2>\n }\n\n @case (MetaFieldType.UPLOAD) {\n <phoenix-meta-upload [disable]=\"isDisabled()\" [formControlName]=\"key\" [control]=\"field\"\n [parentForm]=\"form\"></phoenix-meta-upload>\n }\n\n @case (MetaFieldType.UPLOAD_DRAG_DROP) {\n <phoenix-meta-upload-dragdrop [disable]=\"isDisabled()\" [formControlName]=\"key\" [control]=\"field\"\n [parentForm]=\"form\"></phoenix-meta-upload-dragdrop>\n }\n\n @case (MetaFieldType.LINKS_DATA) {\n <!-- <input pInputText [id]=\"key\" [formControlName]=\"key\" [readonly]=\"true\"> -->\n }\n\n @case (MetaFieldType.SLOT) { }\n\n @default {\n <input pInputText [id]=\"key\" [formControlName]=\"key\">\n }\n }\n }\n\n\n @if (!readOnly && showError()) {\n <small class=\"p-error block mt-1\">\n <i class=\"pi pi-info-circle mr-1\"></i>{{ errorText() }}\n </small>\n }\n </div>\n }\n</div>", styles: [".meta-field{width:100%}.meta-required{margin-left:4px;color:#ef4444}.meta-textarea{resize:none!important}.meta-inline-label{opacity:.9}.p-inputtext.ng-invalid.ng-dirty{border-color:var(--p-inputtext-border-color)!important}.p-select.ng-invalid.ng-dirty{border-color:var(--p-select-border-color)!important}\n"] }]
9481
+ ], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div [formGroup]=\"form\">\n @if (!field.hidden) {\n <div class=\"meta-field flex flex-column gap-2\" [style.order]=\"field.order ?? null\"\n [attr.data-cy]=\"'meta-field-' + key\">\n\n @if (userFriendlyMessage()) {\n <label class=\"meta-label\" [attr.for]=\"key\">\n {{ userFriendlyMessage()! | translate }}\n @if (field.mandatory) { <span class=\"meta-required\">*</span> }\n </label>\n }\n\n <!-- READ ONLY (page-level ili field-level) -->\n @if (isReadOnly()) {\n <phoenix-read-only-input-v2 [field]=\"field\" [form]=\"form\"></phoenix-read-only-input-v2>\n } @else {\n\n @switch (type) {\n\n @case (MetaFieldType.TEXT) {\n <input pInputText [id]=\"key\" [formControlName]=\"key\"\n [attr.placeholder]=\"placeholderKey() ? (placeholderKey()! | translate) : null\" [readonly]=\"isReadOnly()\">\n }\n\n @case (MetaFieldType.PASSWORD) {\n <phoenix-meta-password-feild [disable]=\"isDisabled()\" [formControlName]=\"key\" [control]=\"field\" [parentForm]=\"form\">\n </phoenix-meta-password-feild>\n }\n\n @case (MetaFieldType.TEXT_AREA) {\n <textarea pTextarea class=\"meta-textarea\" [id]=\"key\" [formControlName]=\"key\" fluid [autoResize]=\"false\" rows=\"5\"\n [readonly]=\"isReadOnly()\" [attr.placeholder]=\"placeholderKey() ? (placeholderKey()! | translate) : null\">\n </textarea>\n }\n\n @case (MetaFieldType.NUMBER) {\n <p-inputNumber [inputId]=\"key\" [formControlName]=\"key\">\n </p-inputNumber>\n }\n\n @case (MetaFieldType.DATE) {\n <p-datepicker [inputId]=\"key\" [formControlName]=\"key\" [showIcon]=\"true\">\n </p-datepicker>\n }\n\n @case (MetaFieldType.SS_OPTION) {\n <p-select [inputId]=\"key\" [options]=\"field.configuration.options ?? []\" optionLabel=\"label\" optionValue=\"value\"\n [formControlName]=\"key\" [showClear]=\"false\">\n </p-select>\n }\n\n @case (MetaFieldType.SS_OPTION_OBJECT_BASED) {\n <p-select [inputId]=\"key\" [options]=\"field.configuration.options ?? []\" optionLabel=\"label\" [formControlName]=\"key\"\n [showClear]=\"true\">\n </p-select>\n }\n\n @case (MetaFieldType.MS_OPTION) {\n <p-multiselect [inputId]=\"key\" [options]=\"field.configuration.options ?? []\" optionLabel=\"label\" optionValue=\"value\"\n [formControlName]=\"key\" [showClear]=\"false\" display=\"chip\">\n </p-multiselect>\n }\n\n @case (MetaFieldType.CHECKBOX) {\n <div class=\"flex align-items-center gap-2\">\n <p-checkbox [inputId]=\"key\" [binary]=\"true\" [formControlName]=\"key\">\n </p-checkbox>\n\n @if (placeholderKey()) {\n <label [attr.for]=\"key\" class=\"meta-inline-label\">\n {{ placeholderKey()! | translate }}\n </label>\n }\n </div>\n }\n\n <!-- advanced: preko postoje\u0107ih komponenti -->\n @case (MetaFieldType.TIMEPERIOD) {\n <phoenix-meta-timeperiod [formControlName]=\"key\" [control]=\"field\" [parentForm]=\"form\"></phoenix-meta-timeperiod>\n }\n\n @case (MetaFieldType.CURRENCY) {\n <phoenix-meta-currency [disable]=\"isDisabled()\" [formControlName]=\"key\" [control]=\"field\"\n [parentForm]=\"form\"></phoenix-meta-currency>\n }\n\n @case (MetaFieldType.START_DUE_DATE) {\n <phoenix-meta-start-due-date-v2\n [formControlName]=\"key\"\n [attr.data-cy]=\"'start-due-' + key\">\n </phoenix-meta-start-due-date-v2>\n }\n\n @case (MetaFieldType.TEXT_EDITOR) {\n <phoenix-meta-text-editor [disable]=\"isDisabled()\" [formControlName]=\"key\" [control]=\"field\"\n [parentForm]=\"form\"></phoenix-meta-text-editor>\n }\n\n @case (MetaFieldType.CHECKBOX_COLOR) {\n <phoenix-meta-checkbox-color-picker-v2\n [formControlName]=\"key\"\n [options]=\"(field.configuration.extra?.['colorGrid'] ?? [])\"\n [disable]=\"isDisabled()\">\n </phoenix-meta-checkbox-color-picker-v2>\n }\n\n @case (MetaFieldType.SWITCH) {\n <phoenix-meta-switch [disable]=\"isDisabled()\" [formControlName]=\"key\" [control]=\"field\"\n [parentForm]=\"form\"></phoenix-meta-switch>\n }\n\n @case (MetaFieldType.SELECT_BUTTON) {\n <phoenix-meta-select-button [disable]=\"isDisabled()\" [formControlName]=\"key\" [control]=\"field\"\n [parentForm]=\"form\"></phoenix-meta-select-button>\n }\n\n @case (MetaFieldType.ASSIGN) {\n <phoenix-meta-assign-responsible-v2\n [formControlName]=\"key\"\n [items]=\"(field.configuration.extra?.['items'] ?? [])\"\n [dialogHeaderKey]=\"(field.configuration.extra?.['dialogHeaderKey'] ?? 'LABELS.ASSIGN_RESPONSIBLE')\"\n ></phoenix-meta-assign-responsible-v2>\n }\n\n <!-- @case (MetaFieldType.ASSIGN_ASSET) {\n <phoenix-meta-assign-asset [disable]=\"isDisabled()\" [formControlName]=\"key\" [control]=\"field\"\n [parentForm]=\"form\"></phoenix-meta-assign-asset>\n } -->\n\n @case (MetaFieldType.COLOR) {\n <phoenix-meta-color-picker-v2\n [formControlName]=\"key\"\n [disable]=\"isDisabled()\">\n </phoenix-meta-color-picker-v2>\n }\n\n @case (MetaFieldType.UPLOAD) {\n <phoenix-meta-upload [disable]=\"isDisabled()\" [formControlName]=\"key\" [control]=\"field\"\n [parentForm]=\"form\"></phoenix-meta-upload>\n }\n\n @case (MetaFieldType.UPLOAD_DRAG_DROP) {\n <phoenix-meta-upload-dragdrop [disable]=\"isDisabled()\" [formControlName]=\"key\" [control]=\"field\"\n [parentForm]=\"form\"></phoenix-meta-upload-dragdrop>\n }\n\n @case (MetaFieldType.LINKS_DATA) {\n <!-- <input pInputText [id]=\"key\" [formControlName]=\"key\" [readonly]=\"true\"> -->\n }\n\n @case (MetaFieldType.SLOT) { }\n\n @default {\n <input pInputText [id]=\"key\" [formControlName]=\"key\">\n }\n }\n }\n\n\n @if (!readOnly && showError()) {\n <small class=\"p-error block mt-1\">\n <i class=\"pi pi-info-circle mr-1\"></i>{{ errorText() }}\n </small>\n }\n </div>\n }\n</div>", styles: [".meta-field{width:100%}.meta-required{margin-left:4px;color:#ef4444}.meta-textarea{resize:none!important}.meta-inline-label{opacity:.9}.p-inputtext.ng-invalid.ng-dirty{border-color:var(--p-inputtext-border-color)!important}.p-select.ng-invalid.ng-dirty{border-color:var(--p-select-border-color)!important}\n"] }]
9044
9482
  }], propDecorators: { field: [{
9045
9483
  type: Input,
9046
9484
  args: [{ required: true }]
@@ -9079,22 +9517,41 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
9079
9517
  */
9080
9518
  const META_FORM_ASYNC_EXECUTOR = new InjectionToken('META_FORM_ASYNC_EXECUTOR');
9081
9519
 
9520
+ // Symbols used to attach submit-only validators and cleanup subscriptions
9521
+ // directly onto the FormGroup instance without polluting its public API.
9082
9522
  const SUBMIT_VALIDATORS = Symbol('META_FORM_V2_SUBMIT_VALIDATORS');
9083
9523
  const SUBMIT_CLEAR_SUBS = Symbol('META_FORM_V2_SUBMIT_CLEAR_SUBS');
9084
9524
  class MetaSubmitValidatorService {
9085
9525
  executor;
9526
+ /** Used to automatically clean up subscriptions when the service is destroyed */
9086
9527
  dr = inject(DestroyRef);
9087
- constructor(executor) {
9528
+ constructor(
9529
+ /**
9530
+ * Optional async executor abstraction used to perform server-side validation
9531
+ * (e.g. uniqueness checks).
9532
+ *
9533
+ * If not provided, submit-only validators will be skipped gracefully.
9534
+ */
9535
+ executor) {
9088
9536
  this.executor = executor;
9089
9537
  }
9538
+ /**
9539
+ * Registers submit-only validators on the given form.
9540
+ *
9541
+ * - Attaches validator metadata to the form instance (via Symbols)
9542
+ * - Cleans up any previous subscriptions
9543
+ * - Subscribes to valueChanges in order to auto-clear submit-only errors
9544
+ * when the user modifies the field after a failed submit
9545
+ */
9090
9546
  register(form, validators) {
9091
9547
  const list = validators ?? [];
9092
9548
  form[SUBMIT_VALIDATORS] = list;
9093
- // cleanup starih sub-ova
9549
+ // Cleanup old auto-clear subscriptions
9094
9550
  const old = form[SUBMIT_CLEAR_SUBS] ?? [];
9095
9551
  old.forEach((s) => s.unsubscribe());
9096
9552
  form[SUBMIT_CLEAR_SUBS] = [];
9097
- // auto-clear submit-only errora čim user menja input
9553
+ // Subscribe to valueChanges in order to remove submit-only errors
9554
+ // as soon as the user changes the input.
9098
9555
  const subs = [];
9099
9556
  for (const v of list) {
9100
9557
  const ctrl = form.get(v.controlKey);
@@ -9104,29 +9561,45 @@ class MetaSubmitValidatorService {
9104
9561
  subs.push(ctrl.valueChanges
9105
9562
  .pipe(takeUntilDestroyed(this.dr))
9106
9563
  .subscribe(() => {
9107
- // ✅ samo ukloni submit-only error, bez updateValueAndValidity
9564
+ // ✅ Only remove submit-only error.
9565
+ // ❌ Do NOT trigger updateValueAndValidity here to avoid noisy re-validation.
9108
9566
  this.removeError(ctrl, errorKey);
9109
9567
  }));
9110
9568
  }
9111
9569
  form[SUBMIT_CLEAR_SUBS] = subs;
9112
9570
  }
9571
+ /**
9572
+ * Executes submit-only validators.
9573
+ *
9574
+ * Flow:
9575
+ * 1) Mark all controls as touched/dirty so sync validation messages become visible
9576
+ * 2) If form is sync-invalid, skip async validation
9577
+ * 3) Clear previous submit-only errors
9578
+ * 4) Execute async validators sequentially
9579
+ * 5) Apply mapped errors back to controls
9580
+ *
9581
+ * Returns:
9582
+ * - true → form is valid (sync + submit-only async)
9583
+ * - false → at least one submit-only validator failed
9584
+ */
9113
9585
  async run(form) {
9114
9586
  const validators = form[SUBMIT_VALIDATORS] ?? [];
9115
- // pokaži sync greške
9587
+ // Make sure sync validation errors are visible on submit
9116
9588
  this.markAllTouchedOnSubmit(form);
9117
- // ne mora emitEvent true ovde
9589
+ // Do not emit valueChanges/statusChanges events here
9118
9590
  form.updateValueAndValidity({ emitEvent: false });
9119
9591
  if (!validators.length)
9120
9592
  return form.valid;
9121
- // ako nema executor-a, ne blokiraj
9593
+ // If no async executor is provided, do not block submit
9122
9594
  if (!this.executor)
9123
9595
  return form.valid;
9124
- // očisti stare submit-only greške
9596
+ // Clear previous submit-only errors before re-running validation
9125
9597
  this.clearSubmitErrors(form, validators);
9126
- // ako je sync-invalid, nema smisla okidati async
9598
+ // If sync validation fails, skip async calls
9127
9599
  form.updateValueAndValidity({ emitEvent: false });
9128
9600
  if (form.invalid)
9129
9601
  return false;
9602
+ // Execute submit-only validators sequentially
9130
9603
  for (const v of validators) {
9131
9604
  const req = v.buildRequest(form);
9132
9605
  if (!req)
@@ -9141,7 +9614,9 @@ class MetaSubmitValidatorService {
9141
9614
  const errorKey = v.errorKey ?? 'unique';
9142
9615
  const msg = mapped?.message ??
9143
9616
  'VALIDATION_MESSAGE.VALUE_IS_ALREADY_IN_USE';
9617
+ // Attach submit-only error to control
9144
9618
  ctrl.setErrors({ ...(ctrl.errors ?? {}), [errorKey]: msg });
9619
+ // Force visibility of the error in UI
9145
9620
  ctrl.markAsTouched();
9146
9621
  ctrl.markAsDirty();
9147
9622
  return false;
@@ -9149,6 +9624,10 @@ class MetaSubmitValidatorService {
9149
9624
  }
9150
9625
  return form.valid;
9151
9626
  }
9627
+ /**
9628
+ * Removes a specific submit-only error from the control without
9629
+ * touching other validation errors.
9630
+ */
9152
9631
  removeError(ctrl, errorKey) {
9153
9632
  if (!ctrl.errors?.[errorKey])
9154
9633
  return;
@@ -9156,6 +9635,10 @@ class MetaSubmitValidatorService {
9156
9635
  delete next[errorKey];
9157
9636
  ctrl.setErrors(Object.keys(next).length ? next : null);
9158
9637
  }
9638
+ /**
9639
+ * Clears submit-only errors for all controls involved in submit validators.
9640
+ * This does NOT trigger updateValueAndValidity to avoid unnecessary re-validation.
9641
+ */
9159
9642
  clearSubmitErrors(form, validators) {
9160
9643
  const keys = new Set();
9161
9644
  validators.forEach((v) => keys.add(v.controlKey));
@@ -9166,10 +9649,14 @@ class MetaSubmitValidatorService {
9166
9649
  return;
9167
9650
  const nextErrors = { ...(ctrl.errors ?? {}) };
9168
9651
  errorKeys.forEach((ek) => delete nextErrors[ek]);
9169
- // ✅ samo setErrors, bez updateValueAndValidity
9652
+ // ✅ Only update errors object, no re-validation side effects
9170
9653
  ctrl.setErrors(Object.keys(nextErrors).length ? nextErrors : null);
9171
9654
  });
9172
9655
  }
9656
+ /**
9657
+ * Marks all form controls as touched and dirty.
9658
+ * This is used on submit to make validation errors visible to the user.
9659
+ */
9173
9660
  markAllTouchedOnSubmit(form) {
9174
9661
  Object.values(form.controls).forEach((c) => {
9175
9662
  c.markAsTouched();
@@ -9189,71 +9676,129 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
9189
9676
  args: [META_FORM_ASYNC_EXECUTOR]
9190
9677
  }] }] });
9191
9678
 
9679
+ /**
9680
+ * Validator for START_DUE_DATE composite field.
9681
+ *
9682
+ * Responsibilities:
9683
+ * - Optionally enforce that BOTH startDate and endDate are provided (requireBoth)
9684
+ * - Validate that provided dates are valid Date values
9685
+ * - Ensure startDate is not after endDate
9686
+ *
9687
+ * This is a synchronous validator and should be attached to the single FormControl
9688
+ * that represents the composite START_DUE_DATE value.
9689
+ */
9192
9690
  function startDueDateV2Validator(opts) {
9193
9691
  const requireBoth = !!opts?.requireBoth;
9194
9692
  return (control) => {
9195
9693
  const v = control.value;
9196
9694
  const sdRaw = v?.startDate ?? null;
9197
9695
  const edRaw = v?.endDate ?? null;
9198
- // oba prazna
9696
+ // Case 1: both dates are empty
9697
+ // - If both are required -> invalid
9698
+ // - Otherwise -> valid (optional date range)
9199
9699
  if (!sdRaw && !edRaw) {
9200
9700
  return requireBoth ? { bothDates: true } : null;
9201
9701
  }
9202
- // fali jedan (ako su oba obavezna)
9702
+ // Case 2: only one date is provided but both are required
9203
9703
  if (requireBoth && (!sdRaw || !edRaw)) {
9204
9704
  return { bothDates: true };
9205
9705
  }
9206
- // ako nije requireBoth, a jedan fali -> ok
9706
+ // Case 3: both are optional and one is missing -> valid
9207
9707
  if (!sdRaw || !edRaw)
9208
9708
  return null;
9709
+ // Parse raw values into Date instances
9209
9710
  const sd = new Date(sdRaw);
9210
9711
  const ed = new Date(edRaw);
9712
+ // Invalid date values (e.g. unparsable strings)
9211
9713
  if (isNaN(sd.getTime()) || isNaN(ed.getTime()))
9212
9714
  return { dueDate: true };
9715
+ // Normalize both dates to midnight for date-only comparison (ignore time)
9213
9716
  const s = new Date(sd);
9214
9717
  s.setHours(0, 0, 0, 0);
9215
9718
  const e = new Date(ed);
9216
9719
  e.setHours(0, 0, 0, 0);
9720
+ // Start date must not be after end date
9217
9721
  return s.getTime() > e.getTime() ? { dueDate: true } : null;
9218
9722
  };
9219
9723
  }
9220
9724
 
9725
+ /**
9726
+ * Builds synchronous Angular validators for a single meta-field.
9727
+ *
9728
+ * This function only returns *sync* validators:
9729
+ * - Required, min/max, length
9730
+ * - Email
9731
+ * - Regex / phone
9732
+ * - Timeperiod
9733
+ * - Start/Due date cross-field-ish validator (but still sync)
9734
+ * - Whitespace rules
9735
+ * - Dangerous characters
9736
+ *
9737
+ * Submit-only async validators (e.g. uniqueness) are handled elsewhere (MetaSubmitValidatorService).
9738
+ */
9221
9739
  function buildSyncValidators(field, ctx) {
9222
9740
  const out = [];
9741
+ // `field.validators` is a metadata-driven bag of validation rules
9223
9742
  const v = field.validators ?? {};
9743
+ // Field type drives some special-case validators
9224
9744
  const type = field.configuration.type;
9745
+ // ---- numeric constraints ----
9225
9746
  if (v.min != null)
9226
9747
  out.push(Validators.min(v.min));
9227
9748
  if (v.max != null)
9228
9749
  out.push(Validators.max(v.max));
9750
+ // ---- length constraints ----
9229
9751
  if (v.minLength != null)
9230
9752
  out.push(Validators.minLength(v.minLength));
9231
9753
  if (v.maxLength != null)
9232
9754
  out.push(Validators.maxLength(v.maxLength));
9755
+ // ---- email ----
9233
9756
  if (v.email)
9234
9757
  out.push(Validators.email);
9235
- if (v.regex?.regexType)
9758
+ // ---- regex / pattern ----
9759
+ // Supports either:
9760
+ // - plain pattern string
9761
+ // - "/.../flags" format which we try to compile into RegExp
9762
+ if (v.regex?.regexType) {
9236
9763
  out.push(Validators.pattern(resolvePattern(v.regex.regexType) ?? v.regex.regexType));
9764
+ }
9765
+ // ---- phone ----
9766
+ // if (v.phone) out.push(Validators.pattern(/^\+?[1-9]\d{7,14}$/));
9237
9767
  if (v.phone)
9238
- out.push(Validators.pattern(/^\+?[1-9]\d{7,14}$/));
9768
+ out.push(phoneHumanValidator({ minDigits: 8, maxDigits: 15 }));
9769
+ // ---- time period ----
9770
+ // Localized validator; tpMin can enforce minimal value/length depending on your implementation.
9239
9771
  if (v.timeperiod)
9240
9772
  out.push(timePeriod(ctx?.lang ?? 'en', v.tpMin));
9773
+ // ---- start/due date ----
9774
+ // Apply the validator if field is of START_DUE_DATE type or if metadata explicitly enables dueDate.
9775
+ // requireBoth is driven by field.mandatory
9241
9776
  if (type === 'START_DUE_DATE' || v.dueDate) {
9242
9777
  out.push(startDueDateV2Validator({ requireBoth: !!field.mandatory }));
9243
9778
  }
9779
+ // ---- required ----
9244
9780
  if (field.mandatory)
9245
9781
  out.push(Validators.required);
9782
+ // ---- whitespace guard for required text fields ----
9783
+ // Prevent " " from passing required validation.
9246
9784
  if (field.mandatory && (type === 'TEXT' || type === 'TEXT_AREA')) {
9247
9785
  out.push(WhiteSpaceValidator.noWhiteSpaceValidator);
9248
9786
  }
9787
+ // ---- dangerous characters ----
9788
+ // Applied to text and textarea only.
9249
9789
  if (type === 'TEXT' || type === 'TEXT_AREA') {
9250
9790
  out.push(noDangerousCharsValidator());
9251
9791
  }
9252
9792
  return out;
9253
9793
  }
9794
+ /**
9795
+ * Tries to interpret a regex string in "/pattern/flags" format.
9796
+ * If parsing fails, returns null and the caller falls back to using the raw string.
9797
+ */
9254
9798
  function resolvePattern(regexType) {
9255
9799
  if (!regexType)
9256
9800
  return null;
9801
+ // Support format like: "/^[0-9]+$/g"
9257
9802
  const m = regexType.match(/^\/(.+)\/([gimsuy]*)$/);
9258
9803
  if (m) {
9259
9804
  try {
@@ -9263,26 +9808,94 @@ function resolvePattern(regexType) {
9263
9808
  return null;
9264
9809
  }
9265
9810
  }
9811
+ // Otherwise treat it as a plain string pattern
9266
9812
  return regexType;
9267
9813
  }
9814
+ /**
9815
+ * Phone validator that accepts common human formats:
9816
+ * - spaces, hyphens, parentheses
9817
+ * - local numbers starting with 0 (e.g. 060...)
9818
+ * - international numbers with +
9819
+ *
9820
+ * Strategy:
9821
+ * 1) Strip formatting chars
9822
+ * 2) Validate the normalized digits
9823
+ */
9824
+ function phoneHumanValidator(opts) {
9825
+ const min = opts?.minDigits ?? 8;
9826
+ const max = opts?.maxDigits ?? 15;
9827
+ return (control) => {
9828
+ const raw = (control.value ?? '').toString().trim();
9829
+ if (!raw)
9830
+ return null; // let required handle empties
9831
+ // Keep leading "+" if present, remove common separators
9832
+ const normalized = raw
9833
+ .replace(/[\s\-().]/g, '') // remove spaces, dashes, parentheses, dots
9834
+ .replace(/(?!^\+)\+/g, ''); // remove any "+" not at the start
9835
+ // Allow optional leading "+"
9836
+ const hasPlus = normalized.startsWith('+');
9837
+ const digits = hasPlus ? normalized.slice(1) : normalized;
9838
+ // Digits only after normalization
9839
+ if (!/^\d+$/.test(digits))
9840
+ return { phone: true };
9841
+ // Length constraints (digits only)
9842
+ if (digits.length < min || digits.length > max)
9843
+ return { phone: true };
9844
+ // If it has "+", require country code style (first digit not 0)
9845
+ if (hasPlus && digits.startsWith('0'))
9846
+ return { phone: true };
9847
+ // If local, allow leading 0 (e.g. 060...)
9848
+ return null;
9849
+ };
9850
+ }
9268
9851
 
9852
+ /**
9853
+ * Ensures that the FormGroup structure matches the provided metadata (flat field list).
9854
+ *
9855
+ * Responsibilities:
9856
+ * - Create missing FormControls based on MetaFieldConfig
9857
+ * - Apply (and re-apply) synchronous validators derived from metadata
9858
+ * - Sync disabled/enabled state with metadata
9859
+ * - Remove obsolete controls that are no longer present in metadata
9860
+ *
9861
+ * This function is intentionally idempotent:
9862
+ * Calling it multiple times with the same metadata should not break form state.
9863
+ */
9269
9864
  function ensureControlsV2(fb, form, flat, initialValues, ctx) {
9865
+ // Tracks which control keys are allowed by current metadata
9270
9866
  const allowed = new Set();
9271
9867
  for (const field of flat) {
9272
9868
  const key = field?.configuration?.key;
9273
9869
  if (!key)
9274
9870
  continue;
9275
9871
  allowed.add(key);
9872
+ /**
9873
+ * Create control if it does not exist yet.
9874
+ * Initial value is taken from initialValues map if provided,
9875
+ * otherwise defaults to null.
9876
+ */
9276
9877
  if (!form.contains(key)) {
9277
9878
  form.addControl(key, new FormControl(initialValues?.[key] ?? null));
9278
9879
  }
9279
9880
  const ctrl = form.get(key);
9280
9881
  if (!ctrl)
9281
9882
  continue;
9282
- // 1) validators
9883
+ /**
9884
+ * (1) Sync validators:
9885
+ * Rebuild and apply synchronous validators based on current field metadata.
9886
+ * This ensures that changes in metadata (e.g. required, min/max, patterns)
9887
+ * are reflected on the form control.
9888
+ */
9283
9889
  ctrl.setValidators(buildSyncValidators(field, ctx));
9890
+ // Recalculate validity silently (do not emit events to avoid UI side-effects)
9284
9891
  ctrl.updateValueAndValidity({ emitEvent: false });
9285
- // 2) disabled state (symetric)
9892
+ /**
9893
+ * (2) Sync disabled/enabled state:
9894
+ * The control's enabled state must reflect the field metadata.
9895
+ * This is symmetric:
9896
+ * - If metadata says "disable" → disable control
9897
+ * - If metadata says "enable" → enable control
9898
+ */
9286
9899
  const shouldBeDisabled = !!field.disable;
9287
9900
  if (shouldBeDisabled && ctrl.enabled) {
9288
9901
  ctrl.disable({ emitEvent: false });
@@ -9291,6 +9904,13 @@ function ensureControlsV2(fb, form, flat, initialValues, ctx) {
9291
9904
  ctrl.enable({ emitEvent: false });
9292
9905
  }
9293
9906
  }
9907
+ /**
9908
+ * Remove any controls that are present in the FormGroup
9909
+ * but no longer exist in the metadata.
9910
+ *
9911
+ * This keeps the form structure in sync when metadata changes dynamically
9912
+ * (e.g. conditional fields, stage-based forms, etc.).
9913
+ */
9294
9914
  Object.keys(form.controls).forEach((k) => {
9295
9915
  if (!allowed.has(k))
9296
9916
  form.removeControl(k);
@@ -9311,48 +9931,101 @@ function flattenControls(input) {
9311
9931
  }
9312
9932
 
9313
9933
  class MetaFormV2Component {
9934
+ /** Form instance created/owned by the parent (dialog/page) */
9314
9935
  form;
9936
+ /**
9937
+ * V2 metadata/config:
9938
+ * - controls (grouped or flat)
9939
+ * - initialValues
9940
+ * - submitValidators (run on submit only)
9941
+ * - setupDependencies (optional runtime bindings)
9942
+ */
9315
9943
  config;
9316
- /** Page-level readOnly (parent toggles) */
9944
+ /**
9945
+ * Page-level readOnly state controlled by parent.
9946
+ * Used both for rendering and for "enter edit mode" behavior.
9947
+ */
9317
9948
  readOnly = false;
9318
- /** Optional layout props */
9949
+ /** Optional layout customization for inner content wrapper */
9319
9950
  contentStyle = null;
9951
+ /** Optional class name(s) applied to inner content wrapper */
9320
9952
  contentClass = null;
9953
+ /** Builds/ensures controls exist (ensures validators & default values are wired) */
9321
9954
  fb = inject(FormBuilder);
9955
+ /** Registers and executes submit-only validators (async validation on submit) */
9322
9956
  submitValidator = inject(MetaSubmitValidatorService);
9957
+ /** Used for validator localization (lang-dependent validators / messages) */
9323
9958
  translate = inject(TranslateService);
9959
+ /**
9960
+ * A lightweight signature of the current metadata structure.
9961
+ * Used to detect when the form schema changes (and avoid unnecessary resets).
9962
+ */
9324
9963
  lastSignature = '';
9964
+ /**
9965
+ * Cleanup function returned by setupDependencies (if any).
9966
+ * Called only when metadata structure changes or on destroy.
9967
+ */
9325
9968
  depCleanup;
9326
- /** PrimeNG accordion active value(s) */
9969
+ /**
9970
+ * PrimeNG Accordion "value" for opened panels.
9971
+ * For multiple panels, PrimeNG expects an array of ids.
9972
+ */
9327
9973
  expandedGroupIds = [];
9328
9974
  ngOnChanges(changes) {
9329
9975
  if (!this.form || !this.config)
9330
9976
  return;
9977
+ /**
9978
+ * Flatten metadata controls to a single list for:
9979
+ * - building/enforcing FormControls
9980
+ * - dependency binding
9981
+ * - signature calculation
9982
+ */
9331
9983
  const flat = flattenControls(this.config.controls);
9984
+ /**
9985
+ * Signature is used to detect real schema changes.
9986
+ * We intentionally ignore labels/props and track key+type only.
9987
+ */
9332
9988
  const signature = flat.map((f) => `${f.configuration.key}:${f.configuration.type}`).join('|');
9989
+ /** "config changed" includes initialValues, validators, dependencies, etc. */
9333
9990
  const configChanged = !!changes['config'];
9991
+ /** "form changed" means parent passed a different FormGroup instance */
9334
9992
  const formChanged = !!changes['form'];
9993
+ /** schema changed if the signature differs from the last one */
9335
9994
  const metaChanged = signature !== this.lastSignature;
9995
+ /**
9996
+ * Special case: entering edit mode (readOnly true -> false).
9997
+ * Used to selectively show validation for already-filled values.
9998
+ */
9336
9999
  const enteringEdit = !!changes['readOnly'] &&
9337
10000
  changes['readOnly'].previousValue === true &&
9338
10001
  changes['readOnly'].currentValue === false;
9339
- // If nothing relevant changed, exit
10002
+ // If nothing relevant changed, exit early to avoid unnecessary work.
9340
10003
  if (!configChanged && !formChanged && !metaChanged && !enteringEdit)
9341
10004
  return;
9342
- // Cleanup dependencies ONLY when metadata/structure changes
10005
+ /**
10006
+ * Dependencies are tied to the metadata structure.
10007
+ * If schema changed, cleanup old subscriptions/bindings first.
10008
+ */
9343
10009
  if (configChanged || metaChanged) {
9344
10010
  this.depCleanup?.();
9345
10011
  this.depCleanup = undefined;
9346
10012
  }
9347
- // Ensure controls exist + sync validators
10013
+ /**
10014
+ * Ensure controls exist on the passed FormGroup and sync validators.
10015
+ * This is where missing controls are added and validator wiring is applied.
10016
+ */
9348
10017
  const initial = this.config.initialValues ?? {};
9349
10018
  ensureControlsV2(this.fb, this.form, flat, initial, { lang: this.translate.currentLang });
9350
- // Patch initial values (no emit)
10019
+ /**
10020
+ * Patch initial values without emitting changes:
10021
+ * - prevents loops
10022
+ * - keeps create/edit initialization silent
10023
+ */
9351
10024
  this.form.patchValue(initial, { emitEvent: false });
9352
10025
  this.form.updateValueAndValidity({ emitEvent: false });
9353
10026
  /**
9354
- * Initialize accordion expanded panels ONLY when structure changed.
9355
- * This must NOT run on readOnly toggle, otherwise you "reset" user-collapsed state.
10027
+ * Initialize accordion open panels ONLY when schema changes.
10028
+ * IMPORTANT: do NOT do this on readOnly toggle, otherwise user-collapsed state resets.
9356
10029
  */
9357
10030
  if (metaChanged) {
9358
10031
  if (this.isGrouped) {
@@ -9366,9 +10039,16 @@ class MetaFormV2Component {
9366
10039
  this.expandedGroupIds = [];
9367
10040
  }
9368
10041
  }
9369
- // Register submit-only validators (safe even if empty)
10042
+ /**
10043
+ * Register submit-only validators.
10044
+ * Safe to call even if empty; service will attach necessary structures internally.
10045
+ */
9370
10046
  this.submitValidator.register(this.form, this.config.submitValidators);
9371
- // Bind dependencies ONLY when metadata/structure changes
10047
+ /**
10048
+ * Bind dependencies ONLY when schema changes.
10049
+ * setupDependencies can subscribe to valueChanges, set options, reset fields, etc.
10050
+ * If it returns a function, we store it for cleanup.
10051
+ */
9372
10052
  if ((configChanged || metaChanged) && this.config.setupDependencies) {
9373
10053
  const maybeCleanup = this.config.setupDependencies({
9374
10054
  form: this.form,
@@ -9381,10 +10061,10 @@ class MetaFormV2Component {
9381
10061
  this.depCleanup = maybeCleanup;
9382
10062
  }
9383
10063
  /**
9384
- * Entering edit:
9385
- * Show errors ONLY for controls that already have a value.
9386
- * Do NOT trigger mandatory "required" messages for empty fields.
9387
- * Expand groups that contain such "visible invalid" controls.
10064
+ * Entering edit mode:
10065
+ * - mark controls as touched ONLY if they already have a meaningful value
10066
+ * - expand groups that contain "visible invalid" controls (invalid + touched/dirty)
10067
+ * This prevents CREATE dialogs from showing "required" errors immediately.
9388
10068
  */
9389
10069
  if (enteringEdit) {
9390
10070
  queueMicrotask(() => {
@@ -9392,12 +10072,19 @@ class MetaFormV2Component {
9392
10072
  this.expandVisibleInvalidGroupsUnion();
9393
10073
  });
9394
10074
  }
10075
+ // Store signature for the next change detection pass
9395
10076
  this.lastSignature = signature;
9396
10077
  }
9397
- // Template handler (typed exactly for our template usage)
10078
+ /**
10079
+ * PrimeNG Accordion emits value as:
10080
+ * - single id (string/number)
10081
+ * - array of ids
10082
+ * We normalize everything into string[] for stable internal state.
10083
+ */
9398
10084
  onAccordionValueChange(v) {
9399
10085
  this.expandedGroupIds = this.normalizeAccordionValue(v);
9400
10086
  }
10087
+ /** Normalizes Accordion value into a stable string[] representation */
9401
10088
  normalizeAccordionValue(v) {
9402
10089
  if (Array.isArray(v))
9403
10090
  return v.map((x) => `${x}`);
@@ -9406,31 +10093,44 @@ class MetaFormV2Component {
9406
10093
  return [`${v}`];
9407
10094
  }
9408
10095
  // ---------------- template helpers ----------------
10096
+ /** True when metadata contains at least one control definition */
9409
10097
  get hasControls() {
9410
10098
  return Array.isArray(this.config?.controls) && this.config.controls.length > 0;
9411
10099
  }
10100
+ /**
10101
+ * Heuristic: grouped config has "ctrl" on first element.
10102
+ * (Keeps template simple and avoids extra schema fields.)
10103
+ */
9412
10104
  get isGrouped() {
9413
10105
  const c = this.config?.controls ?? [];
9414
10106
  return !!c[0]?.ctrl;
9415
10107
  }
10108
+ /** Returns grouped schema structure (accordion groups) */
9416
10109
  get groupedControls() {
9417
10110
  return this.config?.controls ?? [];
9418
10111
  }
10112
+ /** Returns flat schema structure (grid mode) */
9419
10113
  get flatControls() {
9420
10114
  return this.config?.controls ?? [];
9421
10115
  }
10116
+ /** TrackBy for group rendering */
9422
10117
  groupTrack(g, idx) {
9423
10118
  return g?.id ?? idx;
9424
10119
  }
9425
- /** PrimeNG panel value must match accordion value type */
10120
+ /**
10121
+ * PrimeNG accordion panel `value` must match accordion `value` type.
10122
+ * We always convert group id to string for consistent behavior.
10123
+ */
9426
10124
  panelValue(g) {
9427
10125
  const id = g?.id;
9428
10126
  return id === null || id === undefined ? '' : `${id}`;
9429
10127
  }
9430
10128
  // ---------------- core behavior ----------------
9431
10129
  /**
9432
- * Mark & validate ONLY controls that already have a meaningful value.
9433
- * This prevents "required" errors from showing immediately on CREATE dialogs.
10130
+ * Marks & validates ONLY controls that already have meaningful values.
10131
+ * This is used when switching from readOnly -> edit mode to avoid:
10132
+ * - triggering "required" errors for empty fields
10133
+ * - expanding groups based on empty mandatory fields on CREATE dialogs
9434
10134
  */
9435
10135
  touchAndValidateOnlyFilledControls() {
9436
10136
  const controls = this.form?.controls ?? {};
@@ -9438,21 +10138,21 @@ class MetaFormV2Component {
9438
10138
  if (!ctrl)
9439
10139
  continue;
9440
10140
  const value = ctrl.value;
9441
- // Touch only if user already has a value (edit case) or prefilled values exist
10141
+ // Touch only if this field is already populated (edit case) or prefilled.
9442
10142
  if (this.hasMeaningfulValue(value)) {
9443
10143
  ctrl.markAsTouched();
9444
10144
  ctrl.updateValueAndValidity({ emitEvent: true });
9445
10145
  }
9446
10146
  }
9447
- // Keep form validity in sync (optional)
10147
+ // Optional: keep form status consistent after selective updates
9448
10148
  this.form.updateValueAndValidity({ emitEvent: true });
9449
10149
  }
9450
10150
  /**
9451
- * What counts as a "meaningful value"?
10151
+ * Defines what counts as a "meaningful" value:
9452
10152
  * - non-empty strings
9453
10153
  * - numbers / booleans
9454
10154
  * - non-empty arrays
9455
- * - objects with at least one key (or common "selected item" shapes)
10155
+ * - objects with common identifiers (key/uuid/id) or any own keys
9456
10156
  */
9457
10157
  hasMeaningfulValue(v) {
9458
10158
  if (v === null || v === undefined)
@@ -9466,34 +10166,43 @@ class MetaFormV2Component {
9466
10166
  if (Array.isArray(v))
9467
10167
  return v.length > 0;
9468
10168
  if (typeof v === 'object') {
9469
- // common selection shapes: { key }, { uuid }, { id }, etc.
10169
+ // Common selection shapes: { key }, { uuid }, { id }, etc.
9470
10170
  if ('key' in v && v.key != null && `${v.key}`.trim() !== '')
9471
10171
  return true;
9472
10172
  if ('uuid' in v && v.uuid != null && `${v.uuid}`.trim() !== '')
9473
10173
  return true;
9474
10174
  if ('id' in v && v.id != null && `${v.id}`.trim() !== '')
9475
10175
  return true;
9476
- // fallback: any own keys
10176
+ // Fallback: any own keys
9477
10177
  return Object.keys(v).length > 0;
9478
10178
  }
9479
10179
  return false;
9480
10180
  }
9481
10181
  /**
9482
- * Visible invalid = invalid AND (touched OR dirty).
9483
- * This aligns with the goal: don't expand groups due to empty required fields on create.
10182
+ * "Visible invalid" means:
10183
+ * - invalid
10184
+ * - AND user has interacted with it (touched or dirty)
10185
+ * This matches typical UI behavior: show errors only after interaction.
9484
10186
  */
9485
10187
  isVisibleInvalid(ctrl) {
9486
10188
  if (!ctrl)
9487
10189
  return false;
9488
10190
  return ctrl.invalid && (ctrl.touched || ctrl.dirty);
9489
10191
  }
10192
+ /**
10193
+ * Checks if a group contains at least one visible invalid control.
10194
+ * Used to auto-expand groups when entering edit mode.
10195
+ */
9490
10196
  groupHasVisibleInvalid(g) {
9491
10197
  const keys = (g?.ctrl ?? [])
9492
10198
  .map((f) => f?.configuration?.key)
9493
10199
  .filter(Boolean);
9494
10200
  return keys.some((k) => this.isVisibleInvalid(this.form.get(k)));
9495
10201
  }
9496
- /** Expand invalid groups but KEEP already opened ones */
10202
+ /**
10203
+ * Expands all groups that contain visible invalid controls,
10204
+ * while preserving any groups already expanded by the user.
10205
+ */
9497
10206
  expandVisibleInvalidGroupsUnion() {
9498
10207
  if (!this.isGrouped)
9499
10208
  return;
@@ -9506,6 +10215,7 @@ class MetaFormV2Component {
9506
10215
  return;
9507
10216
  this.expandedGroupIds = Array.from(new Set([...this.expandedGroupIds, ...invalidIds]));
9508
10217
  }
10218
+ /** Cleanup dependency subscriptions when component is destroyed */
9509
10219
  ngOnDestroy() {
9510
10220
  this.depCleanup?.();
9511
10221
  }