@acorex/platform 20.9.37 → 20.9.39

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.
@@ -1,4 +1,4 @@
1
- import { mergeForeignKeyFieldIntoCreateActions, collectNestedCreateHiddenProperties, AXPEntityCommandScope, normalizeEntityDisplayTemplate, sanitizeResolvedEntityDisplayTitle, isUnresolvedEntityDisplayTemplate, resolveEntityListFilterContextField, resolveEntityListFilterQueryField, AXPEntityDataSourceKeys, isCardFieldBadgeDisplay, resolveCardFieldBadgeColor, isEntityActionPermitted, getEntityInfo, resolveEnabledMasterListLayouts, resolveDefaultMasterListLayout, AXPEntityEventsKeys, collectEntityListSortableFields, collectEntityListFilterDefinitions, parseDottedFieldPath, ENTITY_ROW_POPUP_TITLE_OPTION, resolveLookupWidgetCreateOptions, buildPageListWidgetName, AXPEntityQueryType, buildEntitySearchTitleContext } from '@acorex/platform/layout/entity-contracts';
1
+ import { mergeForeignKeyFieldIntoCreateActions, collectNestedCreateHiddenProperties, AXPEntityCommandScope, normalizeEntityDisplayTemplate, sanitizeResolvedEntityDisplayTitle, isUnresolvedEntityDisplayTemplate, resolveEntityListFilterContextField, resolveEntityListFilterQueryField, AXPEntityDataSourceKeys, isCardFieldBadgeDisplay, resolveCardFieldBadgeColor, isEntityActionPermitted, getEntityInfo, resolveEnabledMasterListLayouts, resolveDefaultMasterListLayout, AXPEntityEventsKeys, collectEntityListSortableFields, collectEntityListFilterDefinitions, parseDottedFieldPath, ENTITY_ROW_POPUP_TITLE_OPTION, buildPageListWidgetName, resolveLookupWidgetCreateOptions, AXPEntityQueryType, buildEntitySearchTitleContext } from '@acorex/platform/layout/entity-contracts';
2
2
  export * from '@acorex/platform/layout/entity-contracts';
3
3
  export { collectNestedCreateHiddenProperties, entityDetailsCreateActions, entityDetailsCreateActionsDeferredParent, entityDetailsCrudActions, entityDetailsEditAction, entityDetailsNewEditAction, entityDetailsReferenceCondition, entityDetailsReferenceCreateActions, entityDetailsSimpleCondition, entityMasterBulkDeleteAction, entityMasterCreateAction, entityMasterCrudActions, entityMasterDeleteAction, entityMasterEditAction, entityMasterRecordActions, entityMasterViewAction, entityOverrideDetailsViewAction, mergeForeignKeyFieldIntoCreateActions } from '@acorex/platform/layout/entity-contracts';
4
4
  import { AXToastService } from '@acorex/components/toast';
@@ -4552,12 +4552,14 @@ class AXPEntityListViewColumnViewModel {
4552
4552
  this.width = this.column?.options?.width;
4553
4553
  this.node = computed(() => {
4554
4554
  const widget = this.property.schema.interface;
4555
+ // Column `visible` is resolved by the list viewmodel; do not forward expressions into widget options.
4556
+ const { visible: _columnVisible, ...columnOptions } = this.column?.options ?? {};
4555
4557
  return {
4556
4558
  path: this.column.options?.dataPath ?? this.name,
4557
4559
  type: widget.type,
4558
4560
  options: {
4559
4561
  ...widget?.options,
4560
- ...this.column?.options,
4562
+ ...columnOptions,
4561
4563
  columnName: this.name,
4562
4564
  // Include children from widget interface so column components can access them
4563
4565
  ...(widget.children ? { children: widget.children } : {}),
@@ -5587,7 +5589,7 @@ class AXPEntityMasterListViewModel {
5587
5589
  if (shouldApplyViewState) {
5588
5590
  this.view.set(nextView);
5589
5591
  this.applyViewSorts();
5590
- this.applyViewColumns();
5592
+ await this.applyViewColumns();
5591
5593
  this.applyCardFields();
5592
5594
  this.applyViewFilters();
5593
5595
  this.resolvedListPaging.set(null);
@@ -5971,7 +5973,12 @@ class AXPEntityMasterListViewModel {
5971
5973
  for (const column of next) {
5972
5974
  const columnKey = column.column?.options?.dataPath ?? column.name;
5973
5975
  column.width = defaultTo(columnWidthsMap.get(columnKey), column.column?.options?.width);
5974
- column.visible = columnVisibilityMap.get(columnKey) ?? column.visible;
5976
+ // Definition visibility (view.columns whitelist + options.visible) always wins for "hide".
5977
+ // Persisted settings may only further hide an already-allowed column.
5978
+ const definitionVisible = column.visible === true;
5979
+ const persistedVisible = columnVisibilityMap.get(columnKey);
5980
+ column.visible =
5981
+ definitionVisible && (persistedVisible === undefined ? true : persistedVisible !== false);
5975
5982
  }
5976
5983
  return next;
5977
5984
  });
@@ -6107,7 +6114,7 @@ class AXPEntityMasterListViewModel {
6107
6114
  const existingColumns = prev?.list?.views?.[this.view().name]?.columns;
6108
6115
  const baseColumns = Array.isArray(existingColumns) && existingColumns.length
6109
6116
  ? existingColumns
6110
- : this.allAvailableColumns().map((c) => ({
6117
+ : this.columns().map((c) => ({
6111
6118
  name: c.column?.options?.dataPath ?? c.name,
6112
6119
  visible: c.visible,
6113
6120
  width: c.width,
@@ -6116,11 +6123,6 @@ class AXPEntityMasterListViewModel {
6116
6123
  ...c,
6117
6124
  width: c.name === field ? columnSizeData.width : c.width,
6118
6125
  }));
6119
- const x = this.allAvailableColumns().map((c) => ({
6120
- name: c.column?.options?.dataPath ?? c.name,
6121
- visible: c.visible,
6122
- width: c.width,
6123
- }));
6124
6126
  set(newSettings, `list.views.${this.view().name}.columns`, updatedColumns);
6125
6127
  return newSettings;
6126
6128
  });
@@ -6638,22 +6640,59 @@ class AXPEntityMasterListViewModel {
6638
6640
  const relatedDef = this.getRegisteredEntity(related.entity);
6639
6641
  return relatedDef?.properties?.find((property) => property.name === propertyName) ?? null;
6640
6642
  }
6641
- resetColumns() {
6642
- this.applyViewColumns();
6643
+ async resetColumns() {
6644
+ await this.applyViewColumns();
6643
6645
  }
6644
6646
  async onColumnsChanged(columns) {
6645
6647
  this.saveSettings('columnOrders', columns);
6646
6648
  }
6647
- applyViewColumns() {
6648
- const cols = this.view().columns;
6649
- const cloned = this.allAvailableColumns().map((c) => {
6649
+ /**
6650
+ * Builds list columns for the active view:
6651
+ * - `view.columns` is a whitelist (empty = all columns)
6652
+ * - string `options.visible` is evaluated against the master-list scope
6653
+ */
6654
+ async applyViewColumns() {
6655
+ const viewColumnNames = this.view().columns ?? [];
6656
+ const scope = this.createLogicExpressionScope(null);
6657
+ const cloned = await Promise.all(this.allAvailableColumns().map(async (c) => {
6650
6658
  const column = this.entityDef.columns?.find((cc) => cc.name == c.name);
6651
6659
  const col = new AXPEntityListViewColumnViewModel(c.property, column);
6652
- col.visible = !cols.some((c) => c == col.name) && col.visible != false;
6660
+ const expressionVisible = await this.resolveColumnVisibleExpression(col.visible, scope);
6661
+ const includedInView = viewColumnNames.length === 0 || viewColumnNames.some((name) => name === col.name);
6662
+ col.visible = includedInView && expressionVisible;
6653
6663
  return col;
6654
- });
6664
+ }));
6665
+ if (viewColumnNames.length > 0) {
6666
+ cloned.sort((a, b) => {
6667
+ const aIndex = viewColumnNames.indexOf(a.name);
6668
+ const bIndex = viewColumnNames.indexOf(b.name);
6669
+ const aOrder = aIndex === -1 ? Number.MAX_SAFE_INTEGER : aIndex;
6670
+ const bOrder = bIndex === -1 ? Number.MAX_SAFE_INTEGER : bIndex;
6671
+ return aOrder - bOrder;
6672
+ });
6673
+ }
6655
6674
  this.columns.set(cloned);
6656
6675
  }
6676
+ /**
6677
+ * Resolves column `options.visible` (boolean or expression) to a boolean for the current list scope.
6678
+ */
6679
+ async resolveColumnVisibleExpression(visible, scope) {
6680
+ if (visible === false) {
6681
+ return false;
6682
+ }
6683
+ if (visible === true || visible == null || visible === '') {
6684
+ return true;
6685
+ }
6686
+ if (typeof visible === 'string') {
6687
+ if (!visible.includes('{{')) {
6688
+ const normalized = visible.trim().toLowerCase();
6689
+ return normalized !== 'false' && normalized !== '0';
6690
+ }
6691
+ const result = await this.expressionEvaluator.evaluate(visible, scope);
6692
+ return result !== false;
6693
+ }
6694
+ return Boolean(visible);
6695
+ }
6657
6696
  onColumnsOrderChanged(event) {
6658
6697
  const columns = [...this.columns()];
6659
6698
  moveItemInArray(columns, event.previousIndex - 1, event.currentIndex - 1);
@@ -12818,6 +12857,12 @@ class AXPEntityListWidgetViewComponent extends AXPValueWidgetComponent {
12818
12857
  this.selectedItems = signal([], ...(ngDevMode ? [{ debugName: "selectedItems" }] : /* istanbul ignore next */ []));
12819
12858
  this.toolbarNode = signal(null, ...(ngDevMode ? [{ debugName: "toolbarNode" }] : /* istanbul ignore next */ []));
12820
12859
  this.destroyed = new Subject();
12860
+ /** Invalidates in-flight filter/refresh work on remount, rapid page switches, or destroy. */
12861
+ this.opGeneration = 0;
12862
+ this.isViewDestroyed = false;
12863
+ this.initListTimeoutId = null;
12864
+ /** Unpatched data-list refresh (hits the grid). Prefer this over the scoped patch to avoid recursion. */
12865
+ this.boundDataListRefresh = null;
12821
12866
  //options
12822
12867
  this.entitySource = computed(() => this.options()['entity'], ...(ngDevMode ? [{ debugName: "entitySource" }] : /* istanbul ignore next */ []));
12823
12868
  this.excludeColumns = computed(() => this.options()['excludeColumns'], ...(ngDevMode ? [{ debugName: "excludeColumns" }] : /* istanbul ignore next */ []));
@@ -12930,20 +12975,20 @@ class AXPEntityListWidgetViewComponent extends AXPValueWidgetComponent {
12930
12975
  }
12931
12976
  inst.__axpEntityListRefreshPatched = true;
12932
12977
  const originalRefresh = inst.refresh.bind(inst);
12978
+ this.boundDataListRefresh = originalRefresh;
12933
12979
  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
- })();
12980
+ void this.runScopedDataListRefresh(originalRefresh);
12981
+ // Patched refresh is async; callers that need a boolean should use boundDataListRefresh.
12982
+ return false;
12945
12983
  };
12984
+ // If the initial filter apply raced ahead of the grid, retry load now that refresh is bound.
12985
+ untracked(() => {
12986
+ if (!this.isMounted() && !this.isViewDestroyed) {
12987
+ void this.reapplyScopedFiltersToDataSource({ triggerInitialLoad: true });
12988
+ }
12989
+ });
12946
12990
  }, ...(ngDevMode ? [{ debugName: "#patchDataListRefreshEffect" }] : /* istanbul ignore next */ []));
12991
+ //#endregion
12947
12992
  //#region ---- Query Change Handler ----
12948
12993
  this.queries = undefined;
12949
12994
  this.#effect = effect(() => {
@@ -13125,6 +13170,7 @@ class AXPEntityListWidgetViewComponent extends AXPValueWidgetComponent {
13125
13170
  /**
13126
13171
  * Route `filters` apply only when the active details `page` matches this list's entity
13127
13172
  * (e.g. WorkOrder filters must not affect FailureRegister on the same asset layout).
13173
+ * page-list sidebar pages use `page-list-{module.entity}` as their page id.
13128
13174
  */
13129
13175
  shouldApplyRouteFilters() {
13130
13176
  const pageId = this.route.snapshot.queryParamMap.get('page');
@@ -13136,7 +13182,7 @@ class AXPEntityListWidgetViewComponent extends AXPValueWidgetComponent {
13136
13182
  return false;
13137
13183
  }
13138
13184
  const [, entityName] = entitySource.split('.');
13139
- return !!entityName && pageId === entityName;
13185
+ return ((!!entityName && pageId === entityName) || pageId === buildPageListWidgetName(entitySource));
13140
13186
  }
13141
13187
  /**
13142
13188
  * Merges route filters into related-entity toolbar filters (route wins per field).
@@ -13156,14 +13202,29 @@ class AXPEntityListWidgetViewComponent extends AXPValueWidgetComponent {
13156
13202
  const routeFilters = this.parseFiltersFromRoute();
13157
13203
  return routeFilters.length ? this.mergeToolbarFilters(base, routeFilters) : base;
13158
13204
  }
13205
+ //#region ---- Scoped filter / initial load (race-safe) ----
13206
+ beginOp() {
13207
+ return ++this.opGeneration;
13208
+ }
13209
+ isOpStale(generation) {
13210
+ return this.isViewDestroyed || generation !== this.opGeneration;
13211
+ }
13212
+ delay(ms) {
13213
+ return new Promise((resolve) => setTimeout(resolve, ms));
13214
+ }
13159
13215
  /**
13160
13216
  * Applies merged route + parent-scope filters to the widget value and data source.
13161
13217
  */
13162
13218
  applyMergedRouteFiltersToList() {
13163
- void this.reapplyScopedFiltersToDataSource();
13219
+ void this.reapplyScopedFiltersToDataSource({ triggerInitialLoad: true });
13164
13220
  }
13165
- /** Syncs toolbar filters to context and AXDataSource (retries until data-list is mounted). */
13166
- async reapplyScopedFiltersToDataSource() {
13221
+ /**
13222
+ * Syncs toolbar filters to context and AXDataSource (retries until data-list is mounted).
13223
+ * Stale generations (destroy / newer op) exit without touching the grid.
13224
+ */
13225
+ async reapplyScopedFiltersToDataSource(options) {
13226
+ const generation = options?.generation ?? this.beginOp();
13227
+ const triggerInitialLoad = options?.triggerInitialLoad !== false;
13167
13228
  const merged = this.getMergedToolbarFilters();
13168
13229
  const current = this.getValue();
13169
13230
  if (!isEqual(current?.toolbar?.filters, merged)) {
@@ -13172,26 +13233,64 @@ class AXPEntityListWidgetViewComponent extends AXPValueWidgetComponent {
13172
13233
  toolbar: { ...(current?.toolbar ?? {}), filters: merged },
13173
13234
  });
13174
13235
  }
13175
- const pushed = await this.ensureToolbarFiltersOnDataSource();
13176
- if (pushed) {
13177
- this.triggerInitialLoadIfNeeded();
13236
+ if (this.isOpStale(generation)) {
13237
+ return;
13238
+ }
13239
+ const pushed = await this.ensureToolbarFiltersOnDataSource(generation);
13240
+ if (!pushed || this.isOpStale(generation)) {
13241
+ return;
13242
+ }
13243
+ if (triggerInitialLoad) {
13244
+ await this.triggerInitialLoadIfNeeded(generation);
13178
13245
  }
13179
13246
  }
13180
13247
  /**
13181
13248
  * 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).
13249
+ * the grid must be refreshed once. Retries until the data-table view child accepts refresh
13250
+ * so rapid relatedEntity page switches cannot leave the table on "no data loaded yet".
13183
13251
  */
13184
- triggerInitialLoadIfNeeded() {
13185
- if (this.isMounted()) {
13252
+ async triggerInitialLoadIfNeeded(generation) {
13253
+ if (this.isMounted() || this.isOpStale(generation)) {
13186
13254
  return;
13187
13255
  }
13188
- const listInstance = this.listWidget()?.instance;
13189
- if (!listInstance?.call) {
13190
- return;
13256
+ const refreshed = await this.refreshWhenGridReady(generation);
13257
+ if (refreshed && !this.isOpStale(generation)) {
13258
+ this.isMounted.set(true);
13259
+ }
13260
+ }
13261
+ /**
13262
+ * Calls the unpatched data-list refresh until the grid is ready or the op is stale.
13263
+ */
13264
+ async refreshWhenGridReady(generation) {
13265
+ return this.refreshWhenGridReadyWith(generation, () => this.resolveDataListRefresh()?.() ?? false);
13266
+ }
13267
+ /**
13268
+ * Prefer the bound original refresh (pre-patch). Falls back to an unpatched instance refresh.
13269
+ */
13270
+ resolveDataListRefresh() {
13271
+ if (this.boundDataListRefresh) {
13272
+ return this.boundDataListRefresh;
13273
+ }
13274
+ const inst = this.listWidget()?.instance;
13275
+ if (!inst?.refresh || inst.__axpEntityListRefreshPatched) {
13276
+ return null;
13277
+ }
13278
+ return () => inst.refresh?.() ?? false;
13279
+ }
13280
+ async refreshWhenGridReadyWith(generation, refreshFn) {
13281
+ const maxAttempts = 60;
13282
+ const delayMs = 50;
13283
+ for (let i = 0; i < maxAttempts; i++) {
13284
+ if (this.isOpStale(generation)) {
13285
+ return false;
13286
+ }
13287
+ this.pushToolbarFiltersToDataSource();
13288
+ if (refreshFn()) {
13289
+ return true;
13290
+ }
13291
+ await this.delay(delayMs);
13191
13292
  }
13192
- this.pushToolbarFiltersToDataSource();
13193
- listInstance.call('refresh');
13194
- this.isMounted.set(true);
13293
+ return false;
13195
13294
  }
13196
13295
  /**
13197
13296
  * Re-evaluates related-entity list filters from the live dialog form context (e.g. after create saves the main row id).
@@ -13209,7 +13308,7 @@ class AXPEntityListWidgetViewComponent extends AXPValueWidgetComponent {
13209
13308
  value: await this.expressionEvaluator.evaluate(c.value, scope),
13210
13309
  hidden: true,
13211
13310
  })));
13212
- if (!this.fullPath()) {
13311
+ if (this.isViewDestroyed || !this.fullPath()) {
13213
13312
  return;
13214
13313
  }
13215
13314
  const current = this.getValue();
@@ -13241,14 +13340,18 @@ class AXPEntityListWidgetViewComponent extends AXPValueWidgetComponent {
13241
13340
  * Pushes toolbar filters once the embedded data-list exposes its data source
13242
13341
  * (listNode is created via deferred setTimeout in ngOnInit).
13243
13342
  */
13244
- async ensureToolbarFiltersOnDataSource() {
13343
+ async ensureToolbarFiltersOnDataSource(generation) {
13344
+ const opGeneration = generation ?? this.opGeneration;
13245
13345
  if (this.pushToolbarFiltersToDataSource()) {
13246
13346
  return true;
13247
13347
  }
13248
13348
  const maxAttempts = 40;
13249
13349
  const delayMs = 50;
13250
13350
  for (let i = 0; i < maxAttempts; i++) {
13251
- await new Promise((resolve) => setTimeout(resolve, delayMs));
13351
+ await this.delay(delayMs);
13352
+ if (this.isOpStale(opGeneration)) {
13353
+ return false;
13354
+ }
13252
13355
  if (this.pushToolbarFiltersToDataSource()) {
13253
13356
  return true;
13254
13357
  }
@@ -13259,8 +13362,12 @@ class AXPEntityListWidgetViewComponent extends AXPValueWidgetComponent {
13259
13362
  * Writes toolbar filters from specs and pushes them onto the data source so refresh/reload keeps the parent scope.
13260
13363
  */
13261
13364
  async applyRelatedFiltersFromContextAndDatasource(specs) {
13365
+ const generation = this.beginOp();
13262
13366
  await this.applyRelatedFiltersFromContext(specs);
13263
- await this.ensureToolbarFiltersOnDataSource();
13367
+ if (this.isOpStale(generation)) {
13368
+ return;
13369
+ }
13370
+ await this.ensureToolbarFiltersOnDataSource(generation);
13264
13371
  }
13265
13372
  /**
13266
13373
  * Refreshes the embedded data list (toolbar / workflow). In wizard mode, `refresh` is patched to re-apply scoped filters first.
@@ -13270,6 +13377,31 @@ class AXPEntityListWidgetViewComponent extends AXPValueWidgetComponent {
13270
13377
  }
13271
13378
  /** Patches data-list `refresh` so reload keeps parent-scoped toolbar filters on the data source. */
13272
13379
  #patchDataListRefreshEffect;
13380
+ /**
13381
+ * Re-applies parent-scoped filters then refreshes the grid, ignoring stale overlapping calls.
13382
+ */
13383
+ async runScopedDataListRefresh(originalRefresh) {
13384
+ const generation = this.beginOp();
13385
+ const o = this.options();
13386
+ const specs = o['relatedFilterConditionSpecs'];
13387
+ if (o['syncRelatedListFiltersFromDialogContext'] && specs?.length) {
13388
+ await this.applyRelatedFiltersFromContext(specs);
13389
+ if (this.isOpStale(generation)) {
13390
+ return;
13391
+ }
13392
+ await this.ensureToolbarFiltersOnDataSource(generation);
13393
+ }
13394
+ else {
13395
+ await this.reapplyScopedFiltersToDataSource({ triggerInitialLoad: false, generation });
13396
+ }
13397
+ if (this.isOpStale(generation)) {
13398
+ return;
13399
+ }
13400
+ const refreshed = await this.refreshWhenGridReadyWith(generation, originalRefresh);
13401
+ if (refreshed && !this.isOpStale(generation) && !this.isMounted()) {
13402
+ this.isMounted.set(true);
13403
+ }
13404
+ }
13273
13405
  #effect;
13274
13406
  /**
13275
13407
  * Validates that all required dependencies are available
@@ -13375,6 +13507,9 @@ class AXPEntityListWidgetViewComponent extends AXPValueWidgetComponent {
13375
13507
  }
13376
13508
  // Resolve entity and set it in the signal
13377
13509
  const resolvedEntity = await this.entityService.resolve(moduleName, entityName);
13510
+ if (this.isViewDestroyed) {
13511
+ return;
13512
+ }
13378
13513
  if (!resolvedEntity) {
13379
13514
  throw new Error(`Entity not found: ${this.entitySource()}`);
13380
13515
  }
@@ -13387,8 +13522,18 @@ class AXPEntityListWidgetViewComponent extends AXPValueWidgetComponent {
13387
13522
  excludeProperties: this.options()['excludeProperties'],
13388
13523
  };
13389
13524
  const listOptions = await this.entityListTableService.convertEntityToListOptions(resolvedEntity, options, this.allActions());
13525
+ if (this.isViewDestroyed) {
13526
+ return;
13527
+ }
13390
13528
  const toolbarOptions = await this.entityListToolbarService.convertEntityToolbarOptions(resolvedEntity, options);
13391
- setTimeout(() => {
13529
+ if (this.isViewDestroyed) {
13530
+ return;
13531
+ }
13532
+ this.initListTimeoutId = setTimeout(() => {
13533
+ this.initListTimeoutId = null;
13534
+ if (this.isViewDestroyed) {
13535
+ return;
13536
+ }
13392
13537
  this.listNode.set({
13393
13538
  type: AXPWidgetsCatalog.dataList,
13394
13539
  options: listOptions,
@@ -13410,7 +13555,9 @@ class AXPEntityListWidgetViewComponent extends AXPValueWidgetComponent {
13410
13555
  },
13411
13556
  });
13412
13557
  queueMicrotask(() => {
13413
- this.applyMergedRouteFiltersToList();
13558
+ if (!this.isViewDestroyed) {
13559
+ this.applyMergedRouteFiltersToList();
13560
+ }
13414
13561
  });
13415
13562
  }, 100);
13416
13563
  }
@@ -13435,6 +13582,9 @@ class AXPEntityListWidgetViewComponent extends AXPValueWidgetComponent {
13435
13582
  }
13436
13583
  });
13437
13584
  const listWidget = (await this.layoutService.waitForWidget(`${this.entitySource()}-tab-list_table`, 500));
13585
+ if (this.isViewDestroyed) {
13586
+ return;
13587
+ }
13438
13588
  if (listWidget?.api && typeof listWidget.api === 'function') {
13439
13589
  const onSelectionChange = listWidget.api()['onSelectionChange'];
13440
13590
  if (onSelectionChange) {
@@ -13445,6 +13595,13 @@ class AXPEntityListWidgetViewComponent extends AXPValueWidgetComponent {
13445
13595
  }
13446
13596
  }
13447
13597
  ngOnDestroy() {
13598
+ this.isViewDestroyed = true;
13599
+ this.beginOp();
13600
+ if (this.initListTimeoutId != null) {
13601
+ clearTimeout(this.initListTimeoutId);
13602
+ this.initListTimeoutId = null;
13603
+ }
13604
+ this.boundDataListRefresh = null;
13448
13605
  this.listWidget.set(undefined);
13449
13606
  this.destroyed.next();
13450
13607
  this.destroyed.complete();
@@ -23289,8 +23446,10 @@ class AXPPageListConverter extends AXPBaseRelatedEntityConverter {
23289
23446
  hidden: true,
23290
23447
  };
23291
23448
  }) ?? []);
23449
+ // Unique from host overview page id (`entity.name`) so self-referential related lists navigate correctly.
23450
+ const pageId = buildPageListWidgetName(relatedEntity.entity);
23292
23451
  return {
23293
- id: entityDef?.name ?? '',
23452
+ id: pageId,
23294
23453
  title: context.rootTitle ?? '',
23295
23454
  label: relatedEntity.title,
23296
23455
  icon: relatedEntity.icon || entityDef.icon,
@@ -24521,11 +24680,14 @@ class AXPLayoutAdapterFactory {
24521
24680
  composePagesWithPositions(mainPage, relatedPages, entity) {
24522
24681
  // Only consider related entities with layout types page-detail or page-list
24523
24682
  const pageEntities = (entity?.relatedEntities || []).filter((re) => !re.hidden && (re.layout?.type === 'page-detail' || re.layout?.type === 'page-list'));
24524
- // Build a map from entity name to its layout config (order/position)
24683
+ // Build a map from page id to its layout config (order/position).
24684
+ // page-list ids use `page-list-{module.entity}` so they never collide with the host overview page.
24525
24685
  const layoutConfigByEntity = {};
24526
24686
  for (const re of pageEntities) {
24527
24687
  const [, entityName] = (re.entity || '').split('.');
24528
- const key = entityName || re.entity;
24688
+ const key = re.layout?.type === 'page-list'
24689
+ ? buildPageListWidgetName(re.entity)
24690
+ : entityName || re.entity;
24529
24691
  layoutConfigByEntity[key] = { order: re.layout?.order, position: re.layout?.position };
24530
24692
  }
24531
24693
  // Nested pages from merge-detail (e.g. PartyContact from Party) have no layout config.