@acorex/platform 20.9.37 → 20.9.38

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.
@@ -12818,6 +12818,12 @@ class AXPEntityListWidgetViewComponent extends AXPValueWidgetComponent {
12818
12818
  this.selectedItems = signal([], ...(ngDevMode ? [{ debugName: "selectedItems" }] : /* istanbul ignore next */ []));
12819
12819
  this.toolbarNode = signal(null, ...(ngDevMode ? [{ debugName: "toolbarNode" }] : /* istanbul ignore next */ []));
12820
12820
  this.destroyed = new Subject();
12821
+ /** Invalidates in-flight filter/refresh work on remount, rapid page switches, or destroy. */
12822
+ this.opGeneration = 0;
12823
+ this.isViewDestroyed = false;
12824
+ this.initListTimeoutId = null;
12825
+ /** Unpatched data-list refresh (hits the grid). Prefer this over the scoped patch to avoid recursion. */
12826
+ this.boundDataListRefresh = null;
12821
12827
  //options
12822
12828
  this.entitySource = computed(() => this.options()['entity'], ...(ngDevMode ? [{ debugName: "entitySource" }] : /* istanbul ignore next */ []));
12823
12829
  this.excludeColumns = computed(() => this.options()['excludeColumns'], ...(ngDevMode ? [{ debugName: "excludeColumns" }] : /* istanbul ignore next */ []));
@@ -12930,20 +12936,20 @@ class AXPEntityListWidgetViewComponent extends AXPValueWidgetComponent {
12930
12936
  }
12931
12937
  inst.__axpEntityListRefreshPatched = true;
12932
12938
  const originalRefresh = inst.refresh.bind(inst);
12939
+ this.boundDataListRefresh = originalRefresh;
12933
12940
  inst.refresh = () => {
12934
- void (async () => {
12935
- const o = this.options();
12936
- const specs = o['relatedFilterConditionSpecs'];
12937
- if (o['syncRelatedListFiltersFromDialogContext'] && specs?.length) {
12938
- await this.applyRelatedFiltersFromContextAndDatasource(specs);
12939
- }
12940
- else {
12941
- await this.reapplyScopedFiltersToDataSource();
12942
- }
12943
- originalRefresh();
12944
- })();
12941
+ void this.runScopedDataListRefresh(originalRefresh);
12942
+ // Patched refresh is async; callers that need a boolean should use boundDataListRefresh.
12943
+ return false;
12945
12944
  };
12945
+ // If the initial filter apply raced ahead of the grid, retry load now that refresh is bound.
12946
+ untracked(() => {
12947
+ if (!this.isMounted() && !this.isViewDestroyed) {
12948
+ void this.reapplyScopedFiltersToDataSource({ triggerInitialLoad: true });
12949
+ }
12950
+ });
12946
12951
  }, ...(ngDevMode ? [{ debugName: "#patchDataListRefreshEffect" }] : /* istanbul ignore next */ []));
12952
+ //#endregion
12947
12953
  //#region ---- Query Change Handler ----
12948
12954
  this.queries = undefined;
12949
12955
  this.#effect = effect(() => {
@@ -13156,14 +13162,29 @@ class AXPEntityListWidgetViewComponent extends AXPValueWidgetComponent {
13156
13162
  const routeFilters = this.parseFiltersFromRoute();
13157
13163
  return routeFilters.length ? this.mergeToolbarFilters(base, routeFilters) : base;
13158
13164
  }
13165
+ //#region ---- Scoped filter / initial load (race-safe) ----
13166
+ beginOp() {
13167
+ return ++this.opGeneration;
13168
+ }
13169
+ isOpStale(generation) {
13170
+ return this.isViewDestroyed || generation !== this.opGeneration;
13171
+ }
13172
+ delay(ms) {
13173
+ return new Promise((resolve) => setTimeout(resolve, ms));
13174
+ }
13159
13175
  /**
13160
13176
  * Applies merged route + parent-scope filters to the widget value and data source.
13161
13177
  */
13162
13178
  applyMergedRouteFiltersToList() {
13163
- void this.reapplyScopedFiltersToDataSource();
13179
+ void this.reapplyScopedFiltersToDataSource({ triggerInitialLoad: true });
13164
13180
  }
13165
- /** Syncs toolbar filters to context and AXDataSource (retries until data-list is mounted). */
13166
- async reapplyScopedFiltersToDataSource() {
13181
+ /**
13182
+ * Syncs toolbar filters to context and AXDataSource (retries until data-list is mounted).
13183
+ * Stale generations (destroy / newer op) exit without touching the grid.
13184
+ */
13185
+ async reapplyScopedFiltersToDataSource(options) {
13186
+ const generation = options?.generation ?? this.beginOp();
13187
+ const triggerInitialLoad = options?.triggerInitialLoad !== false;
13167
13188
  const merged = this.getMergedToolbarFilters();
13168
13189
  const current = this.getValue();
13169
13190
  if (!isEqual(current?.toolbar?.filters, merged)) {
@@ -13172,26 +13193,64 @@ class AXPEntityListWidgetViewComponent extends AXPValueWidgetComponent {
13172
13193
  toolbar: { ...(current?.toolbar ?? {}), filters: merged },
13173
13194
  });
13174
13195
  }
13175
- const pushed = await this.ensureToolbarFiltersOnDataSource();
13176
- if (pushed) {
13177
- this.triggerInitialLoadIfNeeded();
13196
+ if (this.isOpStale(generation)) {
13197
+ return;
13198
+ }
13199
+ const pushed = await this.ensureToolbarFiltersOnDataSource(generation);
13200
+ if (!pushed || this.isOpStale(generation)) {
13201
+ return;
13202
+ }
13203
+ if (triggerInitialLoad) {
13204
+ await this.triggerInitialLoadIfNeeded(generation);
13178
13205
  }
13179
13206
  }
13180
13207
  /**
13181
13208
  * Entity lists use `fetchDataMode: 'manual'`; after scoped filters reach the data source,
13182
- * the grid must be refreshed once (initial #effect may run before the data-list is ready).
13209
+ * the grid must be refreshed once. Retries until the data-table view child accepts refresh
13210
+ * so rapid relatedEntity page switches cannot leave the table on "no data loaded yet".
13183
13211
  */
13184
- triggerInitialLoadIfNeeded() {
13185
- if (this.isMounted()) {
13212
+ async triggerInitialLoadIfNeeded(generation) {
13213
+ if (this.isMounted() || this.isOpStale(generation)) {
13186
13214
  return;
13187
13215
  }
13188
- const listInstance = this.listWidget()?.instance;
13189
- if (!listInstance?.call) {
13190
- return;
13216
+ const refreshed = await this.refreshWhenGridReady(generation);
13217
+ if (refreshed && !this.isOpStale(generation)) {
13218
+ this.isMounted.set(true);
13219
+ }
13220
+ }
13221
+ /**
13222
+ * Calls the unpatched data-list refresh until the grid is ready or the op is stale.
13223
+ */
13224
+ async refreshWhenGridReady(generation) {
13225
+ return this.refreshWhenGridReadyWith(generation, () => this.resolveDataListRefresh()?.() ?? false);
13226
+ }
13227
+ /**
13228
+ * Prefer the bound original refresh (pre-patch). Falls back to an unpatched instance refresh.
13229
+ */
13230
+ resolveDataListRefresh() {
13231
+ if (this.boundDataListRefresh) {
13232
+ return this.boundDataListRefresh;
13233
+ }
13234
+ const inst = this.listWidget()?.instance;
13235
+ if (!inst?.refresh || inst.__axpEntityListRefreshPatched) {
13236
+ return null;
13237
+ }
13238
+ return () => inst.refresh?.() ?? false;
13239
+ }
13240
+ async refreshWhenGridReadyWith(generation, refreshFn) {
13241
+ const maxAttempts = 60;
13242
+ const delayMs = 50;
13243
+ for (let i = 0; i < maxAttempts; i++) {
13244
+ if (this.isOpStale(generation)) {
13245
+ return false;
13246
+ }
13247
+ this.pushToolbarFiltersToDataSource();
13248
+ if (refreshFn()) {
13249
+ return true;
13250
+ }
13251
+ await this.delay(delayMs);
13191
13252
  }
13192
- this.pushToolbarFiltersToDataSource();
13193
- listInstance.call('refresh');
13194
- this.isMounted.set(true);
13253
+ return false;
13195
13254
  }
13196
13255
  /**
13197
13256
  * Re-evaluates related-entity list filters from the live dialog form context (e.g. after create saves the main row id).
@@ -13209,7 +13268,7 @@ class AXPEntityListWidgetViewComponent extends AXPValueWidgetComponent {
13209
13268
  value: await this.expressionEvaluator.evaluate(c.value, scope),
13210
13269
  hidden: true,
13211
13270
  })));
13212
- if (!this.fullPath()) {
13271
+ if (this.isViewDestroyed || !this.fullPath()) {
13213
13272
  return;
13214
13273
  }
13215
13274
  const current = this.getValue();
@@ -13241,14 +13300,18 @@ class AXPEntityListWidgetViewComponent extends AXPValueWidgetComponent {
13241
13300
  * Pushes toolbar filters once the embedded data-list exposes its data source
13242
13301
  * (listNode is created via deferred setTimeout in ngOnInit).
13243
13302
  */
13244
- async ensureToolbarFiltersOnDataSource() {
13303
+ async ensureToolbarFiltersOnDataSource(generation) {
13304
+ const opGeneration = generation ?? this.opGeneration;
13245
13305
  if (this.pushToolbarFiltersToDataSource()) {
13246
13306
  return true;
13247
13307
  }
13248
13308
  const maxAttempts = 40;
13249
13309
  const delayMs = 50;
13250
13310
  for (let i = 0; i < maxAttempts; i++) {
13251
- await new Promise((resolve) => setTimeout(resolve, delayMs));
13311
+ await this.delay(delayMs);
13312
+ if (this.isOpStale(opGeneration)) {
13313
+ return false;
13314
+ }
13252
13315
  if (this.pushToolbarFiltersToDataSource()) {
13253
13316
  return true;
13254
13317
  }
@@ -13259,8 +13322,12 @@ class AXPEntityListWidgetViewComponent extends AXPValueWidgetComponent {
13259
13322
  * Writes toolbar filters from specs and pushes them onto the data source so refresh/reload keeps the parent scope.
13260
13323
  */
13261
13324
  async applyRelatedFiltersFromContextAndDatasource(specs) {
13325
+ const generation = this.beginOp();
13262
13326
  await this.applyRelatedFiltersFromContext(specs);
13263
- await this.ensureToolbarFiltersOnDataSource();
13327
+ if (this.isOpStale(generation)) {
13328
+ return;
13329
+ }
13330
+ await this.ensureToolbarFiltersOnDataSource(generation);
13264
13331
  }
13265
13332
  /**
13266
13333
  * Refreshes the embedded data list (toolbar / workflow). In wizard mode, `refresh` is patched to re-apply scoped filters first.
@@ -13270,6 +13337,31 @@ class AXPEntityListWidgetViewComponent extends AXPValueWidgetComponent {
13270
13337
  }
13271
13338
  /** Patches data-list `refresh` so reload keeps parent-scoped toolbar filters on the data source. */
13272
13339
  #patchDataListRefreshEffect;
13340
+ /**
13341
+ * Re-applies parent-scoped filters then refreshes the grid, ignoring stale overlapping calls.
13342
+ */
13343
+ async runScopedDataListRefresh(originalRefresh) {
13344
+ const generation = this.beginOp();
13345
+ const o = this.options();
13346
+ const specs = o['relatedFilterConditionSpecs'];
13347
+ if (o['syncRelatedListFiltersFromDialogContext'] && specs?.length) {
13348
+ await this.applyRelatedFiltersFromContext(specs);
13349
+ if (this.isOpStale(generation)) {
13350
+ return;
13351
+ }
13352
+ await this.ensureToolbarFiltersOnDataSource(generation);
13353
+ }
13354
+ else {
13355
+ await this.reapplyScopedFiltersToDataSource({ triggerInitialLoad: false, generation });
13356
+ }
13357
+ if (this.isOpStale(generation)) {
13358
+ return;
13359
+ }
13360
+ const refreshed = await this.refreshWhenGridReadyWith(generation, originalRefresh);
13361
+ if (refreshed && !this.isOpStale(generation) && !this.isMounted()) {
13362
+ this.isMounted.set(true);
13363
+ }
13364
+ }
13273
13365
  #effect;
13274
13366
  /**
13275
13367
  * Validates that all required dependencies are available
@@ -13375,6 +13467,9 @@ class AXPEntityListWidgetViewComponent extends AXPValueWidgetComponent {
13375
13467
  }
13376
13468
  // Resolve entity and set it in the signal
13377
13469
  const resolvedEntity = await this.entityService.resolve(moduleName, entityName);
13470
+ if (this.isViewDestroyed) {
13471
+ return;
13472
+ }
13378
13473
  if (!resolvedEntity) {
13379
13474
  throw new Error(`Entity not found: ${this.entitySource()}`);
13380
13475
  }
@@ -13387,8 +13482,18 @@ class AXPEntityListWidgetViewComponent extends AXPValueWidgetComponent {
13387
13482
  excludeProperties: this.options()['excludeProperties'],
13388
13483
  };
13389
13484
  const listOptions = await this.entityListTableService.convertEntityToListOptions(resolvedEntity, options, this.allActions());
13485
+ if (this.isViewDestroyed) {
13486
+ return;
13487
+ }
13390
13488
  const toolbarOptions = await this.entityListToolbarService.convertEntityToolbarOptions(resolvedEntity, options);
13391
- setTimeout(() => {
13489
+ if (this.isViewDestroyed) {
13490
+ return;
13491
+ }
13492
+ this.initListTimeoutId = setTimeout(() => {
13493
+ this.initListTimeoutId = null;
13494
+ if (this.isViewDestroyed) {
13495
+ return;
13496
+ }
13392
13497
  this.listNode.set({
13393
13498
  type: AXPWidgetsCatalog.dataList,
13394
13499
  options: listOptions,
@@ -13410,7 +13515,9 @@ class AXPEntityListWidgetViewComponent extends AXPValueWidgetComponent {
13410
13515
  },
13411
13516
  });
13412
13517
  queueMicrotask(() => {
13413
- this.applyMergedRouteFiltersToList();
13518
+ if (!this.isViewDestroyed) {
13519
+ this.applyMergedRouteFiltersToList();
13520
+ }
13414
13521
  });
13415
13522
  }, 100);
13416
13523
  }
@@ -13435,6 +13542,9 @@ class AXPEntityListWidgetViewComponent extends AXPValueWidgetComponent {
13435
13542
  }
13436
13543
  });
13437
13544
  const listWidget = (await this.layoutService.waitForWidget(`${this.entitySource()}-tab-list_table`, 500));
13545
+ if (this.isViewDestroyed) {
13546
+ return;
13547
+ }
13438
13548
  if (listWidget?.api && typeof listWidget.api === 'function') {
13439
13549
  const onSelectionChange = listWidget.api()['onSelectionChange'];
13440
13550
  if (onSelectionChange) {
@@ -13445,6 +13555,13 @@ class AXPEntityListWidgetViewComponent extends AXPValueWidgetComponent {
13445
13555
  }
13446
13556
  }
13447
13557
  ngOnDestroy() {
13558
+ this.isViewDestroyed = true;
13559
+ this.beginOp();
13560
+ if (this.initListTimeoutId != null) {
13561
+ clearTimeout(this.initListTimeoutId);
13562
+ this.initListTimeoutId = null;
13563
+ }
13564
+ this.boundDataListRefresh = null;
13448
13565
  this.listWidget.set(undefined);
13449
13566
  this.destroyed.next();
13450
13567
  this.destroyed.complete();