@acorex/platform 20.7.20 → 20.7.21

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.
@@ -380,7 +380,6 @@ class AXPEntityMiddleware {
380
380
  }
381
381
  // Apply all matching modifiers in order
382
382
  for (const modifier of modifiers) {
383
- //TODO Check side EFFECTS
384
383
  runInInjectionContext(this.injector, () => modifier(context));
385
384
  }
386
385
  return context.toEntity();
@@ -4532,8 +4531,82 @@ const AXPEntityDetailViewModelResolver = (route, state, service = inject(AXPEnti
4532
4531
  return service.create(moduleName, entityName, id);
4533
4532
  };
4534
4533
 
4534
+ class AXPDetailsViewBadgeStatusService {
4535
+ constructor() {
4536
+ this.entityRegistry = inject(AXPEntityDefinitionRegistryService);
4537
+ this.translateService = inject(AXTranslationService);
4538
+ }
4539
+ async getPageBadge(entityName, _context, isDirty) {
4540
+ if (!isDirty) {
4541
+ return null;
4542
+ }
4543
+ return {
4544
+ title: await this.translateService.translateAsync('@general:terms.interface.save-status.not-saved.title'),
4545
+ color: 'warning',
4546
+ description: await this.translateService.translateAsync('@general:terms.interface.save-status.not-saved.description'),
4547
+ };
4548
+ }
4549
+ async getPageStatus(entityName, context, currentPage) {
4550
+ if (!entityName) {
4551
+ return null;
4552
+ }
4553
+ const [moduleName, entityNamePart] = entityName.split('.');
4554
+ if (!moduleName || !entityNamePart) {
4555
+ return null;
4556
+ }
4557
+ try {
4558
+ const entityDefinition = await this.entityRegistry.resolve(moduleName, entityNamePart);
4559
+ if (!entityDefinition) {
4560
+ return null;
4561
+ }
4562
+ const statusPlugin = entityDefinition.plugins?.find((p) => p.name === 'status');
4563
+ if (!statusPlugin?.options) {
4564
+ return null;
4565
+ }
4566
+ const statusOptions = statusPlugin.options;
4567
+ const statusField = statusOptions.field ?? 'statusId';
4568
+ const statusDefinitionKey = statusOptions.definition;
4569
+ let definitionKey = typeof statusDefinitionKey === 'string' ? statusDefinitionKey : undefined;
4570
+ if (!definitionKey) {
4571
+ const statusProperty = entityDefinition.properties?.find((p) => p.name === statusField);
4572
+ const widgetOptions = statusProperty?.schema?.interface?.options;
4573
+ if (widgetOptions?.definitionKey) {
4574
+ definitionKey = widgetOptions.definitionKey;
4575
+ }
4576
+ }
4577
+ if (!definitionKey) {
4578
+ return null;
4579
+ }
4580
+ const statusValue = context?.[statusField];
4581
+ if (statusValue == null) {
4582
+ return null;
4583
+ }
4584
+ const value = typeof statusValue === 'string' ? statusValue : statusValue?.name ?? statusValue;
4585
+ return {
4586
+ definitionKey,
4587
+ value: String(value),
4588
+ dataPath: statusField,
4589
+ readonly: currentPage?.isReadonly ?? false,
4590
+ };
4591
+ }
4592
+ catch (error) {
4593
+ console.warn('Failed to get page status:', error);
4594
+ return null;
4595
+ }
4596
+ }
4597
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: AXPDetailsViewBadgeStatusService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
4598
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: AXPDetailsViewBadgeStatusService, providedIn: 'root' }); }
4599
+ }
4600
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: AXPDetailsViewBadgeStatusService, decorators: [{
4601
+ type: Injectable,
4602
+ args: [{
4603
+ providedIn: 'root',
4604
+ }]
4605
+ }] });
4606
+
4535
4607
  class AXPLayoutAdapterBuilder {
4536
4608
  constructor() {
4609
+ this.badgeStatusService = inject(AXPDetailsViewBadgeStatusService);
4537
4610
  this.adapter = {};
4538
4611
  }
4539
4612
  setEntity(entity, rootContext) {
@@ -4562,8 +4635,9 @@ class AXPLayoutAdapterBuilder {
4562
4635
  return this;
4563
4636
  }
4564
4637
  build() {
4638
+ const name = `${this.entity?.module}.${this.entity?.name}`;
4565
4639
  return {
4566
- name: `${this.entity?.module}.${this.entity?.name}`,
4640
+ name,
4567
4641
  title: `${this.entity?.interfaces?.master?.single?.title}`,
4568
4642
  label: this.entity?.formats.plural,
4569
4643
  actions: [],
@@ -4572,6 +4646,8 @@ class AXPLayoutAdapterBuilder {
4572
4646
  load: this.createLoadFunction(),
4573
4647
  pages: this.adapter.pages || [],
4574
4648
  exitUrl: this.adapter.exitUrl,
4649
+ getPageBadge: (context, isDirty) => this.badgeStatusService.getPageBadge(name, context, isDirty),
4650
+ getPageStatus: (context, currentPage) => this.badgeStatusService.getPageStatus(name, context, currentPage),
4575
4651
  };
4576
4652
  }
4577
4653
  createBreadcrumbs() {
@@ -5989,11 +6065,14 @@ class AXPLayoutAdapterFactory {
5989
6065
  throw new Error(`Entity ${moduleName}.${entityName} not found`);
5990
6066
  }
5991
6067
  const rootContext = await this.loadRootContext(entity, id);
5992
- // Build main and related pages
5993
- const mainPage = await this.buildMainPage(entity, rootContext, id, dependencies);
5994
- const relatedPages = await this.buildRelatedPages(entity, rootContext, dependencies);
6068
+ // Evaluate hidden expressions for related entities once, reuse across building and composing
6069
+ const evaluatedRelatedEntities = await this.evaluateRelatedEntitiesHidden(entity?.relatedEntities ?? [], rootContext, dependencies);
6070
+ // Build main and related pages using evaluated related entities
6071
+ const entityWithEvaluatedRelated = { ...entity, relatedEntities: evaluatedRelatedEntities };
6072
+ const mainPage = await this.buildMainPage(entityWithEvaluatedRelated, rootContext, id, dependencies);
6073
+ const relatedPages = await this.buildRelatedPages(entityWithEvaluatedRelated, rootContext, dependencies);
5995
6074
  // Compose ordered pages around the primary page
5996
- const orderedPages = this.composePagesWithPositions(mainPage, relatedPages, entity);
6075
+ const orderedPages = this.composePagesWithPositions(mainPage, relatedPages, entityWithEvaluatedRelated);
5997
6076
  const applicationName = dependencies.session.application?.name ?? 'default';
5998
6077
  return this.layoutAdapterBuilder
5999
6078
  .setEntity(entity, rootContext)
@@ -6009,11 +6088,40 @@ class AXPLayoutAdapterFactory {
6009
6088
  async buildMainPage(entity, rootContext, id, dependencies) {
6010
6089
  return this.mainEntityContentBuilder.build(entity, rootContext, { ...dependencies, reloadRootContext: () => this.loadRootContext(entity, id) }, await this.getRootTitle(entity, rootContext, dependencies));
6011
6090
  }
6091
+ /**
6092
+ * Evaluates the 'hidden' expression for each related entity using the expression evaluator.
6093
+ * Returns a new array of related entities with the evaluated hidden values.
6094
+ */
6095
+ async evaluateRelatedEntitiesHidden(relatedEntities, rootContext, dependencies) {
6096
+ if (!relatedEntities?.length || !dependencies?.expressionEvaluator) {
6097
+ return relatedEntities ?? [];
6098
+ }
6099
+ return Promise.all(relatedEntities.map(async (re) => {
6100
+ let hidden = re.hidden;
6101
+ if (hidden && typeof hidden === 'string') {
6102
+ try {
6103
+ const scope = {
6104
+ context: {
6105
+ eval: (path) => get(rootContext, path),
6106
+ },
6107
+ };
6108
+ const result = await dependencies.expressionEvaluator.evaluate({ hidden }, scope);
6109
+ hidden = result.hidden;
6110
+ }
6111
+ catch {
6112
+ // Keep original hidden value if evaluation fails
6113
+ }
6114
+ }
6115
+ return { ...re, hidden };
6116
+ }));
6117
+ }
6012
6118
  async buildRelatedPages(entity, rootContext, dependencies) {
6013
6119
  const pages = [];
6014
6120
  const rootTitle = await this.getRootTitle(entity, rootContext, dependencies);
6121
+ // Evaluate hidden expressions before filtering related entities
6122
+ const evaluatedRelatedEntities = await this.evaluateRelatedEntitiesHidden(entity?.relatedEntities ?? [], rootContext, dependencies);
6015
6123
  // Page Details
6016
- const pageDetailEntities = entity?.relatedEntities?.filter((re) => !re.hidden && re.layout?.type === 'page-detail');
6124
+ const pageDetailEntities = evaluatedRelatedEntities.filter((re) => !re.hidden && re.layout?.type === 'page-detail');
6017
6125
  for (const relatedEntity of pageDetailEntities || []) {
6018
6126
  const converter = this.relatedEntityConverterFactory.createPageDetailsConverter();
6019
6127
  pages.push(await converter.convert(relatedEntity, {
@@ -6023,7 +6131,7 @@ class AXPLayoutAdapterFactory {
6023
6131
  }));
6024
6132
  }
6025
6133
  // Page Lists
6026
- const pageListEntities = entity?.relatedEntities?.filter((re) => !re.hidden && re.layout?.type === 'page-list');
6134
+ const pageListEntities = evaluatedRelatedEntities.filter((re) => !re.hidden && re.layout?.type === 'page-list');
6027
6135
  for (const relatedEntity of pageListEntities || []) {
6028
6136
  const converter = this.relatedEntityConverterFactory.createPageListConverter();
6029
6137
  pages.push(await converter.convert(relatedEntity, {
@@ -6034,7 +6142,7 @@ class AXPLayoutAdapterFactory {
6034
6142
  }));
6035
6143
  }
6036
6144
  // Include nested page-related entities from merge-detail related entities
6037
- const mergeDetailEntities = entity?.relatedEntities?.filter((re) => !re.hidden && re.layout?.type === 'merge-detail');
6145
+ const mergeDetailEntities = evaluatedRelatedEntities.filter((re) => !re.hidden && re.layout?.type === 'merge-detail');
6038
6146
  for (const mergeRelated of mergeDetailEntities || []) {
6039
6147
  try {
6040
6148
  const [moduleName, childEntityName] = (mergeRelated.entity || '').split('.');
@@ -6044,7 +6152,9 @@ class AXPLayoutAdapterFactory {
6044
6152
  }
6045
6153
  const nestedDataPath = mergeRelated?.persistence?.dataPath || childEntityName;
6046
6154
  const nestedContext = get(rootContext, nestedDataPath) ?? rootContext;
6047
- const nestedPageDetailEntities = childEntity?.relatedEntities?.filter((re) => !re.hidden && re.layout?.type === 'page-detail');
6155
+ // Evaluate hidden expressions for nested related entities
6156
+ const evaluatedNestedEntities = await this.evaluateRelatedEntitiesHidden(childEntity?.relatedEntities ?? [], nestedContext, dependencies);
6157
+ const nestedPageDetailEntities = evaluatedNestedEntities.filter((re) => !re.hidden && re.layout?.type === 'page-detail');
6048
6158
  for (const nestedRelated of nestedPageDetailEntities || []) {
6049
6159
  const converter = this.relatedEntityConverterFactory.createPageDetailsConverter();
6050
6160
  pages.push(await converter.convert(nestedRelated, {
@@ -6053,7 +6163,7 @@ class AXPLayoutAdapterFactory {
6053
6163
  rootTitle: rootTitle,
6054
6164
  }));
6055
6165
  }
6056
- const nestedPageListEntities = childEntity?.relatedEntities?.filter((re) => !re.hidden && re.layout?.type === 'page-list');
6166
+ const nestedPageListEntities = evaluatedNestedEntities.filter((re) => !re.hidden && re.layout?.type === 'page-list');
6057
6167
  for (const nestedRelated of nestedPageListEntities || []) {
6058
6168
  const converter = this.relatedEntityConverterFactory.createPageListConverter();
6059
6169
  pages.push(await converter.convert(nestedRelated, {
@@ -6219,6 +6329,47 @@ const columnWidthMiddlewareProvider = {
6219
6329
  };
6220
6330
  //#endregion
6221
6331
 
6332
+ const DEFAULT_APPLY_ORDERING = true;
6333
+ /**
6334
+ * Provides synchronous access to the ApplyLayoutOrdering setting.
6335
+ * Used by layout ordering middleware to avoid async in the modifier and prevent startup deadlocks.
6336
+ * Value is updated on onLoaded/onChanged; first read can trigger a one-time background get (no await).
6337
+ */
6338
+ class AXPLayoutOrderingConfigService {
6339
+ constructor() {
6340
+ this.settingsService = inject(AXPSettingsService);
6341
+ this._applyOrdering = signal(DEFAULT_APPLY_ORDERING, ...(ngDevMode ? [{ debugName: "_applyOrdering" }] : []));
6342
+ this.syncScheduled = false;
6343
+ this.settingsService.onLoaded.subscribe(() => this.syncFromSettings());
6344
+ this.settingsService.onChanged.subscribe(() => this.syncFromSettings());
6345
+ }
6346
+ getApplyOrdering() {
6347
+ if (!this.syncScheduled) {
6348
+ this.syncScheduled = true;
6349
+ this.settingsService
6350
+ .get(AXPCommonSettings.ApplyLayoutOrdering)
6351
+ .then((value) => this._applyOrdering.set(value ?? DEFAULT_APPLY_ORDERING))
6352
+ .catch(() => this._applyOrdering.set(DEFAULT_APPLY_ORDERING));
6353
+ }
6354
+ return this._applyOrdering();
6355
+ }
6356
+ async syncFromSettings() {
6357
+ try {
6358
+ const value = await this.settingsService.get(AXPCommonSettings.ApplyLayoutOrdering);
6359
+ this._applyOrdering.set(value ?? DEFAULT_APPLY_ORDERING);
6360
+ }
6361
+ catch {
6362
+ this._applyOrdering.set(DEFAULT_APPLY_ORDERING);
6363
+ }
6364
+ }
6365
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: AXPLayoutOrderingConfigService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
6366
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: AXPLayoutOrderingConfigService, providedIn: 'root' }); }
6367
+ }
6368
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: AXPLayoutOrderingConfigService, decorators: [{
6369
+ type: Injectable,
6370
+ args: [{ providedIn: 'root' }]
6371
+ }], ctorParameters: () => [] });
6372
+
6222
6373
  /**
6223
6374
  * Default order for common sections
6224
6375
  */
@@ -6289,6 +6440,10 @@ const getPropertyOrder = (sectionId, propertyName, orderConfig) => {
6289
6440
  */
6290
6441
  const layoutOrderingMiddlewareFactory = (config) => {
6291
6442
  return (context) => {
6443
+ const configService = inject(AXPLayoutOrderingConfigService);
6444
+ if (!configService.getApplyOrdering()) {
6445
+ return;
6446
+ }
6292
6447
  const { sections: sectionOrder, properties: propertyOrder } = config;
6293
6448
  //#region ---- Order Sections ----
6294
6449
  const orderSections = (layout) => {
@@ -6438,7 +6593,9 @@ const AXPCrudModifier = {
6438
6593
  if (!queries?.byKey) {
6439
6594
  queries.byKey = {
6440
6595
  execute: async (id) => {
6441
- return await dataService.getOne(id);
6596
+ const data = await dataService.getOne(id);
6597
+ // debugger;
6598
+ return data;
6442
6599
  },
6443
6600
  type: AXPEntityQueryType.Single,
6444
6601
  };
@@ -7683,7 +7840,7 @@ class AXPEntityCategoryTreeSelectorComponent extends AXBasePageComponent {
7683
7840
  await this.selectAllLeafDescendants(nodeId);
7684
7841
  }
7685
7842
  else {
7686
- // childrenCount is undefined - need to check if it has children
7843
+ // childrenCount is undefined - fetch once and reuse to avoid double datasource call
7687
7844
  const childNodes = await this.datasource(nodeId);
7688
7845
  if (!childNodes || childNodes.length === 0) {
7689
7846
  // No children - this is a leaf node, add it
@@ -7692,8 +7849,8 @@ class AXPEntityCategoryTreeSelectorComponent extends AXBasePageComponent {
7692
7849
  this.selectedNodeIds.set(Array.from(currentSelected));
7693
7850
  }
7694
7851
  else {
7695
- // Has children - only add leaf descendants
7696
- await this.selectAllLeafDescendants(nodeId);
7852
+ // Has children - pass prefetched childNodes to avoid datasource(nodeId) again
7853
+ await this.selectAllLeafDescendants(nodeId, childNodes);
7697
7854
  }
7698
7855
  }
7699
7856
  }
@@ -7759,16 +7916,17 @@ class AXPEntityCategoryTreeSelectorComponent extends AXBasePageComponent {
7759
7916
  /**
7760
7917
  * Expands parent nodes, collects all LEAF descendants, and selects them visually.
7761
7918
  * If the parent itself is a leaf (no children), it will be added.
7919
+ * When prefetchedChildren is provided, avoids an extra datasource(parentId) call.
7762
7920
  */
7763
- async selectAllLeafDescendants(parentId) {
7921
+ async selectAllLeafDescendants(parentId, prefetchedChildren) {
7764
7922
  if (!this.treeData || !this.treeConfig) {
7765
7923
  return;
7766
7924
  }
7767
7925
  const treeComponent = this.tree();
7768
7926
  try {
7769
7927
  const leafIds = new Set();
7770
- // Expand and collect leaf nodes simultaneously
7771
- await this.collectLeafNodes(parentId, leafIds, treeComponent);
7928
+ // Expand and collect leaf nodes; pass prefetchedChildren to avoid duplicate API call
7929
+ await this.collectLeafNodes(parentId, leafIds, treeComponent, prefetchedChildren);
7772
7930
  // Also check if the parent itself is a leaf (has no children)
7773
7931
  if (parentId !== 'all') {
7774
7932
  const isLeaf = await this.isLeafNodeCheck(parentId);
@@ -7843,14 +8001,19 @@ class AXPEntityCategoryTreeSelectorComponent extends AXBasePageComponent {
7843
8001
  /**
7844
8002
  * Recursively expands parent nodes and collects LEAF node IDs.
7845
8003
  * When treeComponent is provided, expands each parent node before loading children.
8004
+ * When prefetchedChildren is provided, uses it instead of calling datasource(parentId) (one less API call).
8005
+ * Fetches each node's children at most once by passing grandchildren when recursing.
7846
8006
  */
7847
- async collectLeafNodes(parentId, collection, treeComponent) {
8007
+ async collectLeafNodes(parentId, collection, treeComponent, prefetchedChildren) {
7848
8008
  if (!this.treeData || !this.treeConfig) {
7849
8009
  return;
7850
8010
  }
7851
8011
  try {
7852
8012
  let childNodes;
7853
- if (parentId === 'all') {
8013
+ if (prefetchedChildren !== undefined) {
8014
+ childNodes = prefetchedChildren;
8015
+ }
8016
+ else if (parentId === 'all') {
7854
8017
  // Expand 'all' node if tree component provided
7855
8018
  if (treeComponent) {
7856
8019
  try {
@@ -7860,7 +8023,7 @@ class AXPEntityCategoryTreeSelectorComponent extends AXBasePageComponent {
7860
8023
  // Ignore expand errors
7861
8024
  }
7862
8025
  }
7863
- // Get root node's children
8026
+ // Get root node's children (single datasource call)
7864
8027
  const rootNodes = await this.datasource();
7865
8028
  if (!rootNodes || rootNodes.length === 0) {
7866
8029
  return;
@@ -7878,13 +8041,13 @@ class AXPEntityCategoryTreeSelectorComponent extends AXBasePageComponent {
7878
8041
  // Ignore expand errors
7879
8042
  }
7880
8043
  }
7881
- // Load children via datasource
8044
+ // Load children via datasource (single call per parent)
7882
8045
  childNodes = await this.datasource(parentId);
7883
8046
  }
7884
8047
  if (!childNodes || childNodes.length === 0) {
7885
8048
  return;
7886
8049
  }
7887
- // Process each child
8050
+ // Process each child: fetch grandchildren once, then recurse with them to avoid double datasource call
7888
8051
  for (const childNode of childNodes) {
7889
8052
  const childId = String(childNode['id'] ?? '');
7890
8053
  if (!childId || childId === 'all' || collection.has(childId)) {
@@ -7892,15 +8055,13 @@ class AXPEntityCategoryTreeSelectorComponent extends AXBasePageComponent {
7892
8055
  }
7893
8056
  // Cache node data
7894
8057
  this.cacheNodeFromTreeNode(childNode);
7895
- // Check if this child has children
7896
- const hasChildren = await this.nodeHasChildren(childId, childNode);
7897
- if (!hasChildren) {
7898
- // This is a LEAF node - add it
8058
+ // Fetch children once; use result to decide leaf vs recurse (avoids nodeHasChildren -> extra datasource)
8059
+ const grandchildNodes = await this.datasource(childId);
8060
+ if (!grandchildNodes || grandchildNodes.length === 0) {
7899
8061
  collection.add(childId);
7900
8062
  }
7901
8063
  else {
7902
- // Has children - expand and recurse (pass tree component to expand children too)
7903
- await this.collectLeafNodes(childId, collection, treeComponent);
8064
+ await this.collectLeafNodes(childId, collection, treeComponent, grandchildNodes);
7904
8065
  }
7905
8066
  }
7906
8067
  }
@@ -7961,6 +8122,7 @@ class AXPEntityCategoryTreeSelectorComponent extends AXBasePageComponent {
7961
8122
  /**
7962
8123
  * Builds complete ancestor chains for all selected node IDs.
7963
8124
  * Returns a Map where key is the selected node ID and value is array of ancestor IDs from root to parent.
8125
+ * Batch-fetches missing node and ancestor data in parallel to minimize API calls.
7964
8126
  */
7965
8127
  async buildAncestorChains(selectedIds) {
7966
8128
  const ancestorChains = new Map();
@@ -7968,18 +8130,44 @@ class AXPEntityCategoryTreeSelectorComponent extends AXBasePageComponent {
7968
8130
  return ancestorChains;
7969
8131
  if (!this.treeData.categoryEntityDef?.parentKey)
7970
8132
  return ancestorChains;
8133
+ // Collect and batch-fetch all missing IDs (selected nodes without parent + ancestors) in rounds
8134
+ let toFetch = new Set();
8135
+ for (const nodeId of selectedIds) {
8136
+ const cached = this.nodeDataCache.get(nodeId);
8137
+ if (!cached || this.getParentIdFromNodeData(cached) === null) {
8138
+ toFetch.add(nodeId);
8139
+ }
8140
+ }
8141
+ while (toFetch.size > 0) {
8142
+ const results = await Promise.all(Array.from(toFetch).map((id) => this.fetchItemById(id)));
8143
+ const idsArr = Array.from(toFetch);
8144
+ results.forEach((item, i) => {
8145
+ if (item)
8146
+ this.nodeDataCache.set(idsArr[i], item);
8147
+ });
8148
+ toFetch = new Set();
8149
+ for (const nodeId of selectedIds) {
8150
+ let currentData = this.nodeDataCache.get(nodeId) ?? null;
8151
+ const visited = new Set();
8152
+ while (currentData) {
8153
+ const parentId = this.getParentIdFromNodeData(currentData);
8154
+ if (!parentId || visited.has(parentId))
8155
+ break;
8156
+ visited.add(parentId);
8157
+ if (!this.nodeDataCache.has(parentId)) {
8158
+ toFetch.add(parentId);
8159
+ break;
8160
+ }
8161
+ currentData = this.nodeDataCache.get(parentId) ?? null;
8162
+ }
8163
+ }
8164
+ }
8165
+ // Build chains from cache (no further API calls)
7971
8166
  for (const nodeId of selectedIds) {
7972
8167
  const chain = [];
7973
8168
  let currentData = this.nodeDataCache.get(nodeId) ?? null;
7974
8169
  if (!currentData)
7975
8170
  continue;
7976
- if (!this.getParentIdFromNodeData(currentData)) {
7977
- const refetched = await this.fetchItemById(nodeId);
7978
- if (refetched) {
7979
- currentData = refetched;
7980
- this.nodeDataCache.set(nodeId, refetched);
7981
- }
7982
- }
7983
8171
  const visited = new Set();
7984
8172
  while (currentData) {
7985
8173
  const parentId = this.getParentIdFromNodeData(currentData);
@@ -7987,19 +8175,7 @@ class AXPEntityCategoryTreeSelectorComponent extends AXBasePageComponent {
7987
8175
  break;
7988
8176
  visited.add(parentId);
7989
8177
  chain.unshift(parentId);
7990
- if (!this.nodeDataCache.has(parentId)) {
7991
- const parentData = await this.fetchItemById(parentId);
7992
- if (parentData) {
7993
- this.nodeDataCache.set(parentId, parentData);
7994
- currentData = parentData;
7995
- }
7996
- else {
7997
- break;
7998
- }
7999
- }
8000
- else {
8001
- currentData = this.nodeDataCache.get(parentId) ?? null;
8002
- }
8178
+ currentData = this.nodeDataCache.get(parentId) ?? null;
8003
8179
  }
8004
8180
  ancestorChains.set(nodeId, chain);
8005
8181
  }
@@ -8163,27 +8339,34 @@ class AXPEntityCategoryTreeSelectorComponent extends AXBasePageComponent {
8163
8339
  // Sort parents by depth (deepest first so we update bottom-up)
8164
8340
  const sortedParents = await this.sortParentsByDepth(Array.from(parentsToUpdate));
8165
8341
  const reversedParents = [...sortedParents].reverse(); // Deepest first
8342
+ // Batch-load children for parents that don't have them in tree (parallel instead of sequential)
8343
+ const parentIdsNeedingLoad = reversedParents.filter((parentId) => {
8344
+ const parentNode = treeComponent.findNode(parentId);
8345
+ const treeChildren = parentNode?.['children'];
8346
+ return !treeChildren || treeChildren.length === 0;
8347
+ });
8348
+ const childrenByParentId = new Map();
8349
+ if (parentIdsNeedingLoad.length > 0) {
8350
+ const results = await Promise.all(parentIdsNeedingLoad.map((id) => this.datasource(id)));
8351
+ results.forEach((dsChildren, index) => {
8352
+ if (dsChildren && dsChildren.length > 0) {
8353
+ childrenByParentId.set(parentIdsNeedingLoad[index], dsChildren.map((c) => ({ id: String(c['id'] ?? '') })));
8354
+ }
8355
+ });
8356
+ }
8166
8357
  for (const parentId of reversedParents) {
8167
8358
  try {
8168
8359
  const parentNode = treeComponent.findNode(parentId);
8169
- // Get all direct children of this parent
8170
- // First try from tree node, then fall back to datasource
8360
+ // Get all direct children: from tree node, or from batch-loaded map
8171
8361
  let childrenList = [];
8172
8362
  const treeChildren = parentNode?.['children'];
8173
8363
  if (treeChildren && treeChildren.length > 0) {
8174
8364
  childrenList = treeChildren.map((c) => ({ id: String(c['id'] ?? '') }));
8175
8365
  }
8176
8366
  else {
8177
- // Children not in tree node - load via datasource
8178
- try {
8179
- const dsChildren = await this.datasource(parentId);
8180
- if (dsChildren && dsChildren.length > 0) {
8181
- childrenList = dsChildren.map((c) => ({ id: String(c['id'] ?? '') }));
8182
- }
8183
- }
8184
- catch {
8185
- // Datasource failed, skip this parent
8186
- continue;
8367
+ const loaded = childrenByParentId.get(parentId);
8368
+ if (loaded && loaded.length > 0) {
8369
+ childrenList = loaded;
8187
8370
  }
8188
8371
  }
8189
8372
  if (childrenList.length === 0) {
@@ -8244,6 +8427,7 @@ class AXPEntityCategoryTreeSelectorComponent extends AXBasePageComponent {
8244
8427
  /**
8245
8428
  * Loads node data for IDs that are selected but not yet in the cache.
8246
8429
  * This is critical for pre-selected values in collapsed branches.
8430
+ * Fetches all missing IDs in parallel to minimize API calls and latency.
8247
8431
  */
8248
8432
  async loadMissingNodeData(selectedIds) {
8249
8433
  if (!this.treeData || !this.treeConfig || selectedIds.length === 0) {
@@ -8253,13 +8437,14 @@ class AXPEntityCategoryTreeSelectorComponent extends AXBasePageComponent {
8253
8437
  if (missingIds.length === 0)
8254
8438
  return;
8255
8439
  try {
8256
- // Prefer fetching each id via service (uses byKey when available = full item with parent)
8257
8440
  const valueField = this.treeConfig.valueField || 'id';
8258
- for (const id of missingIds) {
8259
- const item = await this.fetchItemById(id);
8260
- if (item)
8441
+ const results = await Promise.all(missingIds.map((id) => this.fetchItemById(id)));
8442
+ results.forEach((item, index) => {
8443
+ if (item) {
8444
+ const id = missingIds[index];
8261
8445
  this.nodeDataCache.set(String(item[valueField] ?? id), item);
8262
- }
8446
+ }
8447
+ });
8263
8448
  }
8264
8449
  catch (error) {
8265
8450
  console.error('Error loading missing node data:', error);
@@ -8268,18 +8453,22 @@ class AXPEntityCategoryTreeSelectorComponent extends AXBasePageComponent {
8268
8453
  /**
8269
8454
  * For each selected id, if cached item has no parent (e.g. batch query returned minimal fields),
8270
8455
  * re-fetch by id so we have parent/parentId for building ancestor chains.
8456
+ * Fetches all needing refresh in parallel.
8271
8457
  */
8272
8458
  async ensureParentDataInCache(selectedIds) {
8273
8459
  if (!this.treeConfig)
8274
8460
  return;
8275
- for (const id of selectedIds) {
8461
+ const idsNeedingFetch = selectedIds.filter((id) => {
8276
8462
  const cached = this.nodeDataCache.get(id);
8277
- if (cached && this.getParentIdFromNodeData(cached) === null) {
8278
- const full = await this.fetchItemById(id);
8279
- if (full)
8280
- this.nodeDataCache.set(id, full);
8281
- }
8282
- }
8463
+ return cached && this.getParentIdFromNodeData(cached) === null;
8464
+ });
8465
+ if (idsNeedingFetch.length === 0)
8466
+ return;
8467
+ const results = await Promise.all(idsNeedingFetch.map((id) => this.fetchItemById(id)));
8468
+ results.forEach((full, index) => {
8469
+ if (full)
8470
+ this.nodeDataCache.set(idsNeedingFetch[index], full);
8471
+ });
8283
8472
  }
8284
8473
  /**
8285
8474
  * Resolves parent ID from node data: supports nested `parent` object or flat parentId/parentKey.
@@ -15332,5 +15521,5 @@ function detectEntityChanges(oldObj, newObj) {
15332
15521
  * Generated bundle index. Do not edit.
15333
15522
  */
15334
15523
 
15335
- export { AXMEntityCrudService, AXMEntityCrudServiceImpl, AXPCategoryTreeService, AXPCreateEntityCommand, AXPCreateEntityWorkflow, AXPDataSeederService, AXPDeleteEntityWorkflow, AXPEntitiesListDataSourceDefinition, AXPEntityApplyUpdatesAction, AXPEntityCategoryTreeSelectorComponent, AXPEntityCategoryWidget, AXPEntityCategoryWidgetColumnComponent, AXPEntityCategoryWidgetEditComponent, AXPEntityCategoryWidgetViewComponent, AXPEntityCommandTriggerViewModel, AXPEntityCreateEvent, AXPEntityCreatePopupAction, AXPEntityCreateSubmittedAction, AXPEntityCreateViewElementViewModel, AXPEntityCreateViewModelFactory, AXPEntityCreateViewSectionViewModel, AXPEntityDataProvider, AXPEntityDataProviderImpl, AXPEntityDataSelectorService, AXPEntityDefinitionRegistryService, AXPEntityDeletedEvent, AXPEntityDetailListViewModel, AXPEntityDetailPopoverComponent, AXPEntityDetailPopoverService, AXPEntityDetailViewModelFactory, AXPEntityDetailViewModelResolver, AXPEntityEventDispatcherService, AXPEntityEventsKeys, AXPEntityFormBuilderService, AXPEntityListTableService, AXPEntityListViewColumnViewModel, AXPEntityListViewModelFactory, AXPEntityListViewModelResolver, AXPEntityListWidget, AXPEntityListWidgetViewComponent, AXPEntityMasterCreateViewModel, AXPEntityMasterListViewModel, AXPEntityMasterListViewQueryViewModel, AXPEntityMasterSingleElementViewModel, AXPEntityMasterSingleViewGroupViewModel, AXPEntityMasterSingleViewModel, AXPEntityMasterUpdateElementViewModel, AXPEntityMasterUpdateViewModel, AXPEntityMasterUpdateViewModelFactory, AXPEntityMiddleware, AXPEntityModifyConfirmedAction, AXPEntityModifyEvent, AXPEntityModifySectionPopupAction, AXPEntityModule, AXPEntityPerformDeleteAction, AXPEntityResolver, AXPEntityService, AXPEntityStorageService, AXPEntityUpdateViewSectionViewModel, AXPGetEntityDetailsQuery, AXPLookupWidget, AXPLookupWidgetColumnComponent, AXPLookupWidgetEditComponent, AXPLookupWidgetViewComponent, AXPMiddlewareAbortError, AXPMiddlewareEntityStorageService, AXPModifyEntitySectionWorkflow, AXPMultiSourceDefinitionProviderContext, AXPMultiSourceDefinitionProviderService, AXPMultiSourceFederatedSearchService, AXPMultiSourceSelectorComponent, AXPMultiSourceSelectorService, AXPMultiSourceSelectorWidget, AXPMultiSourceSelectorWidgetColumnComponent, AXPMultiSourceSelectorWidgetEditComponent, AXPMultiSourceSelectorWidgetViewComponent, AXPMultiSourceType, AXPOpenEntityDetailsCommand, AXPQuickEntityModifyPopupAction, AXPQuickModifyEntityWorkflow, AXPShowDetailViewAction, AXPShowDetailsViewWorkflow, AXPShowListViewAction, AXPShowListViewWorkflow, AXPTruncatedBreadcrumbComponent, AXPUpdateEntityCommand, AXPViewEntityDetailsCommand, AXP_DATA_SEEDER_TOKEN, AXP_ENTITY_ACTION_PLUGIN, AXP_ENTITY_CONFIG_TOKEN, AXP_ENTITY_DEFINITION_LOADER, AXP_ENTITY_MODIFIER, AXP_ENTITY_STORAGE_BACKEND, AXP_ENTITY_STORAGE_MIDDLEWARE, AXP_MULTI_SOURCE_DEFINITION_PROVIDER, DEFAULT_PROPERTY_ORDER, DEFAULT_SECTION_ORDER, EntityBuilder, EntityDataAccessor, actionExists, cloneLayoutArrays, columnWidthMiddleware, columnWidthMiddlewareProvider, createLayoutOrderingMiddlewareProvider, createModifierContext, detectEntityChanges, ensureLayoutPropertyView, ensureLayoutSection, ensureListActions, entityDetailsCreateActions, entityDetailsCrudActions, entityDetailsEditAction, entityDetailsNewEditAction, entityDetailsReferenceCondition, entityDetailsReferenceCreateActions, entityDetailsSimpleCondition, entityMasterBulkDeleteAction, entityMasterCreateAction, entityMasterCrudActions, entityMasterDeleteAction, entityMasterEditAction, entityMasterRecordActions, entityMasterViewAction, entityOverrideDetailsViewAction, eventDispatchMiddleware, isAXPMiddlewareAbortError, layoutOrderingMiddlewareFactory, layoutOrderingMiddlewareProvider };
15524
+ export { AXMEntityCrudService, AXMEntityCrudServiceImpl, AXPCategoryTreeService, AXPCreateEntityCommand, AXPCreateEntityWorkflow, AXPDataSeederService, AXPDeleteEntityWorkflow, AXPEntitiesListDataSourceDefinition, AXPEntityApplyUpdatesAction, AXPEntityCategoryTreeSelectorComponent, AXPEntityCategoryWidget, AXPEntityCategoryWidgetColumnComponent, AXPEntityCategoryWidgetEditComponent, AXPEntityCategoryWidgetViewComponent, AXPEntityCommandTriggerViewModel, AXPEntityCreateEvent, AXPEntityCreatePopupAction, AXPEntityCreateSubmittedAction, AXPEntityCreateViewElementViewModel, AXPEntityCreateViewModelFactory, AXPEntityCreateViewSectionViewModel, AXPEntityDataProvider, AXPEntityDataProviderImpl, AXPEntityDataSelectorService, AXPEntityDefinitionRegistryService, AXPEntityDeletedEvent, AXPEntityDetailListViewModel, AXPEntityDetailPopoverComponent, AXPEntityDetailPopoverService, AXPEntityDetailViewModelFactory, AXPEntityDetailViewModelResolver, AXPEntityEventDispatcherService, AXPEntityEventsKeys, AXPEntityFormBuilderService, AXPEntityListTableService, AXPEntityListViewColumnViewModel, AXPEntityListViewModelFactory, AXPEntityListViewModelResolver, AXPEntityListWidget, AXPEntityListWidgetViewComponent, AXPEntityMasterCreateViewModel, AXPEntityMasterListViewModel, AXPEntityMasterListViewQueryViewModel, AXPEntityMasterSingleElementViewModel, AXPEntityMasterSingleViewGroupViewModel, AXPEntityMasterSingleViewModel, AXPEntityMasterUpdateElementViewModel, AXPEntityMasterUpdateViewModel, AXPEntityMasterUpdateViewModelFactory, AXPEntityMiddleware, AXPEntityModifyConfirmedAction, AXPEntityModifyEvent, AXPEntityModifySectionPopupAction, AXPEntityModule, AXPEntityPerformDeleteAction, AXPEntityResolver, AXPEntityService, AXPEntityStorageService, AXPEntityUpdateViewSectionViewModel, AXPGetEntityDetailsQuery, AXPLayoutOrderingConfigService, AXPLookupWidget, AXPLookupWidgetColumnComponent, AXPLookupWidgetEditComponent, AXPLookupWidgetViewComponent, AXPMiddlewareAbortError, AXPMiddlewareEntityStorageService, AXPModifyEntitySectionWorkflow, AXPMultiSourceDefinitionProviderContext, AXPMultiSourceDefinitionProviderService, AXPMultiSourceFederatedSearchService, AXPMultiSourceSelectorComponent, AXPMultiSourceSelectorService, AXPMultiSourceSelectorWidget, AXPMultiSourceSelectorWidgetColumnComponent, AXPMultiSourceSelectorWidgetEditComponent, AXPMultiSourceSelectorWidgetViewComponent, AXPMultiSourceType, AXPOpenEntityDetailsCommand, AXPQuickEntityModifyPopupAction, AXPQuickModifyEntityWorkflow, AXPShowDetailViewAction, AXPShowDetailsViewWorkflow, AXPShowListViewAction, AXPShowListViewWorkflow, AXPTruncatedBreadcrumbComponent, AXPUpdateEntityCommand, AXPViewEntityDetailsCommand, AXP_DATA_SEEDER_TOKEN, AXP_ENTITY_ACTION_PLUGIN, AXP_ENTITY_CONFIG_TOKEN, AXP_ENTITY_DEFINITION_LOADER, AXP_ENTITY_MODIFIER, AXP_ENTITY_STORAGE_BACKEND, AXP_ENTITY_STORAGE_MIDDLEWARE, AXP_MULTI_SOURCE_DEFINITION_PROVIDER, DEFAULT_PROPERTY_ORDER, DEFAULT_SECTION_ORDER, EntityBuilder, EntityDataAccessor, actionExists, cloneLayoutArrays, columnWidthMiddleware, columnWidthMiddlewareProvider, createLayoutOrderingMiddlewareProvider, createModifierContext, detectEntityChanges, ensureLayoutPropertyView, ensureLayoutSection, ensureListActions, entityDetailsCreateActions, entityDetailsCrudActions, entityDetailsEditAction, entityDetailsNewEditAction, entityDetailsReferenceCondition, entityDetailsReferenceCreateActions, entityDetailsSimpleCondition, entityMasterBulkDeleteAction, entityMasterCreateAction, entityMasterCrudActions, entityMasterDeleteAction, entityMasterEditAction, entityMasterRecordActions, entityMasterViewAction, entityOverrideDetailsViewAction, eventDispatchMiddleware, isAXPMiddlewareAbortError, layoutOrderingMiddlewareFactory, layoutOrderingMiddlewareProvider };
15336
15525
  //# sourceMappingURL=acorex-platform-layout-entity.mjs.map