@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.
- package/fesm2022/acorex-platform-layout-entity-contracts.mjs +3 -2
- package/fesm2022/acorex-platform-layout-entity-contracts.mjs.map +1 -1
- package/fesm2022/acorex-platform-layout-entity.mjs +215 -53
- package/fesm2022/acorex-platform-layout-entity.mjs.map +1 -1
- package/fesm2022/acorex-platform-layout-widgets.mjs +75 -9
- package/fesm2022/acorex-platform-layout-widgets.mjs.map +1 -1
- package/fesm2022/acorex-platform-themes-default.mjs +1 -1
- package/fesm2022/acorex-platform-themes-default.mjs.map +1 -1
- package/package.json +1 -1
- package/types/acorex-platform-layout-entity-contracts.d.ts +12 -3
- package/types/acorex-platform-layout-entity.d.ts +40 -4
- package/types/acorex-platform-layout-widgets.d.ts +13 -1
|
@@ -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,
|
|
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
|
-
...
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
6648
|
-
|
|
6649
|
-
|
|
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
|
-
|
|
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 (
|
|
12935
|
-
|
|
12936
|
-
|
|
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
|
-
/**
|
|
13166
|
-
|
|
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
|
-
|
|
13176
|
-
|
|
13177
|
-
|
|
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
|
|
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
|
|
13189
|
-
if (!
|
|
13190
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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:
|
|
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
|
|
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 =
|
|
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.
|