@acorex/platform 20.9.38 → 20.9.40

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';
@@ -24,7 +24,7 @@ import { AXButtonModule } from '@acorex/components/button';
24
24
  import * as i3 from '@acorex/components/skeleton';
25
25
  import { AXSkeletonModule } from '@acorex/components/skeleton';
26
26
  import * as i2 from '@acorex/platform/layout/widget-core';
27
- import { AXPWidgetCoreModule, AXPWidgetRegistryService, AXPColumnWidgetComponent, AXPValueWidgetComponent, AXPWidgetRendererDirective, AXPBaseWidgetComponent, WIDGET_CASCADE_TRACE_NS, AXP_WIDGETS_ADVANCE_SUB_MEDIA, AXP_WIDGETS_ADVANCE_CATEGORY, AXP_WIDGETS_EDITOR_CATEGORY, AXP_WIDGET_DEFINITION_PROVIDER } from '@acorex/platform/layout/widget-core';
27
+ import { AXPWidgetCoreModule, AXPWidgetRegistryService, AXPColumnWidgetComponent, AXPValueWidgetComponent, AXPWidgetRendererDirective, AXPBaseWidgetComponent, WIDGET_CASCADE_TRACE_NS, AXP_WIDGETS_EDITOR_CATEGORY, AXP_WIDGETS_ADVANCE_SUB_MEDIA, AXP_WIDGETS_ADVANCE_CATEGORY, AXP_WIDGET_DEFINITION_PROVIDER } from '@acorex/platform/layout/widget-core';
28
28
  import { AXPCommandService, AXPQueryService, AXPQueryExecutor, AXPCommandExecutor, provideCommandSetups, provideQuerySetups, AXPCommandRegistry, AXPQueryRegistry } from '@acorex/platform/runtime';
29
29
  import * as i5 from '@angular/common';
30
30
  import { CommonModule, DOCUMENT, AsyncPipe, NgClass } from '@angular/common';
@@ -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);
@@ -12144,6 +12183,8 @@ class AXPEntityDefinitionProviderWidgetEditComponent extends AXPProviderSelectWi
12144
12183
  super(...arguments);
12145
12184
  //#region ---- Services & Dependencies ----
12146
12185
  this.entityRegistry = inject(AXPEntityDefinitionRegistryService);
12186
+ this.entityResolver = inject(AXPEntityResolver);
12187
+ this.translationService = inject(AXTranslationService);
12147
12188
  /**
12148
12189
  * Last applied module filter (from context) to avoid redundant reloads.
12149
12190
  */
@@ -12182,18 +12223,26 @@ class AXPEntityDefinitionProviderWidgetEditComponent extends AXPProviderSelectWi
12182
12223
  * - `valueFormat`: `fullKey` (`module.name`, default) or `entityName` (short name only, for menus that store `entityListRef.entity`).
12183
12224
  */
12184
12225
  async loadDataSourceRows() {
12185
- const entities = this.entityRegistry.getAll();
12226
+ const entries = await this.entityResolver.listAll();
12186
12227
  const filterModuleField = this.options()['filterModuleField'];
12187
12228
  const valueFormat = this.options()['valueFormat'] ?? 'fullKey';
12188
- let list = entities;
12229
+ let list = entries;
12189
12230
  if (filterModuleField) {
12190
12231
  const mod = this.readModuleFromContext(filterModuleField);
12191
- list = mod ? entities.filter((e) => e.module === mod) : [];
12232
+ list = mod ? entries.filter((e) => e.module === mod) : [];
12192
12233
  }
12193
- return list.map((e) => ({
12194
- value: valueFormat === 'entityName' ? e.name : `${e.module}.${e.name}`,
12195
- title: `${e.title} (${e.module}.${e.name})`,
12196
- }));
12234
+ const locale = this.translationService.getActiveLang();
12235
+ const registeredByKey = new Map(this.entityRegistry.getAll().map((e) => [`${e.module}.${e.name}`, e]));
12236
+ return list.map((e) => {
12237
+ const cached = registeredByKey.get(e.fullName);
12238
+ const resolvedTitle = cached
12239
+ ? resolveSearchableText(cached.title, locale, this.translationService).trim() || e.name
12240
+ : e.name;
12241
+ return {
12242
+ value: valueFormat === 'entityName' ? e.name : e.fullName,
12243
+ title: `${resolvedTitle} (${e.fullName})`,
12244
+ };
12245
+ });
12197
12246
  }
12198
12247
  //#endregion
12199
12248
  //#region ---- Refresh (e.g. when sibling module field changes) ----
@@ -13131,6 +13180,7 @@ class AXPEntityListWidgetViewComponent extends AXPValueWidgetComponent {
13131
13180
  /**
13132
13181
  * Route `filters` apply only when the active details `page` matches this list's entity
13133
13182
  * (e.g. WorkOrder filters must not affect FailureRegister on the same asset layout).
13183
+ * page-list sidebar pages use `page-list-{module.entity}` as their page id.
13134
13184
  */
13135
13185
  shouldApplyRouteFilters() {
13136
13186
  const pageId = this.route.snapshot.queryParamMap.get('page');
@@ -13142,7 +13192,7 @@ class AXPEntityListWidgetViewComponent extends AXPValueWidgetComponent {
13142
13192
  return false;
13143
13193
  }
13144
13194
  const [, entityName] = entitySource.split('.');
13145
- return !!entityName && pageId === entityName;
13195
+ return ((!!entityName && pageId === entityName) || pageId === buildPageListWidgetName(entitySource));
13146
13196
  }
13147
13197
  /**
13148
13198
  * Merges route filters into related-entity toolbar filters (route wins per field).
@@ -17364,7 +17414,7 @@ const AXPLookupWidget = {
17364
17414
  name: 'lookup-editor',
17365
17415
  title: 'Lookup',
17366
17416
  categories: [],
17367
- groups: [AXPWidgetGroupEnum.EntityWidget],
17417
+ groups: [AXPWidgetGroupEnum.EntityWidget, AXPWidgetGroupEnum.FormElement],
17368
17418
  defaultFilterWidgetName: 'lookup-filter',
17369
17419
  type: 'editor',
17370
17420
  icon: 'fa-light fa-list-radio',
@@ -17395,7 +17445,25 @@ const AXPLookupWidget = {
17395
17445
  interface: {
17396
17446
  name: 'entity',
17397
17447
  path: 'options.entity',
17398
- type: AXPWidgetsCatalog.text,
17448
+ type: AXPWidgetsCatalog.entityDefinitionProvider,
17449
+ options: {
17450
+ hasClearButton: true,
17451
+ allowSearch: true,
17452
+ },
17453
+ },
17454
+ },
17455
+ visible: true,
17456
+ },
17457
+ {
17458
+ name: 'filter',
17459
+ title: 'Filters',
17460
+ group: AXP_DATA_PROPERTY_GROUP,
17461
+ schema: {
17462
+ dataType: 'object',
17463
+ interface: {
17464
+ name: 'filter',
17465
+ path: 'options.filter',
17466
+ type: 'lookup-filter-options',
17399
17467
  },
17400
17468
  },
17401
17469
  visible: true,
@@ -17533,6 +17601,250 @@ const AXPLookupWidget = {
17533
17601
  },
17534
17602
  };
17535
17603
 
17604
+ //#region ---- Imports ----
17605
+ //#endregion
17606
+ //#region ---- Widget Config ----
17607
+ const AXPLookupFilterOptionsWidget = {
17608
+ name: 'lookup-filter-options',
17609
+ title: 'Lookup Filter Options',
17610
+ description: 'Configure lookup entity filter conditions',
17611
+ icon: 'fa-light fa-filter',
17612
+ categories: AXP_WIDGETS_EDITOR_CATEGORY,
17613
+ groups: [AXPWidgetGroupEnum.EntityWidget],
17614
+ type: 'editor',
17615
+ properties: [AXP_NAME_PROPERTY, AXP_DATA_PATH_PROPERTY],
17616
+ components: {
17617
+ edit: {
17618
+ component: () => Promise.resolve().then(function () { return lookupFilterOptionsWidgetEditor_component; }).then((c) => c.AXPLookupFilterOptionsWidgetEditComponent),
17619
+ },
17620
+ },
17621
+ };
17622
+ //#endregion
17623
+
17624
+ //#endregion
17625
+ //#region ---- Constants ----
17626
+ const LOOKUP_FILTER_OPTIONS_I18N = {
17627
+ filters: '@general:widgets.data-source-options.filters',
17628
+ filtersButtonEmpty: '@general:widgets.data-source-options.filters-button-empty',
17629
+ filtersButtonWithCount: '@general:widgets.data-source-options.filters-button-with-count',
17630
+ filtersModalTitle: '@general:widgets.data-source-options.filters-modal-title',
17631
+ selectEntityFirst: '@general:widgets.lookup-filter-options.select-entity-first',
17632
+ noFilterableFields: '@general:widgets.lookup-filter-options.no-filterable-fields',
17633
+ };
17634
+ //#endregion
17635
+ //#region ---- Helpers ----
17636
+ function getFilterKey(filter, index) {
17637
+ return filter.id ?? filter.field ?? slugFilterTitle(filter.title ?? '') ?? `filter_${index}`;
17638
+ }
17639
+ function slugFilterTitle(title) {
17640
+ return (title
17641
+ .toLowerCase()
17642
+ .replace(/[^a-z0-9\s]/g, '')
17643
+ .replace(/\s+/g, '_')
17644
+ .substring(0, 50) || 'filter');
17645
+ }
17646
+ function flattenFilterLeaves(filter) {
17647
+ if (!filter) {
17648
+ return [];
17649
+ }
17650
+ if (filter.filters?.length) {
17651
+ return filter.filters.flatMap((child) => flattenFilterLeaves(child));
17652
+ }
17653
+ if (filter.field) {
17654
+ return [filter];
17655
+ }
17656
+ return [];
17657
+ }
17658
+ function filterToExpressionMap(filter, definitions) {
17659
+ const map = {};
17660
+ definitions.forEach((definition, index) => {
17661
+ map[getFilterKey(definition, index)] = '';
17662
+ });
17663
+ for (const leaf of flattenFilterLeaves(filter)) {
17664
+ const definitionIndex = definitions.findIndex((definition) => definition.field === leaf.field);
17665
+ if (definitionIndex < 0) {
17666
+ continue;
17667
+ }
17668
+ const key = getFilterKey(definitions[definitionIndex], definitionIndex);
17669
+ const value = leaf.value;
17670
+ map[key] = typeof value === 'string' ? value : value != null ? String(value) : '';
17671
+ }
17672
+ return map;
17673
+ }
17674
+ function expressionsToFilter(expressions, definitions) {
17675
+ const clauses = definitions
17676
+ .map((definition, index) => {
17677
+ const key = getFilterKey(definition, index);
17678
+ const expression = expressions[key]?.trim();
17679
+ if (!expression) {
17680
+ return null;
17681
+ }
17682
+ return {
17683
+ field: definition.field,
17684
+ operator: definition.operator ?? { type: 'equal' },
17685
+ value: expression,
17686
+ };
17687
+ })
17688
+ .filter((clause) => clause != null);
17689
+ if (!clauses.length) {
17690
+ return undefined;
17691
+ }
17692
+ return {
17693
+ logic: 'and',
17694
+ filters: clauses,
17695
+ };
17696
+ }
17697
+ function countConfiguredExpressions(filter) {
17698
+ return flattenFilterLeaves(filter).filter((leaf) => {
17699
+ const value = leaf.value;
17700
+ const text = typeof value === 'string' ? value : value != null ? String(value) : '';
17701
+ return text.trim().length > 0;
17702
+ }).length;
17703
+ }
17704
+ //#endregion
17705
+ //#region ---- Component ----
17706
+ class AXPLookupFilterOptionsWidgetEditComponent extends AXPValueWidgetComponent {
17707
+ constructor() {
17708
+ super(...arguments);
17709
+ //#region ---- Services & Dependencies ----
17710
+ this.entityRegistry = inject(AXPEntityDefinitionRegistryService);
17711
+ this.widgetRegistry = inject(AXPWidgetRegistryService);
17712
+ this.layoutBuilder = inject(AXPLayoutBuilderService);
17713
+ this.translationService = inject(AXTranslationService);
17714
+ //#endregion
17715
+ //#region ---- State ----
17716
+ this.I18N = LOOKUP_FILTER_OPTIONS_I18N;
17717
+ this.filterDefinitions = signal([], ...(ngDevMode ? [{ debugName: "filterDefinitions" }] : /* istanbul ignore next */ []));
17718
+ this.entityTitle = signal('', ...(ngDevMode ? [{ debugName: "entityTitle" }] : /* istanbul ignore next */ []));
17719
+ this.loadedEntityKey = signal(null, ...(ngDevMode ? [{ debugName: "loadedEntityKey" }] : /* istanbul ignore next */ []));
17720
+ //#endregion
17721
+ //#region ---- Computed ----
17722
+ this.entityKey = computed(() => {
17723
+ this.contextService.data();
17724
+ const key = this.contextService.getValue('options.entity');
17725
+ const trimmed = key?.trim();
17726
+ return trimmed && trimmed.length > 0 ? trimmed : null;
17727
+ }, ...(ngDevMode ? [{ debugName: "entityKey" }] : /* istanbul ignore next */ []));
17728
+ this.filterCount = computed(() => countConfiguredExpressions(this.getValue()), ...(ngDevMode ? [{ debugName: "filterCount" }] : /* istanbul ignore next */ []));
17729
+ //#endregion
17730
+ //#region ---- Effects ----
17731
+ this.loadFilterDefinitionsEffect = effect(() => {
17732
+ const key = this.entityKey();
17733
+ untracked(() => {
17734
+ void this.loadFilterDefinitions(key);
17735
+ });
17736
+ }, ...(ngDevMode ? [{ debugName: "loadFilterDefinitionsEffect" }] : /* istanbul ignore next */ []));
17737
+ }
17738
+ //#endregion
17739
+ //#region ---- UI Handlers ----
17740
+ async openFilterEditor() {
17741
+ const definitions = this.filterDefinitions();
17742
+ if (!definitions.length) {
17743
+ return;
17744
+ }
17745
+ const initialFilters = filterToExpressionMap(this.getValue(), definitions);
17746
+ const filtersModalTitle = await this.translationService.translateAsync(LOOKUP_FILTER_OPTIONS_I18N.filtersModalTitle);
17747
+ const ref = await this.layoutBuilder
17748
+ .create()
17749
+ .dialog((dialog) => {
17750
+ dialog
17751
+ .setTitle(filtersModalTitle)
17752
+ .setMessage(this.entityTitle())
17753
+ .setSize('md')
17754
+ .setContext({ filters: initialFilters })
17755
+ .content((layoutBuilder) => {
17756
+ layoutBuilder.flex((flex) => {
17757
+ flex
17758
+ .mode('edit')
17759
+ .setOptions({
17760
+ flexDirection: 'column',
17761
+ maxHeight: '60vh',
17762
+ overflowY: 'auto',
17763
+ });
17764
+ definitions.forEach((filter, index) => {
17765
+ const key = getFilterKey(filter, index);
17766
+ flex.formField(filter.title, (field) => {
17767
+ field.path(`filters.${key}`).customWidget('code-editor', {
17768
+ language: 'javascript',
17769
+ lineNumbers: true,
17770
+ height: 'min-h-48',
17771
+ });
17772
+ });
17773
+ });
17774
+ });
17775
+ })
17776
+ .setActions((actions) => {
17777
+ actions.cancel('@general:actions.cancel.title');
17778
+ actions.submit('@general:actions.save.title');
17779
+ });
17780
+ })
17781
+ .show();
17782
+ if (ref.action() === 'submit') {
17783
+ const context = ref.context();
17784
+ const expressions = context?.filters ?? {};
17785
+ this.setValue(expressionsToFilter(expressions, definitions));
17786
+ ref.close();
17787
+ return;
17788
+ }
17789
+ ref.close();
17790
+ }
17791
+ //#endregion
17792
+ //#region ---- Private Methods ----
17793
+ async loadFilterDefinitions(entityKey) {
17794
+ if (!entityKey) {
17795
+ this.filterDefinitions.set([]);
17796
+ this.entityTitle.set('');
17797
+ if (this.loadedEntityKey()) {
17798
+ this.setValue(undefined);
17799
+ }
17800
+ this.loadedEntityKey.set(null);
17801
+ return;
17802
+ }
17803
+ const [module, entity] = entityKey.split('.');
17804
+ if (!module || !entity) {
17805
+ this.filterDefinitions.set([]);
17806
+ this.entityTitle.set('');
17807
+ if (this.loadedEntityKey()) {
17808
+ this.setValue(undefined);
17809
+ }
17810
+ this.loadedEntityKey.set(null);
17811
+ return;
17812
+ }
17813
+ const previousKey = this.loadedEntityKey();
17814
+ if (previousKey && previousKey !== entityKey) {
17815
+ this.setValue(undefined);
17816
+ }
17817
+ const entityDef = await this.entityRegistry.resolve(module, entity).catch(() => null);
17818
+ if (!entityDef) {
17819
+ this.filterDefinitions.set([]);
17820
+ this.entityTitle.set('');
17821
+ this.loadedEntityKey.set(null);
17822
+ return;
17823
+ }
17824
+ this.entityTitle.set(typeof entityDef.title === 'string' ? entityDef.title : entityKey);
17825
+ const definitions = await collectEntityListFilterDefinitions(entityDef, (moduleName, entityName) => this.entityRegistry.resolve(moduleName, entityName).catch(() => null), {
17826
+ resolveWidget: (type) => this.widgetRegistry.resolve(type),
17827
+ });
17828
+ this.filterDefinitions.set(definitions);
17829
+ this.loadedEntityKey.set(entityKey);
17830
+ const current = this.getValue();
17831
+ if (current && !isEmpty(current) && countConfiguredExpressions(current) === 0) {
17832
+ this.setValue(undefined);
17833
+ }
17834
+ }
17835
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPLookupFilterOptionsWidgetEditComponent, deps: null, target: i0.ɵɵFactoryTarget.Component }); }
17836
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: AXPLookupFilterOptionsWidgetEditComponent, isStandalone: true, selector: "axp-lookup-filter-options-widget-editor", usesInheritance: true, ngImport: i0, template: "<div class=\"axp-lookup-filter-options ax-flex ax-flex-col ax-gap-2 ax-py-2\">\n @if (!entityKey()) {\n <span class=\"ax-text-sm ax-opacity-70\">{{ I18N.selectEntityFirst | translate | async }}</span>\n } @else if (filterDefinitions().length === 0) {\n <span class=\"ax-text-sm ax-opacity-70\">{{ I18N.noFilterableFields | translate | async }}</span>\n } @else {\n @if (filterCount() === 0) {\n <ax-button\n look=\"twotone\"\n color=\"default\"\n size=\"sm\"\n [text]=\"(I18N.filtersButtonEmpty | translate | async) ?? ''\"\n (onClick)=\"openFilterEditor()\"\n class=\"ax-w-full\"\n >\n <ax-prefix>\n <ax-icon icon=\"fa-light fa-filter\"></ax-icon>\n </ax-prefix>\n </ax-button>\n } @else {\n <ax-button\n look=\"twotone\"\n color=\"default\"\n size=\"sm\"\n [text]=\"((I18N.filtersButtonWithCount | translate: { params: { count: filterCount() } }) | async) ?? ''\"\n (onClick)=\"openFilterEditor()\"\n class=\"ax-w-full\"\n >\n <ax-prefix>\n <ax-icon icon=\"fa-light fa-filter\"></ax-icon>\n </ax-prefix>\n </ax-button>\n }\n }\n</div>\n", styles: [".axp-lookup-filter-options{min-width:0}\n"], dependencies: [{ kind: "ngmodule", type: AXButtonModule }, { kind: "component", type: i1.AXButtonComponent, selector: "ax-button", inputs: ["disabled", "size", "tabIndex", "color", "look", "text", "toggleable", "selected", "iconOnly", "type", "loadingText"], outputs: ["onBlur", "onFocus", "onClick", "selectedChange", "toggleableChange", "lookChange", "colorChange", "disabledChange", "loadingTextChange"] }, { kind: "ngmodule", type: AXDecoratorModule }, { kind: "component", type: i3$2.AXDecoratorIconComponent, selector: "ax-icon", inputs: ["icon"] }, { kind: "component", type: i3$2.AXDecoratorGenericComponent, selector: "ax-footer, ax-header, ax-content, ax-divider, ax-form-hint, ax-prefix, ax-suffix, ax-text, ax-title, ax-subtitle, ax-placeholder, ax-overlay" }, { kind: "ngmodule", type: AXTranslationModule }, { kind: "pipe", type: AsyncPipe, name: "async" }, { kind: "pipe", type: i3$1.AXTranslatorPipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
17837
+ }
17838
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPLookupFilterOptionsWidgetEditComponent, decorators: [{
17839
+ type: Component,
17840
+ args: [{ selector: 'axp-lookup-filter-options-widget-editor', changeDetection: ChangeDetectionStrategy.OnPush, imports: [AsyncPipe, AXButtonModule, AXDecoratorModule, AXTranslationModule], template: "<div class=\"axp-lookup-filter-options ax-flex ax-flex-col ax-gap-2 ax-py-2\">\n @if (!entityKey()) {\n <span class=\"ax-text-sm ax-opacity-70\">{{ I18N.selectEntityFirst | translate | async }}</span>\n } @else if (filterDefinitions().length === 0) {\n <span class=\"ax-text-sm ax-opacity-70\">{{ I18N.noFilterableFields | translate | async }}</span>\n } @else {\n @if (filterCount() === 0) {\n <ax-button\n look=\"twotone\"\n color=\"default\"\n size=\"sm\"\n [text]=\"(I18N.filtersButtonEmpty | translate | async) ?? ''\"\n (onClick)=\"openFilterEditor()\"\n class=\"ax-w-full\"\n >\n <ax-prefix>\n <ax-icon icon=\"fa-light fa-filter\"></ax-icon>\n </ax-prefix>\n </ax-button>\n } @else {\n <ax-button\n look=\"twotone\"\n color=\"default\"\n size=\"sm\"\n [text]=\"((I18N.filtersButtonWithCount | translate: { params: { count: filterCount() } }) | async) ?? ''\"\n (onClick)=\"openFilterEditor()\"\n class=\"ax-w-full\"\n >\n <ax-prefix>\n <ax-icon icon=\"fa-light fa-filter\"></ax-icon>\n </ax-prefix>\n </ax-button>\n }\n }\n</div>\n", styles: [".axp-lookup-filter-options{min-width:0}\n"] }]
17841
+ }] });
17842
+
17843
+ var lookupFilterOptionsWidgetEditor_component = /*#__PURE__*/Object.freeze({
17844
+ __proto__: null,
17845
+ AXPLookupFilterOptionsWidgetEditComponent: AXPLookupFilterOptionsWidgetEditComponent
17846
+ });
17847
+
17536
17848
  /**
17537
17849
  * Source type
17538
17850
  */
@@ -22486,6 +22798,7 @@ const AXPSelectorStructureWidget = {
22486
22798
  //#region ---- Entity Widgets Provider ----
22487
22799
  const ENTITY_WIDGETS = [
22488
22800
  AXPLookupWidget,
22801
+ AXPLookupFilterOptionsWidget,
22489
22802
  AXPSelectorStructureWidget,
22490
22803
  AXPEntityListWidget,
22491
22804
  AXPPageComponentWidget,
@@ -23406,8 +23719,10 @@ class AXPPageListConverter extends AXPBaseRelatedEntityConverter {
23406
23719
  hidden: true,
23407
23720
  };
23408
23721
  }) ?? []);
23722
+ // Unique from host overview page id (`entity.name`) so self-referential related lists navigate correctly.
23723
+ const pageId = buildPageListWidgetName(relatedEntity.entity);
23409
23724
  return {
23410
- id: entityDef?.name ?? '',
23725
+ id: pageId,
23411
23726
  title: context.rootTitle ?? '',
23412
23727
  label: relatedEntity.title,
23413
23728
  icon: relatedEntity.icon || entityDef.icon,
@@ -24638,11 +24953,14 @@ class AXPLayoutAdapterFactory {
24638
24953
  composePagesWithPositions(mainPage, relatedPages, entity) {
24639
24954
  // Only consider related entities with layout types page-detail or page-list
24640
24955
  const pageEntities = (entity?.relatedEntities || []).filter((re) => !re.hidden && (re.layout?.type === 'page-detail' || re.layout?.type === 'page-list'));
24641
- // Build a map from entity name to its layout config (order/position)
24956
+ // Build a map from page id to its layout config (order/position).
24957
+ // page-list ids use `page-list-{module.entity}` so they never collide with the host overview page.
24642
24958
  const layoutConfigByEntity = {};
24643
24959
  for (const re of pageEntities) {
24644
24960
  const [, entityName] = (re.entity || '').split('.');
24645
- const key = entityName || re.entity;
24961
+ const key = re.layout?.type === 'page-list'
24962
+ ? buildPageListWidgetName(re.entity)
24963
+ : entityName || re.entity;
24646
24964
  layoutConfigByEntity[key] = { order: re.layout?.order, position: re.layout?.position };
24647
24965
  }
24648
24966
  // Nested pages from merge-detail (e.g. PartyContact from Party) have no layout config.