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

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();
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();
8684
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,197 @@ 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
9175
+ }] } });
9176
+
9177
+ class MetaSwitchV2Component {
9178
+ /**
9179
+ * Optional external disable flag (field-level disable coming from meta config).
9180
+ * This is combined with the disabled state coming from Angular Forms (CVA).
9181
+ */
9182
+ disable = false;
9183
+ /**
9184
+ * Optional flag to hide the switch from UI (meta hidden).
9185
+ * Keep in mind: hidden does not automatically disable the control.
9186
+ */
9187
+ hidden = false;
9188
+ /**
9189
+ * Optional data-cy attribute for e2e tests (e.g. "switch-singleEntity").
9190
+ */
9191
+ dataCy;
9192
+ cdr = inject(ChangeDetectorRef);
9193
+ /**
9194
+ * Current boolean value.
9195
+ * We keep it strictly boolean to avoid "true"/"false" string issues.
9196
+ */
9197
+ value = false;
9198
+ /**
9199
+ * CVA callback invoked when the value changes.
9200
+ */
9201
+ onChange = () => { };
9202
+ /**
9203
+ * CVA callback invoked when the control is marked as touched.
9204
+ * Standard: call on blur, not on every change.
9205
+ */
9206
+ onTouched = () => { };
9207
+ /**
9208
+ * Disabled state coming from Angular Forms (ControlValueAccessor).
9209
+ */
9210
+ isDisabled = false;
9211
+ /**
9212
+ * Final disabled state combining:
9213
+ * - form-level disabled state (CVA)
9214
+ * - field-level disable flag (input)
9215
+ */
9216
+ get disabled() {
9217
+ return this.disable || this.isDisabled;
9218
+ }
9219
+ /**
9220
+ * Writes a new value from the parent form control into the component.
9221
+ * Keep it idempotent and UI-safe.
9222
+ */
9223
+ writeValue(v) {
9224
+ this.value = this.normalizeBool(v);
9225
+ this.cdr.markForCheck();
9226
+ }
9227
+ /**
9228
+ * Registers callback that is triggered when the value changes.
9229
+ */
9230
+ registerOnChange(fn) {
9231
+ this.onChange = fn;
9232
+ }
9233
+ /**
9234
+ * Registers callback that is triggered when the control is touched.
9235
+ */
9236
+ registerOnTouched(fn) {
9237
+ this.onTouched = fn;
9238
+ }
9239
+ /**
9240
+ * Receives disabled state from Angular Forms and updates local state.
9241
+ */
9242
+ setDisabledState(isDisabled) {
9243
+ this.isDisabled = isDisabled;
9244
+ this.cdr.markForCheck();
9245
+ }
9246
+ /**
9247
+ * Handler for switch change.
9248
+ * Propagates value to the parent form control.
9249
+ *
9250
+ * Note: we intentionally do NOT call onTouched here (our standard is: touched on blur).
9251
+ */
9252
+ onSwitchChange(next) {
9253
+ if (this.disabled)
9254
+ return;
9255
+ const normalized = this.normalizeBool(next);
9256
+ // prevent redundant emits
9257
+ if (normalized === this.value) {
9258
+ this.cdr.markForCheck();
9259
+ return;
9260
+ }
9261
+ this.value = normalized;
9262
+ this.onChange(normalized);
9263
+ this.cdr.markForCheck();
9264
+ }
9265
+ /**
9266
+ * Marks control as touched when user leaves the component.
9267
+ */
9268
+ handleBlur() {
9269
+ if (this.disabled)
9270
+ return;
9271
+ this.onTouched();
9272
+ }
9273
+ /**
9274
+ * Normalizes incoming values to strict boolean.
9275
+ * Supports: boolean, "true"/"false", 1/0, "1"/"0".
9276
+ */
9277
+ normalizeBool(v) {
9278
+ if (v === true || v === false)
9279
+ return v;
9280
+ if (v === 1 || v === '1')
9281
+ return true;
9282
+ if (v === 0 || v === '0')
9283
+ return false;
9284
+ if (typeof v === 'string') {
9285
+ const s = v.trim().toLowerCase();
9286
+ if (s === 'true')
9287
+ return true;
9288
+ if (s === 'false')
9289
+ return false;
9290
+ }
9291
+ return !!v;
9292
+ }
9293
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: MetaSwitchV2Component, deps: [], target: i0.ɵɵFactoryTarget.Component });
9294
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.16", type: MetaSwitchV2Component, isStandalone: true, selector: "phoenix-meta-switch-v2", inputs: { disable: "disable", hidden: "hidden", dataCy: "dataCy" }, providers: [
9295
+ {
9296
+ provide: NG_VALUE_ACCESSOR,
9297
+ useExisting: forwardRef(() => MetaSwitchV2Component),
9298
+ multi: true,
9299
+ },
9300
+ ], ngImport: i0, template: `
9301
+ <p-toggleSwitch
9302
+ class="phoenix-switch-v2"
9303
+ [(ngModel)]="value"
9304
+ (ngModelChange)="onSwitchChange($event)"
9305
+ (onBlur)="handleBlur()"
9306
+ [disabled]="disabled"
9307
+ [hidden]="hidden"
9308
+ [attr.data-cy]="dataCy ?? null"
9309
+ ></p-toggleSwitch>
9310
+ `, isInline: true, styles: [":host ::ng-deep .p-toggleswitch{margin-top:12px}\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: ToggleSwitchModule }, { kind: "component", type: i3$7.ToggleSwitch, selector: "p-toggleswitch, p-toggleSwitch, p-toggle-switch", inputs: ["styleClass", "tabindex", "inputId", "readonly", "trueValue", "falseValue", "ariaLabel", "size", "ariaLabelledBy", "autofocus"], outputs: ["onChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
9311
+ }
9312
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: MetaSwitchV2Component, decorators: [{
9313
+ type: Component,
9314
+ args: [{ selector: 'phoenix-meta-switch-v2', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [CommonModule, FormsModule, ToggleSwitchModule], template: `
9315
+ <p-toggleSwitch
9316
+ class="phoenix-switch-v2"
9317
+ [(ngModel)]="value"
9318
+ (ngModelChange)="onSwitchChange($event)"
9319
+ (onBlur)="handleBlur()"
9320
+ [disabled]="disabled"
9321
+ [hidden]="hidden"
9322
+ [attr.data-cy]="dataCy ?? null"
9323
+ ></p-toggleSwitch>
9324
+ `, providers: [
9325
+ {
9326
+ provide: NG_VALUE_ACCESSOR,
9327
+ useExisting: forwardRef(() => MetaSwitchV2Component),
9328
+ multi: true,
9329
+ },
9330
+ ], styles: [":host ::ng-deep .p-toggleswitch{margin-top:12px}\n"] }]
9331
+ }], propDecorators: { disable: [{
9332
+ type: Input
9333
+ }], hidden: [{
9334
+ type: Input
9335
+ }], dataCy: [{
9336
+ type: Input
8797
9337
  }] } });
8798
9338
 
8799
9339
  class MetaFormFieldV2Component {
9340
+ /** Metadata definition of the field (type, key, options, styles, flags, etc.) */
8800
9341
  field;
9342
+ /** Parent FormGroup that contains the FormControl for this field */
8801
9343
  form;
8802
- /** External "page read-only" state; when true, we render read-only fields (like V1). */
9344
+ /**
9345
+ * Page-level read-only flag.
9346
+ * When true, the component renders ReadOnlyInputV2Component instead of editable controls.
9347
+ */
8803
9348
  readOnly = false;
8804
- // ako hoćeš global readOnly/disable (npr. parent toggles)
9349
+ /**
9350
+ * Global disable flag (e.g. parent dialog toggles entire form disabled).
9351
+ * This is merged with field-level disable configuration.
9352
+ */
8805
9353
  disableForm = false;
9354
+ /** Used to manually trigger change detection for OnPush strategy */
8806
9355
  cdr = inject(ChangeDetectorRef);
9356
+ /** Used to automatically unsubscribe from value/status streams on destroy */
8807
9357
  dr = inject(DestroyRef);
9358
+ /** Translation service for validation and display labels */
8808
9359
  translate = inject(TranslateService);
9360
+ /**
9361
+ * Exposed enum-like mapping of MetaFieldType for template usage.
9362
+ * Keeps templates readable and avoids magic strings.
9363
+ */
8809
9364
  MetaFieldType = Object.freeze({
8810
9365
  TEXT: 'TEXT',
8811
9366
  NUMBER: 'NUMBER',
@@ -8831,61 +9386,95 @@ class MetaFormFieldV2Component {
8831
9386
  LINKS_DATA: 'LINKS_DATA',
8832
9387
  SLOT: 'SLOT',
8833
9388
  });
9389
+ /** Control key resolved from MetaFieldConfig */
8834
9390
  get key() {
8835
9391
  return this.field?.configuration?.key ?? '';
8836
9392
  }
9393
+ /** Field type resolved from MetaFieldConfig */
8837
9394
  get type() {
8838
9395
  return this.field?.configuration?.type ?? 'TEXT';
8839
9396
  }
9397
+ /** Column width class for grid layout (falls back to default if not provided) */
8840
9398
  get colClass() {
8841
- return this.field?.hidden ? 'p-0' : (this.field?.style.colWidth ?? 'col-12 md:col-6');
9399
+ return this.field?.hidden
9400
+ ? 'p-0'
9401
+ : (this.field?.style.colWidth ?? 'col-12 md:col-6');
8842
9402
  }
8843
9403
  ngOnInit() {
8844
9404
  const ctrl = this.ctrl();
8845
9405
  if (!ctrl)
8846
9406
  return;
9407
+ /**
9408
+ * Subscribe to both valueChanges and statusChanges so the component:
9409
+ * - re-renders when user changes the value
9410
+ * - re-renders when validation state changes (touched/dirty/errors)
9411
+ */
8847
9412
  merge(ctrl.valueChanges, ctrl.statusChanges)
8848
9413
  .pipe(takeUntilDestroyed(this.dr))
8849
9414
  .subscribe(() => this.cdr.markForCheck());
8850
9415
  }
9416
+ /** Human-friendly label defined in metadata (already localized key) */
8851
9417
  userFriendlyMessage() {
8852
9418
  return this.field?.userFriendlyMessage ?? null;
8853
9419
  }
9420
+ /** Optional placeholder i18n key defined in metadata */
8854
9421
  placeholderKey() {
8855
9422
  return this.field?.configuration?.placeholderKey ?? null;
8856
9423
  }
9424
+ /**
9425
+ * Resolves final read-only state for this field:
9426
+ * - page-level readOnly OR field-level readOnly
9427
+ */
8857
9428
  isReadOnly() {
8858
9429
  return !!this.readOnly || !!this.field?.readOnly;
8859
9430
  }
9431
+ /**
9432
+ * Resolves final disabled state for this field:
9433
+ * - page-level disable OR field-level disable
9434
+ */
8860
9435
  isDisabled() {
8861
9436
  return !!this.disableForm || !!this.field?.disable;
8862
9437
  }
9438
+ /** Shortcut to underlying FormControl */
8863
9439
  ctrl() {
8864
9440
  return this.form.get(this.key);
8865
9441
  }
8866
- /** Used by read-only renderer. Keep it simple, same as V1 behavior. */
9442
+ /**
9443
+ * Minimal value formatter for legacy read-only rendering.
9444
+ * Kept intentionally simple to match V1 behavior.
9445
+ */
8867
9446
  displayValue() {
8868
9447
  const v = this.ctrl()?.value;
8869
9448
  if (v === null || v === undefined)
8870
9449
  return '';
8871
9450
  if (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean')
8872
9451
  return v;
8873
- // common DTO shapes (e.g. assign, option objects, upload)
9452
+ // Common DTO shapes (assign, option objects, uploads, etc.)
8874
9453
  if (typeof v === 'object') {
8875
9454
  return v.label ?? v.name ?? v.fileName ?? JSON.stringify(v);
8876
9455
  }
8877
9456
  return String(v);
8878
9457
  }
9458
+ /**
9459
+ * Determines whether validation error should be displayed.
9460
+ * Errors are shown only after user interaction (touched or dirty).
9461
+ */
8879
9462
  showError() {
8880
9463
  const c = this.ctrl();
8881
9464
  return !!c && (c.touched || c.dirty) && !!c.errors;
8882
9465
  }
8883
- /** mapira error key (sync + submit-only async) */
9466
+ /**
9467
+ * Maps control error object to a normalized error key.
9468
+ * Supports:
9469
+ * - Angular built-in validators
9470
+ * - Phoenix custom validators
9471
+ * - Submit-only async validators
9472
+ */
8884
9473
  errorKey() {
8885
9474
  const c = this.ctrl();
8886
9475
  if (!c?.errors)
8887
9476
  return null;
8888
- // sync (Angular built-ins)
9477
+ // Angular built-in validators
8889
9478
  if (c.errors['required'])
8890
9479
  return 'required';
8891
9480
  if (c.errors['minlength'])
@@ -8900,27 +9489,31 @@ class MetaFormFieldV2Component {
8900
9489
  return 'min';
8901
9490
  if (c.errors['max'])
8902
9491
  return 'max';
8903
- // custom validators (Phoenix)
9492
+ // Phoenix custom validators
8904
9493
  if (c.errors['dangerousChars'])
8905
9494
  return 'dangerousChars';
8906
9495
  if (c.errors['timeperiod'])
8907
9496
  return 'timeperiod';
8908
9497
  if (c.errors['invalidDate'])
8909
- return 'invalidDate'; // ako negde postoji
9498
+ return 'invalidDate';
8910
9499
  if (c.errors['dueDate'])
8911
9500
  return 'dueDate';
8912
9501
  if (c.errors['bothDates'])
8913
9502
  return 'bothDates';
8914
- // submit-only async
9503
+ // Submit-only async validators
8915
9504
  if (c.errors['unique'])
8916
9505
  return 'unique';
8917
9506
  if (c.errors['uniqueEntry'])
8918
9507
  return 'uniqueEntry';
8919
9508
  if (c.errors['custom'])
8920
9509
  return 'custom';
9510
+ // Fallback: return first error key
8921
9511
  return Object.keys(c.errors)[0] ?? null;
8922
9512
  }
8923
- /** minimal: posle prevežeš na i18n ključeve */
9513
+ /**
9514
+ * Resolves translated error message based on errorKey().
9515
+ * This is the single place responsible for validation message UX.
9516
+ */
8924
9517
  errorText() {
8925
9518
  const c = this.ctrl();
8926
9519
  const k = this.errorKey();
@@ -8942,11 +9535,12 @@ class MetaFormFieldV2Component {
8942
9535
  case 'dangerousChars':
8943
9536
  return this.translate.instant('VALIDATION_MESSAGE.NO_SPECIAL_CHARS_ALLOWED');
8944
9537
  case 'custom':
8945
- // legacy ponašanje: custom može biti string ključ poruke
9538
+ // Legacy behavior: custom error can already be a translation key
8946
9539
  return this.translate.instant(c.errors?.['custom']);
8947
9540
  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');
9541
+ // Legacy behavior: uniqueEntry may already be a translated string
9542
+ return (c.errors?.['uniqueEntry'] ??
9543
+ this.translate.instant('VALIDATION_MESSAGE.VALUE_IS_ALREADY_IN_USE'));
8950
9544
  case 'unique':
8951
9545
  return this.translate.instant('VALIDATION_MESSAGE.VALUE_IS_ALREADY_IN_USE');
8952
9546
  case 'timeperiod':
@@ -8961,7 +9555,7 @@ class MetaFormFieldV2Component {
8961
9555
  upperValue: c.errors?.['max']?.max,
8962
9556
  });
8963
9557
  case 'pattern': {
8964
- // isti spec-case kao u starom InlineFieldError
9558
+ // Special-case URL pattern handling (legacy InlineFieldError behavior)
8965
9559
  const re = '^(https?://)?([\\da-z.-]+)\\.([a-z.]{2,6})[/\\w .-]*/?$';
8966
9560
  const requiredPattern = c.errors?.['pattern']?.requiredPattern;
8967
9561
  if (requiredPattern === re) {
@@ -8973,42 +9567,47 @@ class MetaFormFieldV2Component {
8973
9567
  return this.translate.instant('VALIDATION_MESSAGE.INVALID_VALUE');
8974
9568
  }
8975
9569
  }
9570
+ /**
9571
+ * Lightweight text formatter for simple read-only display use cases.
9572
+ * This is used mainly for inline displays and summary UIs.
9573
+ */
8976
9574
  valueText() {
8977
9575
  const c = this.ctrl();
8978
9576
  const v = c?.value;
8979
9577
  if (v === null || v === undefined || v === '')
8980
9578
  return '--';
8981
- // SS_OPTION value object {label,value} ili raw
9579
+ // Single-select option: resolve label from options
8982
9580
  if (this.type === 'SS_OPTION') {
8983
9581
  const opts = this.field?.configuration?.options ?? [];
8984
9582
  if (typeof v !== 'object') {
8985
9583
  const hit = opts.find((o) => o?.value === v);
8986
9584
  const label = hit?.label ?? v;
8987
- // ako je label i18n key
8988
9585
  return this.translate.instant(label);
8989
9586
  }
8990
- // ako je objekat
9587
+ // Object value fallback
8991
9588
  const label = v.label ?? v.value;
8992
9589
  return this.translate.instant(label);
8993
9590
  }
8994
- // DATE
9591
+ // Date formatting
8995
9592
  if (this.type === 'DATE' && v instanceof Date) {
8996
9593
  return v.toLocaleDateString();
8997
9594
  }
8998
- // TEXT_EDITOR / TEXT_AREA: strip html (minimalno)
9595
+ // Text editor / textarea: strip basic HTML tags for compact display
8999
9596
  if (this.type === 'TEXT_EDITOR' || this.type === 'TEXT_AREA') {
9000
9597
  return String(v).replace(/<[^>]*>/g, '').trim() || '--';
9001
9598
  }
9002
9599
  return String(v);
9003
9600
  }
9004
9601
  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
9602
+ 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-v2 [disable]=\"isDisabled()\" [formControlName]=\"key\" [hidden]=\"field.hidden ?? false\"\n [dataCy]=\"'switch-' + key\"></phoenix-meta-switch-v2>\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:
9603
+ // PrimeNG 20 base inputs
9007
9604
  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:
9605
+ // Advanced / custom Phoenix fields
9606
+ 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: MetaSwitchV2Component, selector: "phoenix-meta-switch-v2", inputs: ["disable", "hidden", "dataCy"] }, { 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
9607
  // 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 });
9608
+ 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:
9609
+ // Read-only renderer used when page or field is in read-only mode
9610
+ ReadOnlyInputV2Component, selector: "phoenix-read-only-input-v2", inputs: ["field", "form"] }, { kind: "pipe", type: i3$2.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
9012
9611
  }
9013
9612
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: MetaFormFieldV2Component, decorators: [{
9014
9613
  type: Component,
@@ -9016,7 +9615,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
9016
9615
  CommonModule,
9017
9616
  ReactiveFormsModule,
9018
9617
  TranslateModule,
9019
- // PrimeNG 20
9618
+ // PrimeNG 20 base inputs
9020
9619
  InputTextModule,
9021
9620
  TextareaModule,
9022
9621
  InputNumberModule,
@@ -9025,13 +9624,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
9025
9624
  SelectModule,
9026
9625
  DatePickerModule,
9027
9626
  MessageModule,
9028
- // optional advanced
9627
+ // Advanced / custom Phoenix fields
9029
9628
  MetaTimeperiodComponent,
9030
9629
  MetaCurrencyComponent,
9031
9630
  MetaStartDueDateV2Component,
9032
9631
  MetaTextEditorComponent,
9033
9632
  MetaCheckboxColorPickerV2Component,
9034
- MetaSwitchComponent,
9633
+ MetaSwitchV2Component,
9035
9634
  MetaSelectButtonComponent,
9036
9635
  MetaAssignResponsibleV2Component,
9037
9636
  // MetaAssignAssetComponent,
@@ -9039,8 +9638,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
9039
9638
  MetaColorPickerV2Component,
9040
9639
  MetaUploadComponent,
9041
9640
  MetaUploadComponentDragDrop,
9641
+ // Read-only renderer used when page or field is in read-only mode
9042
9642
  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"] }]
9643
+ ], 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-v2 [disable]=\"isDisabled()\" [formControlName]=\"key\" [hidden]=\"field.hidden ?? false\"\n [dataCy]=\"'switch-' + key\"></phoenix-meta-switch-v2>\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
9644
  }], propDecorators: { field: [{
9045
9645
  type: Input,
9046
9646
  args: [{ required: true }]
@@ -9079,22 +9679,41 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
9079
9679
  */
9080
9680
  const META_FORM_ASYNC_EXECUTOR = new InjectionToken('META_FORM_ASYNC_EXECUTOR');
9081
9681
 
9682
+ // Symbols used to attach submit-only validators and cleanup subscriptions
9683
+ // directly onto the FormGroup instance without polluting its public API.
9082
9684
  const SUBMIT_VALIDATORS = Symbol('META_FORM_V2_SUBMIT_VALIDATORS');
9083
9685
  const SUBMIT_CLEAR_SUBS = Symbol('META_FORM_V2_SUBMIT_CLEAR_SUBS');
9084
9686
  class MetaSubmitValidatorService {
9085
9687
  executor;
9688
+ /** Used to automatically clean up subscriptions when the service is destroyed */
9086
9689
  dr = inject(DestroyRef);
9087
- constructor(executor) {
9690
+ constructor(
9691
+ /**
9692
+ * Optional async executor abstraction used to perform server-side validation
9693
+ * (e.g. uniqueness checks).
9694
+ *
9695
+ * If not provided, submit-only validators will be skipped gracefully.
9696
+ */
9697
+ executor) {
9088
9698
  this.executor = executor;
9089
9699
  }
9700
+ /**
9701
+ * Registers submit-only validators on the given form.
9702
+ *
9703
+ * - Attaches validator metadata to the form instance (via Symbols)
9704
+ * - Cleans up any previous subscriptions
9705
+ * - Subscribes to valueChanges in order to auto-clear submit-only errors
9706
+ * when the user modifies the field after a failed submit
9707
+ */
9090
9708
  register(form, validators) {
9091
9709
  const list = validators ?? [];
9092
9710
  form[SUBMIT_VALIDATORS] = list;
9093
- // cleanup starih sub-ova
9711
+ // Cleanup old auto-clear subscriptions
9094
9712
  const old = form[SUBMIT_CLEAR_SUBS] ?? [];
9095
9713
  old.forEach((s) => s.unsubscribe());
9096
9714
  form[SUBMIT_CLEAR_SUBS] = [];
9097
- // auto-clear submit-only errora čim user menja input
9715
+ // Subscribe to valueChanges in order to remove submit-only errors
9716
+ // as soon as the user changes the input.
9098
9717
  const subs = [];
9099
9718
  for (const v of list) {
9100
9719
  const ctrl = form.get(v.controlKey);
@@ -9104,29 +9723,45 @@ class MetaSubmitValidatorService {
9104
9723
  subs.push(ctrl.valueChanges
9105
9724
  .pipe(takeUntilDestroyed(this.dr))
9106
9725
  .subscribe(() => {
9107
- // ✅ samo ukloni submit-only error, bez updateValueAndValidity
9726
+ // ✅ Only remove submit-only error.
9727
+ // ❌ Do NOT trigger updateValueAndValidity here to avoid noisy re-validation.
9108
9728
  this.removeError(ctrl, errorKey);
9109
9729
  }));
9110
9730
  }
9111
9731
  form[SUBMIT_CLEAR_SUBS] = subs;
9112
9732
  }
9733
+ /**
9734
+ * Executes submit-only validators.
9735
+ *
9736
+ * Flow:
9737
+ * 1) Mark all controls as touched/dirty so sync validation messages become visible
9738
+ * 2) If form is sync-invalid, skip async validation
9739
+ * 3) Clear previous submit-only errors
9740
+ * 4) Execute async validators sequentially
9741
+ * 5) Apply mapped errors back to controls
9742
+ *
9743
+ * Returns:
9744
+ * - true → form is valid (sync + submit-only async)
9745
+ * - false → at least one submit-only validator failed
9746
+ */
9113
9747
  async run(form) {
9114
9748
  const validators = form[SUBMIT_VALIDATORS] ?? [];
9115
- // pokaži sync greške
9749
+ // Make sure sync validation errors are visible on submit
9116
9750
  this.markAllTouchedOnSubmit(form);
9117
- // ne mora emitEvent true ovde
9751
+ // Do not emit valueChanges/statusChanges events here
9118
9752
  form.updateValueAndValidity({ emitEvent: false });
9119
9753
  if (!validators.length)
9120
9754
  return form.valid;
9121
- // ako nema executor-a, ne blokiraj
9755
+ // If no async executor is provided, do not block submit
9122
9756
  if (!this.executor)
9123
9757
  return form.valid;
9124
- // očisti stare submit-only greške
9758
+ // Clear previous submit-only errors before re-running validation
9125
9759
  this.clearSubmitErrors(form, validators);
9126
- // ako je sync-invalid, nema smisla okidati async
9760
+ // If sync validation fails, skip async calls
9127
9761
  form.updateValueAndValidity({ emitEvent: false });
9128
9762
  if (form.invalid)
9129
9763
  return false;
9764
+ // Execute submit-only validators sequentially
9130
9765
  for (const v of validators) {
9131
9766
  const req = v.buildRequest(form);
9132
9767
  if (!req)
@@ -9141,7 +9776,9 @@ class MetaSubmitValidatorService {
9141
9776
  const errorKey = v.errorKey ?? 'unique';
9142
9777
  const msg = mapped?.message ??
9143
9778
  'VALIDATION_MESSAGE.VALUE_IS_ALREADY_IN_USE';
9779
+ // Attach submit-only error to control
9144
9780
  ctrl.setErrors({ ...(ctrl.errors ?? {}), [errorKey]: msg });
9781
+ // Force visibility of the error in UI
9145
9782
  ctrl.markAsTouched();
9146
9783
  ctrl.markAsDirty();
9147
9784
  return false;
@@ -9149,6 +9786,10 @@ class MetaSubmitValidatorService {
9149
9786
  }
9150
9787
  return form.valid;
9151
9788
  }
9789
+ /**
9790
+ * Removes a specific submit-only error from the control without
9791
+ * touching other validation errors.
9792
+ */
9152
9793
  removeError(ctrl, errorKey) {
9153
9794
  if (!ctrl.errors?.[errorKey])
9154
9795
  return;
@@ -9156,6 +9797,10 @@ class MetaSubmitValidatorService {
9156
9797
  delete next[errorKey];
9157
9798
  ctrl.setErrors(Object.keys(next).length ? next : null);
9158
9799
  }
9800
+ /**
9801
+ * Clears submit-only errors for all controls involved in submit validators.
9802
+ * This does NOT trigger updateValueAndValidity to avoid unnecessary re-validation.
9803
+ */
9159
9804
  clearSubmitErrors(form, validators) {
9160
9805
  const keys = new Set();
9161
9806
  validators.forEach((v) => keys.add(v.controlKey));
@@ -9166,10 +9811,14 @@ class MetaSubmitValidatorService {
9166
9811
  return;
9167
9812
  const nextErrors = { ...(ctrl.errors ?? {}) };
9168
9813
  errorKeys.forEach((ek) => delete nextErrors[ek]);
9169
- // ✅ samo setErrors, bez updateValueAndValidity
9814
+ // ✅ Only update errors object, no re-validation side effects
9170
9815
  ctrl.setErrors(Object.keys(nextErrors).length ? nextErrors : null);
9171
9816
  });
9172
9817
  }
9818
+ /**
9819
+ * Marks all form controls as touched and dirty.
9820
+ * This is used on submit to make validation errors visible to the user.
9821
+ */
9173
9822
  markAllTouchedOnSubmit(form) {
9174
9823
  Object.values(form.controls).forEach((c) => {
9175
9824
  c.markAsTouched();
@@ -9189,71 +9838,129 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
9189
9838
  args: [META_FORM_ASYNC_EXECUTOR]
9190
9839
  }] }] });
9191
9840
 
9841
+ /**
9842
+ * Validator for START_DUE_DATE composite field.
9843
+ *
9844
+ * Responsibilities:
9845
+ * - Optionally enforce that BOTH startDate and endDate are provided (requireBoth)
9846
+ * - Validate that provided dates are valid Date values
9847
+ * - Ensure startDate is not after endDate
9848
+ *
9849
+ * This is a synchronous validator and should be attached to the single FormControl
9850
+ * that represents the composite START_DUE_DATE value.
9851
+ */
9192
9852
  function startDueDateV2Validator(opts) {
9193
9853
  const requireBoth = !!opts?.requireBoth;
9194
9854
  return (control) => {
9195
9855
  const v = control.value;
9196
9856
  const sdRaw = v?.startDate ?? null;
9197
9857
  const edRaw = v?.endDate ?? null;
9198
- // oba prazna
9858
+ // Case 1: both dates are empty
9859
+ // - If both are required -> invalid
9860
+ // - Otherwise -> valid (optional date range)
9199
9861
  if (!sdRaw && !edRaw) {
9200
9862
  return requireBoth ? { bothDates: true } : null;
9201
9863
  }
9202
- // fali jedan (ako su oba obavezna)
9864
+ // Case 2: only one date is provided but both are required
9203
9865
  if (requireBoth && (!sdRaw || !edRaw)) {
9204
9866
  return { bothDates: true };
9205
9867
  }
9206
- // ako nije requireBoth, a jedan fali -> ok
9868
+ // Case 3: both are optional and one is missing -> valid
9207
9869
  if (!sdRaw || !edRaw)
9208
9870
  return null;
9871
+ // Parse raw values into Date instances
9209
9872
  const sd = new Date(sdRaw);
9210
9873
  const ed = new Date(edRaw);
9874
+ // Invalid date values (e.g. unparsable strings)
9211
9875
  if (isNaN(sd.getTime()) || isNaN(ed.getTime()))
9212
9876
  return { dueDate: true };
9877
+ // Normalize both dates to midnight for date-only comparison (ignore time)
9213
9878
  const s = new Date(sd);
9214
9879
  s.setHours(0, 0, 0, 0);
9215
9880
  const e = new Date(ed);
9216
9881
  e.setHours(0, 0, 0, 0);
9882
+ // Start date must not be after end date
9217
9883
  return s.getTime() > e.getTime() ? { dueDate: true } : null;
9218
9884
  };
9219
9885
  }
9220
9886
 
9887
+ /**
9888
+ * Builds synchronous Angular validators for a single meta-field.
9889
+ *
9890
+ * This function only returns *sync* validators:
9891
+ * - Required, min/max, length
9892
+ * - Email
9893
+ * - Regex / phone
9894
+ * - Timeperiod
9895
+ * - Start/Due date cross-field-ish validator (but still sync)
9896
+ * - Whitespace rules
9897
+ * - Dangerous characters
9898
+ *
9899
+ * Submit-only async validators (e.g. uniqueness) are handled elsewhere (MetaSubmitValidatorService).
9900
+ */
9221
9901
  function buildSyncValidators(field, ctx) {
9222
9902
  const out = [];
9903
+ // `field.validators` is a metadata-driven bag of validation rules
9223
9904
  const v = field.validators ?? {};
9905
+ // Field type drives some special-case validators
9224
9906
  const type = field.configuration.type;
9907
+ // ---- numeric constraints ----
9225
9908
  if (v.min != null)
9226
9909
  out.push(Validators.min(v.min));
9227
9910
  if (v.max != null)
9228
9911
  out.push(Validators.max(v.max));
9912
+ // ---- length constraints ----
9229
9913
  if (v.minLength != null)
9230
9914
  out.push(Validators.minLength(v.minLength));
9231
9915
  if (v.maxLength != null)
9232
9916
  out.push(Validators.maxLength(v.maxLength));
9917
+ // ---- email ----
9233
9918
  if (v.email)
9234
9919
  out.push(Validators.email);
9235
- if (v.regex?.regexType)
9920
+ // ---- regex / pattern ----
9921
+ // Supports either:
9922
+ // - plain pattern string
9923
+ // - "/.../flags" format which we try to compile into RegExp
9924
+ if (v.regex?.regexType) {
9236
9925
  out.push(Validators.pattern(resolvePattern(v.regex.regexType) ?? v.regex.regexType));
9926
+ }
9927
+ // ---- phone ----
9928
+ // if (v.phone) out.push(Validators.pattern(/^\+?[1-9]\d{7,14}$/));
9237
9929
  if (v.phone)
9238
- out.push(Validators.pattern(/^\+?[1-9]\d{7,14}$/));
9930
+ out.push(phoneHumanValidator({ minDigits: 8, maxDigits: 15 }));
9931
+ // ---- time period ----
9932
+ // Localized validator; tpMin can enforce minimal value/length depending on your implementation.
9239
9933
  if (v.timeperiod)
9240
9934
  out.push(timePeriod(ctx?.lang ?? 'en', v.tpMin));
9935
+ // ---- start/due date ----
9936
+ // Apply the validator if field is of START_DUE_DATE type or if metadata explicitly enables dueDate.
9937
+ // requireBoth is driven by field.mandatory
9241
9938
  if (type === 'START_DUE_DATE' || v.dueDate) {
9242
9939
  out.push(startDueDateV2Validator({ requireBoth: !!field.mandatory }));
9243
9940
  }
9941
+ // ---- required ----
9244
9942
  if (field.mandatory)
9245
9943
  out.push(Validators.required);
9944
+ // ---- whitespace guard for required text fields ----
9945
+ // Prevent " " from passing required validation.
9246
9946
  if (field.mandatory && (type === 'TEXT' || type === 'TEXT_AREA')) {
9247
9947
  out.push(WhiteSpaceValidator.noWhiteSpaceValidator);
9248
9948
  }
9949
+ // ---- dangerous characters ----
9950
+ // Applied to text and textarea only.
9249
9951
  if (type === 'TEXT' || type === 'TEXT_AREA') {
9250
9952
  out.push(noDangerousCharsValidator());
9251
9953
  }
9252
9954
  return out;
9253
9955
  }
9956
+ /**
9957
+ * Tries to interpret a regex string in "/pattern/flags" format.
9958
+ * If parsing fails, returns null and the caller falls back to using the raw string.
9959
+ */
9254
9960
  function resolvePattern(regexType) {
9255
9961
  if (!regexType)
9256
9962
  return null;
9963
+ // Support format like: "/^[0-9]+$/g"
9257
9964
  const m = regexType.match(/^\/(.+)\/([gimsuy]*)$/);
9258
9965
  if (m) {
9259
9966
  try {
@@ -9263,26 +9970,94 @@ function resolvePattern(regexType) {
9263
9970
  return null;
9264
9971
  }
9265
9972
  }
9973
+ // Otherwise treat it as a plain string pattern
9266
9974
  return regexType;
9267
9975
  }
9976
+ /**
9977
+ * Phone validator that accepts common human formats:
9978
+ * - spaces, hyphens, parentheses
9979
+ * - local numbers starting with 0 (e.g. 060...)
9980
+ * - international numbers with +
9981
+ *
9982
+ * Strategy:
9983
+ * 1) Strip formatting chars
9984
+ * 2) Validate the normalized digits
9985
+ */
9986
+ function phoneHumanValidator(opts) {
9987
+ const min = opts?.minDigits ?? 8;
9988
+ const max = opts?.maxDigits ?? 15;
9989
+ return (control) => {
9990
+ const raw = (control.value ?? '').toString().trim();
9991
+ if (!raw)
9992
+ return null; // let required handle empties
9993
+ // Keep leading "+" if present, remove common separators
9994
+ const normalized = raw
9995
+ .replace(/[\s\-().]/g, '') // remove spaces, dashes, parentheses, dots
9996
+ .replace(/(?!^\+)\+/g, ''); // remove any "+" not at the start
9997
+ // Allow optional leading "+"
9998
+ const hasPlus = normalized.startsWith('+');
9999
+ const digits = hasPlus ? normalized.slice(1) : normalized;
10000
+ // Digits only after normalization
10001
+ if (!/^\d+$/.test(digits))
10002
+ return { phone: true };
10003
+ // Length constraints (digits only)
10004
+ if (digits.length < min || digits.length > max)
10005
+ return { phone: true };
10006
+ // If it has "+", require country code style (first digit not 0)
10007
+ if (hasPlus && digits.startsWith('0'))
10008
+ return { phone: true };
10009
+ // If local, allow leading 0 (e.g. 060...)
10010
+ return null;
10011
+ };
10012
+ }
9268
10013
 
10014
+ /**
10015
+ * Ensures that the FormGroup structure matches the provided metadata (flat field list).
10016
+ *
10017
+ * Responsibilities:
10018
+ * - Create missing FormControls based on MetaFieldConfig
10019
+ * - Apply (and re-apply) synchronous validators derived from metadata
10020
+ * - Sync disabled/enabled state with metadata
10021
+ * - Remove obsolete controls that are no longer present in metadata
10022
+ *
10023
+ * This function is intentionally idempotent:
10024
+ * Calling it multiple times with the same metadata should not break form state.
10025
+ */
9269
10026
  function ensureControlsV2(fb, form, flat, initialValues, ctx) {
10027
+ // Tracks which control keys are allowed by current metadata
9270
10028
  const allowed = new Set();
9271
10029
  for (const field of flat) {
9272
10030
  const key = field?.configuration?.key;
9273
10031
  if (!key)
9274
10032
  continue;
9275
10033
  allowed.add(key);
10034
+ /**
10035
+ * Create control if it does not exist yet.
10036
+ * Initial value is taken from initialValues map if provided,
10037
+ * otherwise defaults to null.
10038
+ */
9276
10039
  if (!form.contains(key)) {
9277
10040
  form.addControl(key, new FormControl(initialValues?.[key] ?? null));
9278
10041
  }
9279
10042
  const ctrl = form.get(key);
9280
10043
  if (!ctrl)
9281
10044
  continue;
9282
- // 1) validators
10045
+ /**
10046
+ * (1) Sync validators:
10047
+ * Rebuild and apply synchronous validators based on current field metadata.
10048
+ * This ensures that changes in metadata (e.g. required, min/max, patterns)
10049
+ * are reflected on the form control.
10050
+ */
9283
10051
  ctrl.setValidators(buildSyncValidators(field, ctx));
10052
+ // Recalculate validity silently (do not emit events to avoid UI side-effects)
9284
10053
  ctrl.updateValueAndValidity({ emitEvent: false });
9285
- // 2) disabled state (symetric)
10054
+ /**
10055
+ * (2) Sync disabled/enabled state:
10056
+ * The control's enabled state must reflect the field metadata.
10057
+ * This is symmetric:
10058
+ * - If metadata says "disable" → disable control
10059
+ * - If metadata says "enable" → enable control
10060
+ */
9286
10061
  const shouldBeDisabled = !!field.disable;
9287
10062
  if (shouldBeDisabled && ctrl.enabled) {
9288
10063
  ctrl.disable({ emitEvent: false });
@@ -9291,6 +10066,13 @@ function ensureControlsV2(fb, form, flat, initialValues, ctx) {
9291
10066
  ctrl.enable({ emitEvent: false });
9292
10067
  }
9293
10068
  }
10069
+ /**
10070
+ * Remove any controls that are present in the FormGroup
10071
+ * but no longer exist in the metadata.
10072
+ *
10073
+ * This keeps the form structure in sync when metadata changes dynamically
10074
+ * (e.g. conditional fields, stage-based forms, etc.).
10075
+ */
9294
10076
  Object.keys(form.controls).forEach((k) => {
9295
10077
  if (!allowed.has(k))
9296
10078
  form.removeControl(k);
@@ -9311,48 +10093,101 @@ function flattenControls(input) {
9311
10093
  }
9312
10094
 
9313
10095
  class MetaFormV2Component {
10096
+ /** Form instance created/owned by the parent (dialog/page) */
9314
10097
  form;
10098
+ /**
10099
+ * V2 metadata/config:
10100
+ * - controls (grouped or flat)
10101
+ * - initialValues
10102
+ * - submitValidators (run on submit only)
10103
+ * - setupDependencies (optional runtime bindings)
10104
+ */
9315
10105
  config;
9316
- /** Page-level readOnly (parent toggles) */
10106
+ /**
10107
+ * Page-level readOnly state controlled by parent.
10108
+ * Used both for rendering and for "enter edit mode" behavior.
10109
+ */
9317
10110
  readOnly = false;
9318
- /** Optional layout props */
10111
+ /** Optional layout customization for inner content wrapper */
9319
10112
  contentStyle = null;
10113
+ /** Optional class name(s) applied to inner content wrapper */
9320
10114
  contentClass = null;
10115
+ /** Builds/ensures controls exist (ensures validators & default values are wired) */
9321
10116
  fb = inject(FormBuilder);
10117
+ /** Registers and executes submit-only validators (async validation on submit) */
9322
10118
  submitValidator = inject(MetaSubmitValidatorService);
10119
+ /** Used for validator localization (lang-dependent validators / messages) */
9323
10120
  translate = inject(TranslateService);
10121
+ /**
10122
+ * A lightweight signature of the current metadata structure.
10123
+ * Used to detect when the form schema changes (and avoid unnecessary resets).
10124
+ */
9324
10125
  lastSignature = '';
10126
+ /**
10127
+ * Cleanup function returned by setupDependencies (if any).
10128
+ * Called only when metadata structure changes or on destroy.
10129
+ */
9325
10130
  depCleanup;
9326
- /** PrimeNG accordion active value(s) */
10131
+ /**
10132
+ * PrimeNG Accordion "value" for opened panels.
10133
+ * For multiple panels, PrimeNG expects an array of ids.
10134
+ */
9327
10135
  expandedGroupIds = [];
9328
10136
  ngOnChanges(changes) {
9329
10137
  if (!this.form || !this.config)
9330
10138
  return;
10139
+ /**
10140
+ * Flatten metadata controls to a single list for:
10141
+ * - building/enforcing FormControls
10142
+ * - dependency binding
10143
+ * - signature calculation
10144
+ */
9331
10145
  const flat = flattenControls(this.config.controls);
10146
+ /**
10147
+ * Signature is used to detect real schema changes.
10148
+ * We intentionally ignore labels/props and track key+type only.
10149
+ */
9332
10150
  const signature = flat.map((f) => `${f.configuration.key}:${f.configuration.type}`).join('|');
10151
+ /** "config changed" includes initialValues, validators, dependencies, etc. */
9333
10152
  const configChanged = !!changes['config'];
10153
+ /** "form changed" means parent passed a different FormGroup instance */
9334
10154
  const formChanged = !!changes['form'];
10155
+ /** schema changed if the signature differs from the last one */
9335
10156
  const metaChanged = signature !== this.lastSignature;
10157
+ /**
10158
+ * Special case: entering edit mode (readOnly true -> false).
10159
+ * Used to selectively show validation for already-filled values.
10160
+ */
9336
10161
  const enteringEdit = !!changes['readOnly'] &&
9337
10162
  changes['readOnly'].previousValue === true &&
9338
10163
  changes['readOnly'].currentValue === false;
9339
- // If nothing relevant changed, exit
10164
+ // If nothing relevant changed, exit early to avoid unnecessary work.
9340
10165
  if (!configChanged && !formChanged && !metaChanged && !enteringEdit)
9341
10166
  return;
9342
- // Cleanup dependencies ONLY when metadata/structure changes
10167
+ /**
10168
+ * Dependencies are tied to the metadata structure.
10169
+ * If schema changed, cleanup old subscriptions/bindings first.
10170
+ */
9343
10171
  if (configChanged || metaChanged) {
9344
10172
  this.depCleanup?.();
9345
10173
  this.depCleanup = undefined;
9346
10174
  }
9347
- // Ensure controls exist + sync validators
10175
+ /**
10176
+ * Ensure controls exist on the passed FormGroup and sync validators.
10177
+ * This is where missing controls are added and validator wiring is applied.
10178
+ */
9348
10179
  const initial = this.config.initialValues ?? {};
9349
10180
  ensureControlsV2(this.fb, this.form, flat, initial, { lang: this.translate.currentLang });
9350
- // Patch initial values (no emit)
10181
+ /**
10182
+ * Patch initial values without emitting changes:
10183
+ * - prevents loops
10184
+ * - keeps create/edit initialization silent
10185
+ */
9351
10186
  this.form.patchValue(initial, { emitEvent: false });
9352
10187
  this.form.updateValueAndValidity({ emitEvent: false });
9353
10188
  /**
9354
- * Initialize accordion expanded panels ONLY when structure changed.
9355
- * This must NOT run on readOnly toggle, otherwise you "reset" user-collapsed state.
10189
+ * Initialize accordion open panels ONLY when schema changes.
10190
+ * IMPORTANT: do NOT do this on readOnly toggle, otherwise user-collapsed state resets.
9356
10191
  */
9357
10192
  if (metaChanged) {
9358
10193
  if (this.isGrouped) {
@@ -9366,9 +10201,16 @@ class MetaFormV2Component {
9366
10201
  this.expandedGroupIds = [];
9367
10202
  }
9368
10203
  }
9369
- // Register submit-only validators (safe even if empty)
10204
+ /**
10205
+ * Register submit-only validators.
10206
+ * Safe to call even if empty; service will attach necessary structures internally.
10207
+ */
9370
10208
  this.submitValidator.register(this.form, this.config.submitValidators);
9371
- // Bind dependencies ONLY when metadata/structure changes
10209
+ /**
10210
+ * Bind dependencies ONLY when schema changes.
10211
+ * setupDependencies can subscribe to valueChanges, set options, reset fields, etc.
10212
+ * If it returns a function, we store it for cleanup.
10213
+ */
9372
10214
  if ((configChanged || metaChanged) && this.config.setupDependencies) {
9373
10215
  const maybeCleanup = this.config.setupDependencies({
9374
10216
  form: this.form,
@@ -9381,10 +10223,10 @@ class MetaFormV2Component {
9381
10223
  this.depCleanup = maybeCleanup;
9382
10224
  }
9383
10225
  /**
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.
10226
+ * Entering edit mode:
10227
+ * - mark controls as touched ONLY if they already have a meaningful value
10228
+ * - expand groups that contain "visible invalid" controls (invalid + touched/dirty)
10229
+ * This prevents CREATE dialogs from showing "required" errors immediately.
9388
10230
  */
9389
10231
  if (enteringEdit) {
9390
10232
  queueMicrotask(() => {
@@ -9392,12 +10234,19 @@ class MetaFormV2Component {
9392
10234
  this.expandVisibleInvalidGroupsUnion();
9393
10235
  });
9394
10236
  }
10237
+ // Store signature for the next change detection pass
9395
10238
  this.lastSignature = signature;
9396
10239
  }
9397
- // Template handler (typed exactly for our template usage)
10240
+ /**
10241
+ * PrimeNG Accordion emits value as:
10242
+ * - single id (string/number)
10243
+ * - array of ids
10244
+ * We normalize everything into string[] for stable internal state.
10245
+ */
9398
10246
  onAccordionValueChange(v) {
9399
10247
  this.expandedGroupIds = this.normalizeAccordionValue(v);
9400
10248
  }
10249
+ /** Normalizes Accordion value into a stable string[] representation */
9401
10250
  normalizeAccordionValue(v) {
9402
10251
  if (Array.isArray(v))
9403
10252
  return v.map((x) => `${x}`);
@@ -9406,31 +10255,44 @@ class MetaFormV2Component {
9406
10255
  return [`${v}`];
9407
10256
  }
9408
10257
  // ---------------- template helpers ----------------
10258
+ /** True when metadata contains at least one control definition */
9409
10259
  get hasControls() {
9410
10260
  return Array.isArray(this.config?.controls) && this.config.controls.length > 0;
9411
10261
  }
10262
+ /**
10263
+ * Heuristic: grouped config has "ctrl" on first element.
10264
+ * (Keeps template simple and avoids extra schema fields.)
10265
+ */
9412
10266
  get isGrouped() {
9413
10267
  const c = this.config?.controls ?? [];
9414
10268
  return !!c[0]?.ctrl;
9415
10269
  }
10270
+ /** Returns grouped schema structure (accordion groups) */
9416
10271
  get groupedControls() {
9417
10272
  return this.config?.controls ?? [];
9418
10273
  }
10274
+ /** Returns flat schema structure (grid mode) */
9419
10275
  get flatControls() {
9420
10276
  return this.config?.controls ?? [];
9421
10277
  }
10278
+ /** TrackBy for group rendering */
9422
10279
  groupTrack(g, idx) {
9423
10280
  return g?.id ?? idx;
9424
10281
  }
9425
- /** PrimeNG panel value must match accordion value type */
10282
+ /**
10283
+ * PrimeNG accordion panel `value` must match accordion `value` type.
10284
+ * We always convert group id to string for consistent behavior.
10285
+ */
9426
10286
  panelValue(g) {
9427
10287
  const id = g?.id;
9428
10288
  return id === null || id === undefined ? '' : `${id}`;
9429
10289
  }
9430
10290
  // ---------------- core behavior ----------------
9431
10291
  /**
9432
- * Mark & validate ONLY controls that already have a meaningful value.
9433
- * This prevents "required" errors from showing immediately on CREATE dialogs.
10292
+ * Marks & validates ONLY controls that already have meaningful values.
10293
+ * This is used when switching from readOnly -> edit mode to avoid:
10294
+ * - triggering "required" errors for empty fields
10295
+ * - expanding groups based on empty mandatory fields on CREATE dialogs
9434
10296
  */
9435
10297
  touchAndValidateOnlyFilledControls() {
9436
10298
  const controls = this.form?.controls ?? {};
@@ -9438,21 +10300,21 @@ class MetaFormV2Component {
9438
10300
  if (!ctrl)
9439
10301
  continue;
9440
10302
  const value = ctrl.value;
9441
- // Touch only if user already has a value (edit case) or prefilled values exist
10303
+ // Touch only if this field is already populated (edit case) or prefilled.
9442
10304
  if (this.hasMeaningfulValue(value)) {
9443
10305
  ctrl.markAsTouched();
9444
10306
  ctrl.updateValueAndValidity({ emitEvent: true });
9445
10307
  }
9446
10308
  }
9447
- // Keep form validity in sync (optional)
10309
+ // Optional: keep form status consistent after selective updates
9448
10310
  this.form.updateValueAndValidity({ emitEvent: true });
9449
10311
  }
9450
10312
  /**
9451
- * What counts as a "meaningful value"?
10313
+ * Defines what counts as a "meaningful" value:
9452
10314
  * - non-empty strings
9453
10315
  * - numbers / booleans
9454
10316
  * - non-empty arrays
9455
- * - objects with at least one key (or common "selected item" shapes)
10317
+ * - objects with common identifiers (key/uuid/id) or any own keys
9456
10318
  */
9457
10319
  hasMeaningfulValue(v) {
9458
10320
  if (v === null || v === undefined)
@@ -9466,34 +10328,43 @@ class MetaFormV2Component {
9466
10328
  if (Array.isArray(v))
9467
10329
  return v.length > 0;
9468
10330
  if (typeof v === 'object') {
9469
- // common selection shapes: { key }, { uuid }, { id }, etc.
10331
+ // Common selection shapes: { key }, { uuid }, { id }, etc.
9470
10332
  if ('key' in v && v.key != null && `${v.key}`.trim() !== '')
9471
10333
  return true;
9472
10334
  if ('uuid' in v && v.uuid != null && `${v.uuid}`.trim() !== '')
9473
10335
  return true;
9474
10336
  if ('id' in v && v.id != null && `${v.id}`.trim() !== '')
9475
10337
  return true;
9476
- // fallback: any own keys
10338
+ // Fallback: any own keys
9477
10339
  return Object.keys(v).length > 0;
9478
10340
  }
9479
10341
  return false;
9480
10342
  }
9481
10343
  /**
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.
10344
+ * "Visible invalid" means:
10345
+ * - invalid
10346
+ * - AND user has interacted with it (touched or dirty)
10347
+ * This matches typical UI behavior: show errors only after interaction.
9484
10348
  */
9485
10349
  isVisibleInvalid(ctrl) {
9486
10350
  if (!ctrl)
9487
10351
  return false;
9488
10352
  return ctrl.invalid && (ctrl.touched || ctrl.dirty);
9489
10353
  }
10354
+ /**
10355
+ * Checks if a group contains at least one visible invalid control.
10356
+ * Used to auto-expand groups when entering edit mode.
10357
+ */
9490
10358
  groupHasVisibleInvalid(g) {
9491
10359
  const keys = (g?.ctrl ?? [])
9492
10360
  .map((f) => f?.configuration?.key)
9493
10361
  .filter(Boolean);
9494
10362
  return keys.some((k) => this.isVisibleInvalid(this.form.get(k)));
9495
10363
  }
9496
- /** Expand invalid groups but KEEP already opened ones */
10364
+ /**
10365
+ * Expands all groups that contain visible invalid controls,
10366
+ * while preserving any groups already expanded by the user.
10367
+ */
9497
10368
  expandVisibleInvalidGroupsUnion() {
9498
10369
  if (!this.isGrouped)
9499
10370
  return;
@@ -9506,6 +10377,7 @@ class MetaFormV2Component {
9506
10377
  return;
9507
10378
  this.expandedGroupIds = Array.from(new Set([...this.expandedGroupIds, ...invalidIds]));
9508
10379
  }
10380
+ /** Cleanup dependency subscriptions when component is destroyed */
9509
10381
  ngOnDestroy() {
9510
10382
  this.depCleanup?.();
9511
10383
  }