@acorex/platform 20.9.9 → 20.9.11

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.
@@ -7182,6 +7182,9 @@ class AXPSelectBoxWidgetEditComponent extends AXPDataListWidgetComponent {
7182
7182
  this.destroyRef = inject(DestroyRef);
7183
7183
  /** Toggled to force ax-select-box to re-render after locale / language changes. */
7184
7184
  this.selectBoxKey = signal(true, ...(ngDevMode ? [{ debugName: "selectBoxKey" }] : /* istanbul ignore next */ []));
7185
+ /** Registry list query runs only after the user interacts (not on form mount). */
7186
+ this.listLoadEnabled = signal(false, ...(ngDevMode ? [{ debugName: "listLoadEnabled" }] : /* istanbul ignore next */ []));
7187
+ this.isLazyRegistrySelect = computed(() => !this.filterMode() && !!this.registryDataSourceName(), ...(ngDevMode ? [{ debugName: "isLazyRegistrySelect" }] : /* istanbul ignore next */ []));
7185
7188
  this.selectBoxLocaleChanged = merge(this.translationService.langChanges$, this.localeService.profileChanged$)
7186
7189
  .pipe(takeUntilDestroyed(this.destroyRef))
7187
7190
  .subscribe(() => this.rerenderSelectBox());
@@ -7203,6 +7206,33 @@ class AXPSelectBoxWidgetEditComponent extends AXPDataListWidgetComponent {
7203
7206
  this.isItemTruncated = computed(() => this.options()['isItemTruncated'] ?? true, ...(ngDevMode ? [{ debugName: "isItemTruncated" }] : /* istanbul ignore next */ []));
7204
7207
  this.filter = computed(() => this.options()['filter'], ...(ngDevMode ? [{ debugName: "filter" }] : /* istanbul ignore next */ []));
7205
7208
  this.filterMode = computed(() => this.options()['filterMode'] ?? false, ...(ngDevMode ? [{ debugName: "filterMode" }] : /* istanbul ignore next */ []));
7209
+ /**
7210
+ * True when a cascade/parent filter is configured but required values are still empty
7211
+ * (e.g. state select before country is chosen).
7212
+ */
7213
+ this.hasPendingCascadeFilter = computed(() => {
7214
+ if (this.filterMode()) {
7215
+ return false;
7216
+ }
7217
+ const rawFilter = this.filter();
7218
+ if (!rawFilter) {
7219
+ return false;
7220
+ }
7221
+ return AXPCleanNestedFilters([rawFilter]).length === 0;
7222
+ }, ...(ngDevMode ? [{ debugName: "hasPendingCascadeFilter" }] : /* istanbul ignore next */ []));
7223
+ /**
7224
+ * Registry-backed selects defer list loading until the user opens the dropdown.
7225
+ * Existing values are still resolved via `byKey` in the base widget.
7226
+ */
7227
+ this.shouldDeferListLoad = computed(() => this.isLazyRegistrySelect() && !this.listLoadEnabled(), ...(ngDevMode ? [{ debugName: "shouldDeferListLoad" }] : /* istanbul ignore next */ []));
7228
+ /** Only swap datasource when cascade parent is missing; defer load via patched `load` instead. */
7229
+ this.effectiveDataSource = computed(() => {
7230
+ if (this.hasPendingCascadeFilter()) {
7231
+ return AXPSelectBoxWidgetEditComponent.emptyDataSource;
7232
+ }
7233
+ return this.dataSource();
7234
+ }, ...(ngDevMode ? [{ debugName: "effectiveDataSource" }] : /* istanbul ignore next */ []));
7235
+ this.isSelectBoxDisabled = computed(() => this.disabled() || this.hasPendingCascadeFilter(), ...(ngDevMode ? [{ debugName: "isSelectBoxDisabled" }] : /* istanbul ignore next */ []));
7206
7236
  /** For filter mode: display valueField from object to avoid [object Object] when value is stored as full item */
7207
7237
  this.selectFilterDisplayValue = computed(() => {
7208
7238
  const raw = this.getValue();
@@ -7269,21 +7299,31 @@ class AXPSelectBoxWidgetEditComponent extends AXPDataListWidgetComponent {
7269
7299
  }
7270
7300
  }
7271
7301
  }, ...(ngDevMode ? [{ debugName: "#focusEffect" }] : /* istanbul ignore next */ []));
7272
- this.#effect = effect(() => {
7273
- if (this.filter()) {
7274
- const cleanedFilters = AXPCleanNestedFilters([this.filter()]);
7275
- if (cleanedFilters.length > 0) {
7276
- untracked(() => {
7277
- setTimeout(() => {
7278
- const normalized = this.filterMode()
7279
- ? this.normalizeFilterValues(cleanedFilters[0])
7280
- : cleanedFilters[0];
7281
- this.dataSource()?.filter(normalized);
7282
- });
7283
- });
7284
- }
7302
+ /** Applies widget filter to the isolated datasource whenever options change (cascade parent id). */
7303
+ this.#filterEffect = effect(() => {
7304
+ const filterOption = this.filter();
7305
+ const registryReady = this.registryDataSourceReady();
7306
+ if (!registryReady || !filterOption || this.filterMode() || this.hasPendingCascadeFilter()) {
7307
+ return;
7285
7308
  }
7286
- }, ...(ngDevMode ? [{ debugName: "#effect" }] : /* istanbul ignore next */ []));
7309
+ const widgetFilter = this.resolveActiveWidgetFilter();
7310
+ if (!widgetFilter) {
7311
+ return;
7312
+ }
7313
+ untracked(() => {
7314
+ this.dataSource()?.filter(widgetFilter);
7315
+ });
7316
+ }, ...(ngDevMode ? [{ debugName: "#filterEffect" }] : /* istanbul ignore next */ []));
7317
+ this.#registryDataSourceEffect = effect(() => {
7318
+ const registryReady = this.registryDataSourceReady();
7319
+ const registryName = this.registryDataSourceName();
7320
+ if (!registryReady || !registryName || this.filterMode()) {
7321
+ return;
7322
+ }
7323
+ untracked(() => {
7324
+ this.patchRegistryDataSourceLoad();
7325
+ });
7326
+ }, ...(ngDevMode ? [{ debugName: "#registryDataSourceEffect" }] : /* istanbul ignore next */ []));
7287
7327
  this.#clearOnDataSourceChangedEffect = effect(() => {
7288
7328
  const currentDataSource = this.options()['dataSource'];
7289
7329
  if (!this.hasSeenDataSource) {
@@ -7301,6 +7341,7 @@ class AXPSelectBoxWidgetEditComponent extends AXPDataListWidgetComponent {
7301
7341
  this.refresh();
7302
7342
  }, ...(ngDevMode ? [{ debugName: "#clearOnDataSourceChangedEffect" }] : /* istanbul ignore next */ []));
7303
7343
  }
7344
+ static { this.emptyDataSource = convertArrayToDataSource([]); }
7304
7345
  /**
7305
7346
  * In filter mode the stored value is wrapped as `{ value, operation, displayText }`.
7306
7347
  * Item resolution in the base widget needs the unwrapped key(s), so expose the same
@@ -7428,12 +7469,15 @@ class AXPSelectBoxWidgetEditComponent extends AXPDataListWidgetComponent {
7428
7469
  handleSearchValueChange(e) {
7429
7470
  if (e.isUserInteraction) {
7430
7471
  this.searchValue.set(e.value);
7472
+ if (this.shouldDeferListLoad()) {
7473
+ return;
7474
+ }
7475
+ const baseFilter = this.resolveActiveWidgetFilter();
7431
7476
  const searchFilter = this.filterOperatorMiddleware.transformFilter({
7432
7477
  field: this.textField(),
7433
7478
  operator: { type: 'contains' },
7434
7479
  value: e.value,
7435
7480
  });
7436
- const baseFilter = this.filter() ? AXPCleanNestedFilters([this.filter()])[0] : null;
7437
7481
  let mergedFilter = searchFilter;
7438
7482
  if (baseFilter) {
7439
7483
  if (this.searchValue() !== undefined) {
@@ -7453,20 +7497,74 @@ class AXPSelectBoxWidgetEditComponent extends AXPDataListWidgetComponent {
7453
7497
  this.dataSource()?.refresh();
7454
7498
  }
7455
7499
  }
7456
- #effect;
7500
+ /** Normalized, middleware-ready filter for the current widget options (or null). */
7501
+ resolveActiveWidgetFilter() {
7502
+ const filterOption = this.filter();
7503
+ if (!filterOption) {
7504
+ return null;
7505
+ }
7506
+ const cleanedFilters = AXPCleanNestedFilters([filterOption]);
7507
+ if (cleanedFilters.length === 0) {
7508
+ return null;
7509
+ }
7510
+ return this.filterOperatorMiddleware.transformFilter(this.normalizeDataSourceFilter(cleanedFilters[0]));
7511
+ }
7512
+ /** Applies widget filter to the isolated datasource whenever options change (cascade parent id). */
7513
+ #filterEffect;
7514
+ #registryDataSourceEffect;
7515
+ /**
7516
+ * Patches registry `load` to defer list fetch until the user opens the dropdown.
7517
+ * Filtering is handled via {@link #filterEffect} on the datasource (lookup-select pattern).
7518
+ */
7519
+ patchRegistryDataSourceLoad() {
7520
+ const ds = this.dataSource();
7521
+ const config = ds?.config;
7522
+ if (!config?.load || config.__axpSelectFilterPatched) {
7523
+ return;
7524
+ }
7525
+ const originalLoad = config.load.bind(ds);
7526
+ config.load = async (event) => {
7527
+ if (this.shouldDeferListLoad() || this.hasPendingCascadeFilter()) {
7528
+ return { items: [], total: 0 };
7529
+ }
7530
+ return originalLoad(event);
7531
+ };
7532
+ config.__axpSelectFilterPatched = true;
7533
+ }
7457
7534
  #clearOnDataSourceChangedEffect;
7535
+ // TODO: Duplicate registry list API calls — opening a cascade select (or country-change refresh)
7536
+ // currently triggers two loads (e.g. dataSource.refresh + selectBox.refresh, and/or #filterEffect
7537
+ // plus refresh/enableListLoad). Coalesce to a single load path without breaking cascade filter refresh.
7458
7538
  refresh() {
7459
7539
  setTimeout(() => {
7460
7540
  this.searchValue.set(undefined);
7541
+ this.applyActiveFilterToDataSource();
7542
+ this.listLoadEnabled.set(true);
7543
+ this.dataSource()?.refresh();
7461
7544
  this.selectBox()?.refresh();
7462
- }, 100);
7545
+ }, 0);
7546
+ }
7547
+ /** Push the current widget filter onto the isolated datasource. */
7548
+ applyActiveFilterToDataSource() {
7549
+ const widgetFilter = this.resolveActiveWidgetFilter();
7550
+ if (widgetFilter) {
7551
+ this.dataSource()?.filter(widgetFilter);
7552
+ }
7553
+ }
7554
+ /** Allow the patched datasource `load` before the dropdown panel opens. */
7555
+ enableListLoad() {
7556
+ if (this.filterMode() || this.hasPendingCascadeFilter() || !this.isLazyRegistrySelect()) {
7557
+ return;
7558
+ }
7559
+ this.listLoadEnabled.set(true);
7560
+ this.applyActiveFilterToDataSource();
7463
7561
  }
7464
7562
  /** Re-mount ax-select-box so translate pipe picks up the active locale. */
7465
7563
  rerenderSelectBox() {
7466
7564
  this.refreshRegistryDataSource();
7565
+ this.listLoadEnabled.set(false);
7467
7566
  this.selectBoxKey.set(false);
7468
7567
  setTimeout(() => {
7469
- this.dataSource()?.refresh();
7470
7568
  this.selectBoxKey.set(true);
7471
7569
  setTimeout(() => this.selectBox()?.refresh(), 0);
7472
7570
  }, 0);
@@ -7510,6 +7608,27 @@ class AXPSelectBoxWidgetEditComponent extends AXPDataListWidgetComponent {
7510
7608
  singleOrMultiple(values) {
7511
7609
  return this.multiple() ? values : values[0];
7512
7610
  }
7611
+ /**
7612
+ * Normalizes filter payloads for datasource queries: unwraps filter-mode values and
7613
+ * reduces selected row objects to scalar keys (default `id`).
7614
+ */
7615
+ normalizeDataSourceFilter(filter) {
7616
+ if (!filter) {
7617
+ return filter;
7618
+ }
7619
+ let normalized = this.filterMode() ? this.normalizeFilterValues(filter) : filter;
7620
+ if (Array.isArray(normalized.filters) && normalized.filters.length > 0) {
7621
+ return {
7622
+ ...normalized,
7623
+ filters: normalized.filters.map((entry) => this.normalizeDataSourceFilter(entry)),
7624
+ };
7625
+ }
7626
+ const extracted = extractValue(normalized.value, 'id');
7627
+ if (extracted != null && extracted !== normalized.value) {
7628
+ normalized = { ...normalized, value: extracted };
7629
+ }
7630
+ return normalized;
7631
+ }
7513
7632
  /**
7514
7633
  * In filterMode, widget context stores values as `{ value, operation, displayText }`.
7515
7634
  * DataSource filtering expects the raw scalar/array value, so unwrap `value.value`.
@@ -7540,8 +7659,8 @@ class AXPSelectBoxWidgetEditComponent extends AXPDataListWidgetComponent {
7540
7659
  } @else if (selectBoxKey()) {
7541
7660
  <ax-select-box
7542
7661
  [placeholder]="(placeholder() | translate | async) ?? ''"
7543
- [dataSource]="dataSource()"
7544
- [disabled]="disabled()"
7662
+ [dataSource]="effectiveDataSource()"
7663
+ [disabled]="isSelectBoxDisabled()"
7545
7664
  [valueField]="valueField()"
7546
7665
  [textField]="textField()"
7547
7666
  [textTemplate]="textTemplate()"
@@ -7550,6 +7669,8 @@ class AXPSelectBoxWidgetEditComponent extends AXPDataListWidgetComponent {
7550
7669
  [showItemTooltip]="showItemTooltip()"
7551
7670
  [isItemTruncated]="isItemTruncated()"
7552
7671
  [ngModel]="selectBoxModel()"
7672
+ (mousedown)="enableListLoad()"
7673
+ (focus)="enableListLoad()"
7553
7674
  (onValueChanged)="handleValueChange($event)"
7554
7675
  >
7555
7676
  @if (allowSearch()) {
@@ -7579,8 +7700,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
7579
7700
  } @else if (selectBoxKey()) {
7580
7701
  <ax-select-box
7581
7702
  [placeholder]="(placeholder() | translate | async) ?? ''"
7582
- [dataSource]="dataSource()"
7583
- [disabled]="disabled()"
7703
+ [dataSource]="effectiveDataSource()"
7704
+ [disabled]="isSelectBoxDisabled()"
7584
7705
  [valueField]="valueField()"
7585
7706
  [textField]="textField()"
7586
7707
  [textTemplate]="textTemplate()"
@@ -7589,6 +7710,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
7589
7710
  [showItemTooltip]="showItemTooltip()"
7590
7711
  [isItemTruncated]="isItemTruncated()"
7591
7712
  [ngModel]="selectBoxModel()"
7713
+ (mousedown)="enableListLoad()"
7714
+ (focus)="enableListLoad()"
7592
7715
  (onValueChanged)="handleValueChange($event)"
7593
7716
  >
7594
7717
  @if (allowSearch()) {