@acorex/platform 20.7.7 → 20.7.9

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,15 +1,14 @@
1
1
  import { AXToastService } from '@acorex/components/toast';
2
- import { AXPlatform } from '@acorex/core/platform';
3
2
  import * as i6 from '@acorex/core/translation';
4
3
  import { AXTranslationService, AXTranslationModule } from '@acorex/core/translation';
5
- import * as i4$1 from '@acorex/platform/common';
6
- import { AXPSettingsService, AXPFilterOperatorMiddlewareService, AXPEntityCommandScope, getEntityInfo, AXPRefreshEvent, AXPReloadEvent, AXPCommonSettings, AXPEntityQueryType, AXPCleanNestedFilters, AXPWorkflowNavigateAction, AXPToastAction, AXP_SEARCH_DEFINITION_PROVIDER } from '@acorex/platform/common';
7
- import { AXPDeviceService, AXPBroadcastEventService, applyFilterArray, applySortArray, resolveActionLook, AXPExpressionEvaluatorService, AXPDistributedEventListenerService, AXPPlatformScope, AXPColumnWidthService, AXHighlightService, extractValue, setSmart, getChangedPaths, defaultColumnWidthProvider, AXP_COLUMN_WIDTH_PROVIDER, AXP_DATASOURCE_DEFINITION_PROVIDER, AXPSystemActionType } from '@acorex/platform/core';
8
4
  import * as i0 from '@angular/core';
9
5
  import { InjectionToken, inject, Injector, runInInjectionContext, Injectable, input, viewChild, signal, ElementRef, ChangeDetectionStrategy, Component, ApplicationRef, EnvironmentInjector, createComponent, computed, ChangeDetectorRef, effect, Input, afterNextRender, untracked, ViewEncapsulation, viewChildren, linkedSignal, HostBinding, output, NgModule } from '@angular/core';
10
6
  import { Subject, takeUntil } from 'rxjs';
11
7
  import { AXPLayoutBuilderService } from '@acorex/platform/layout/builder';
8
+ import { AXPDeviceService, AXPBroadcastEventService, applyFilterArray, applySortArray, resolveActionLook, AXPExpressionEvaluatorService, AXPDistributedEventListenerService, AXPPlatformScope, AXPColumnWidthService, AXHighlightService, extractValue, setSmart, getChangedPaths, defaultColumnWidthProvider, AXP_COLUMN_WIDTH_PROVIDER, AXP_DATASOURCE_DEFINITION_PROVIDER, AXPSystemActionType } from '@acorex/platform/core';
12
9
  import { merge, castArray, get, cloneDeep, set, orderBy, isNil, isEmpty, isEqual } from 'lodash-es';
10
+ import * as i4$1 from '@acorex/platform/common';
11
+ import { AXPSettingsService, AXPFilterOperatorMiddlewareService, AXPEntityCommandScope, getEntityInfo, AXPRefreshEvent, AXPReloadEvent, AXPCommonSettings, AXPEntityQueryType, AXPCleanNestedFilters, AXPWorkflowNavigateAction, AXPToastAction, AXP_SEARCH_DEFINITION_PROVIDER } from '@acorex/platform/common';
13
12
  import { AXPSessionService, AXPAuthGuard } from '@acorex/platform/auth';
14
13
  import { Router, RouterModule, ROUTES } from '@angular/router';
15
14
  import * as i3 from '@acorex/components/button';
@@ -32,6 +31,7 @@ import { moveItemInArray } from '@angular/cdk/drag-drop';
32
31
  import { AXDialogService } from '@acorex/components/dialog';
33
32
  import { AXLoadingDialogService } from '@acorex/components/loading-dialog';
34
33
  import { AXPopupService } from '@acorex/components/popup';
34
+ import { AXPlatform } from '@acorex/core/platform';
35
35
  import * as i2$1 from '@acorex/components/badge';
36
36
  import { AXBadgeModule } from '@acorex/components/badge';
37
37
  import { AXCheckBoxModule } from '@acorex/components/check-box';
@@ -728,6 +728,10 @@ class PropertyFilter {
728
728
  this.externalActionsDelegate = delegate;
729
729
  return this;
730
730
  }
731
+ onAction(handler) {
732
+ this.onActionHandler = handler;
733
+ return this;
734
+ }
731
735
  field(groupId, path, delegate) {
732
736
  const list = this.extraFieldsByGroup.get(groupId) ?? [];
733
737
  list.push({ path, delegate });
@@ -1042,6 +1046,9 @@ class PropertyFilter {
1042
1046
  // For create/update, show cancel + submit
1043
1047
  d.setActions((a) => a.cancel().submit());
1044
1048
  }
1049
+ if (this.onActionHandler) {
1050
+ d.onAction(this.onActionHandler);
1051
+ }
1045
1052
  });
1046
1053
  return dialog;
1047
1054
  }
@@ -1376,12 +1383,9 @@ function sortMergedSections(mainSections, extraSections, mainSectionIds) {
1376
1383
  class AXPCreateEntityCommand {
1377
1384
  constructor() {
1378
1385
  this.entityForm = inject(AXPEntityFormBuilderService);
1379
- this.settingsService = inject(AXPSettingsService);
1380
1386
  this.entityService = inject(AXPEntityDefinitionRegistryService);
1381
1387
  this.toastService = inject(AXToastService);
1382
1388
  this.translationService = inject(AXTranslationService);
1383
- this.eventService = inject(AXPBroadcastEventService);
1384
- this.platform = inject(AXPlatform);
1385
1389
  this.context = {};
1386
1390
  }
1387
1391
  async execute(input) {
@@ -1404,100 +1408,83 @@ class AXPCreateEntityCommand {
1404
1408
  };
1405
1409
  }
1406
1410
  const entityRef = await this.entityService.resolve(moduleName, entityName);
1407
- let dialogRef;
1408
- try {
1409
- let chain = this.entityForm.entity(`${moduleName}.${entityName}`).create(data);
1410
- chain.actions((actions) => {
1411
- actions.cancel('@general:actions.cancel.title');
1412
- actions.submit('@general:actions.create.title');
1413
- });
1414
- if (excludeProperties && excludeProperties.length > 0) {
1415
- chain = chain.exclude(...excludeProperties);
1416
- }
1417
- if (includeProperties && includeProperties.length > 0) {
1418
- chain = chain.include(...includeProperties);
1419
- }
1420
- // Set dialog title: use decoration.header.title if available, otherwise use entityInfo.title
1421
- if (headerTitle) {
1422
- chain = chain.title(headerTitle);
1423
- }
1424
- else if (entityInfo?.title) {
1425
- const createText = await this.translationService.translateAsync('@general:actions.create.title');
1426
- const translatedTitle = await this.translationService.translateAsync(entityInfo.title);
1427
- chain = chain.title(`${createText} ${translatedTitle}`);
1428
- }
1429
- // Set dialog size: prioritize layout.size, then dialogSize from input, with platform-aware defaults
1430
- const finalSize = layoutSize || dialogSize;
1431
- if (this.platform.is('Mobile') || this.platform.is('SM')) {
1432
- chain.size('full');
1433
- }
1434
- else if (finalSize) {
1435
- chain.size(finalSize);
1436
- }
1437
- else {
1438
- chain.size('md');
1439
- }
1440
- dialogRef = await chain.show();
1441
- if (dialogRef.action() == 'cancel') {
1442
- dialogRef.close();
1443
- return {
1444
- success: false,
1445
- };
1411
+ let chain = this.entityForm.entity(`${moduleName}.${entityName}`).create(data);
1412
+ chain.actions((actions) => {
1413
+ actions.cancel('@general:actions.cancel.title');
1414
+ actions.submit('@general:actions.create.title');
1415
+ });
1416
+ if (excludeProperties && excludeProperties.length > 0) {
1417
+ chain = chain.exclude(...excludeProperties);
1418
+ }
1419
+ if (includeProperties && includeProperties.length > 0) {
1420
+ chain = chain.include(...includeProperties);
1421
+ }
1422
+ // Set dialog title: use decoration.header.title if available, otherwise use entityInfo.title
1423
+ if (headerTitle) {
1424
+ chain = chain.title(headerTitle);
1425
+ }
1426
+ else if (entityInfo?.title) {
1427
+ const createText = await this.translationService.translateAsync('@general:actions.create.title');
1428
+ const translatedTitle = await this.translationService.translateAsync(entityInfo.title);
1429
+ chain = chain.title(`${createText} ${translatedTitle}`);
1430
+ }
1431
+ // Set dialog size: prioritize layout.size, then dialogSize from input
1432
+ const finalSize = layoutSize || dialogSize;
1433
+ if (finalSize) {
1434
+ chain.size(finalSize);
1435
+ }
1436
+ const result = await chain
1437
+ .onAction(async (dialogRef) => {
1438
+ if (dialogRef.action() === 'cancel') {
1439
+ return { success: false };
1440
+ }
1441
+ const createFn = entityRef.commands?.create?.execute;
1442
+ if (!createFn) {
1443
+ const msg = await this.translationService.translateAsync('@general:messages.entity.create-command-unavailable');
1444
+ this.toastService.show({
1445
+ color: 'danger',
1446
+ title: await this.translationService.translateAsync('@general:messages.generic.error.title'),
1447
+ content: msg,
1448
+ });
1449
+ throw new Error(msg);
1446
1450
  }
1447
- else if (dialogRef.action() == 'submit') {
1448
- dialogRef.setLoading(true);
1451
+ dialogRef.setLoading(true);
1452
+ try {
1449
1453
  const context = dialogRef.context();
1450
- const createFn = entityRef.commands?.create?.execute;
1451
- if (!createFn) {
1452
- return {
1453
- success: false,
1454
- message: {
1455
- text: await this.translationService.translateAsync('@general:messages.entity.create-command-unavailable'),
1456
- },
1457
- };
1458
- }
1459
1454
  const result = await createFn(context);
1460
1455
  if (result) {
1461
- dialogRef.close();
1462
1456
  return {
1463
1457
  success: true,
1464
- data: result,
1458
+ data: result.data ?? result,
1465
1459
  message: {
1466
1460
  text: await this.translationService.translateAsync('@general:messages.generic.success.description'),
1467
1461
  },
1468
1462
  };
1469
1463
  }
1470
1464
  else {
1471
- return (result ?? {
1465
+ return {
1472
1466
  success: false,
1473
1467
  message: {
1474
- text: await this.translationService.translateAsync('@general:messages.entity.command-no-result'),
1468
+ text: await this.translationService.translateAsync('@general:messages.entity.create-failed'),
1475
1469
  },
1476
- });
1470
+ };
1477
1471
  }
1478
1472
  }
1479
- return {
1480
- success: false,
1481
- message: {
1482
- text: await this.translationService.translateAsync('@general:messages.entity.invalid-action'),
1483
- },
1484
- };
1485
- }
1486
- catch (error) {
1487
- return {
1488
- success: false,
1489
- message: {
1490
- text: error instanceof Error
1491
- ? error.message
1492
- : await this.translationService.translateAsync('@general:messages.entity.create-failed'),
1493
- },
1494
- };
1495
- }
1496
- finally {
1497
- if (dialogRef) {
1473
+ catch (e) {
1474
+ const errorMsg = e.message ?? (await this.translationService.translateAsync('@general:messages.entity.create-failed'));
1475
+ this.toastService.show({
1476
+ color: 'danger',
1477
+ title: await this.translationService.translateAsync('@general:messages.generic.error.title'),
1478
+ content: errorMsg,
1479
+ });
1480
+ throw e;
1481
+ }
1482
+ finally {
1498
1483
  dialogRef.setLoading(false);
1499
1484
  }
1500
- }
1485
+ })
1486
+ .show();
1487
+ return result;
1501
1488
  }
1502
1489
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: AXPCreateEntityCommand, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
1503
1490
  static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: AXPCreateEntityCommand, providedIn: 'root' }); }
@@ -5012,7 +4999,9 @@ class AXPPageListConverter extends AXPBaseRelatedEntityConverter {
5012
4999
  };
5013
5000
  return await context.expressionEvaluator.evaluate(actionData, scope);
5014
5001
  };
5015
- const evaluatedActions = await evaluateExpressions(relatedEntity?.actions);
5002
+ // Don't evaluate actions here - keep expression strings for lazy evaluation at execution time
5003
+ // This ensures actions use the latest context data when executed
5004
+ const actions = relatedEntity?.actions ?? [];
5016
5005
  const filters = await Promise.all(relatedEntity.conditions?.map(async (c) => {
5017
5006
  const value = await evaluateExpressions(c.value);
5018
5007
  return {
@@ -5027,7 +5016,7 @@ class AXPPageListConverter extends AXPBaseRelatedEntityConverter {
5027
5016
  title: `${context.rootTitle}`,
5028
5017
  label: relatedEntity.title,
5029
5018
  icon: relatedEntity.icon || entityDef.icon,
5030
- actions: this.mergeActions(entityDef, evaluatedActions)
5019
+ actions: this.mergeActions(entityDef, actions)
5031
5020
  ?.filter((a) => a.priority === 'primary')
5032
5021
  ?.map((a) => {
5033
5022
  return {
@@ -5051,7 +5040,7 @@ class AXPPageListConverter extends AXPBaseRelatedEntityConverter {
5051
5040
  execute: async (command, executeContext) => {
5052
5041
  try {
5053
5042
  const commandName = command.name.split('&')[0];
5054
- const mergedActions = this.mergeActions(entityDef, evaluatedActions);
5043
+ const mergedActions = this.mergeActions(entityDef, actions);
5055
5044
  const action = mergedActions.find((a) => {
5056
5045
  return a.name === commandName || a.name.split('&')[0] === commandName;
5057
5046
  });
@@ -5065,6 +5054,28 @@ class AXPPageListConverter extends AXPBaseRelatedEntityConverter {
5065
5054
  },
5066
5055
  };
5067
5056
  }
5057
+ // Evaluate action options with current root context for lazy evaluation
5058
+ // This ensures actions use the latest context data when executed
5059
+ let evaluatedOptions = action.options;
5060
+ if (action.options && context.context && context.expressionEvaluator) {
5061
+ try {
5062
+ const scope = {
5063
+ context: {
5064
+ eval: (path) => {
5065
+ return get(context.context, path);
5066
+ },
5067
+ },
5068
+ };
5069
+ evaluatedOptions = await context.expressionEvaluator.evaluate(action.options, scope);
5070
+ }
5071
+ catch {
5072
+ // Keep original options if evaluation fails
5073
+ evaluatedOptions = action.options;
5074
+ }
5075
+ }
5076
+ const actionData = action.scope == AXPEntityCommandScope.Selected
5077
+ ? executeContext
5078
+ : evaluatedOptions?.['process']?.data || null;
5068
5079
  if (context.commandService.exists(commandName)) {
5069
5080
  // check options for evaluation
5070
5081
  await context.commandService.execute(commandName, {
@@ -5077,13 +5088,11 @@ class AXPPageListConverter extends AXPBaseRelatedEntityConverter {
5077
5088
  parentKey: entityDef.parentKey,
5078
5089
  source: `${entityDef.module}.${entityDef.name}`,
5079
5090
  },
5080
- data: action.scope == AXPEntityCommandScope.Selected
5081
- ? executeContext
5082
- : action.options?.['process']?.data || null,
5083
- options: action.options,
5091
+ data: actionData,
5092
+ options: evaluatedOptions,
5084
5093
  metadata: action.metadata,
5085
5094
  },
5086
- options: action.options,
5095
+ options: evaluatedOptions,
5087
5096
  metadata: action.metadata,
5088
5097
  });
5089
5098
  }
@@ -5097,10 +5106,8 @@ class AXPPageListConverter extends AXPBaseRelatedEntityConverter {
5097
5106
  parentKey: entityDef.parentKey,
5098
5107
  source: `${entityDef.module}.${entityDef.name}`,
5099
5108
  },
5100
- data: action.scope == AXPEntityCommandScope.Selected
5101
- ? executeContext
5102
- : action.options?.['process']?.data || null,
5103
- options: action.options,
5109
+ data: actionData,
5110
+ options: evaluatedOptions,
5104
5111
  metadata: action.metadata,
5105
5112
  });
5106
5113
  }
@@ -5129,7 +5136,7 @@ class AXPPageListConverter extends AXPBaseRelatedEntityConverter {
5129
5136
  options: {
5130
5137
  entity: relatedEntity.entity,
5131
5138
  showEntityActions: false,
5132
- actions: evaluatedActions,
5139
+ actions: actions,
5133
5140
  includeColumns: relatedEntity.columns,
5134
5141
  },
5135
5142
  },
@@ -5216,7 +5223,9 @@ class AXPTabListConverter extends AXPBaseRelatedEntityConverter {
5216
5223
  hidden: true,
5217
5224
  };
5218
5225
  }) ?? []);
5219
- const actions = await evaluateExpressions(relatedEntity.actions);
5226
+ // Don't evaluate actions here - keep expression strings for lazy evaluation at execution time
5227
+ // This ensures actions use the latest context data when executed
5228
+ const actions = relatedEntity.actions;
5220
5229
  return {
5221
5230
  id: entityDef?.name ?? '',
5222
5231
  title: relatedEntity.title ?? entityDef?.title ?? '',
@@ -6618,11 +6627,22 @@ class AXPCategoryTreeService {
6618
6627
  convertToTreeNode(item, config) {
6619
6628
  const textField = config.textField ?? 'title';
6620
6629
  const valueField = config.valueField ?? 'id';
6630
+ // Determine childrenCount properly:
6631
+ // - Use explicit childrenCount from backend if it's a number (including 0)
6632
+ // - Or use children array length if available
6633
+ // - Default to undefined to indicate "unknown" - this allows lazy loading to work
6634
+ let childrenCount;
6635
+ if (typeof item['childrenCount'] === 'number') {
6636
+ childrenCount = item['childrenCount'];
6637
+ }
6638
+ else if (Array.isArray(item['children'])) {
6639
+ childrenCount = item['children'].length;
6640
+ }
6621
6641
  return {
6622
6642
  id: String(item[valueField] ?? ''),
6623
6643
  title: String(item[textField] ?? ''),
6624
6644
  icon: 'fa-solid fa-folder',
6625
- childrenCount: item['childrenCount'] || item['children']?.length || 0,
6645
+ childrenCount,
6626
6646
  expanded: false,
6627
6647
  data: item,
6628
6648
  };
@@ -6778,7 +6798,6 @@ class AXPEntityCategoryTreeSelectorComponent extends AXBasePageComponent {
6778
6798
  this.relevantNodeIds = new Set(); // For search filtering
6779
6799
  this.expandedNodesBeforeSearch = []; // Store expanded nodes before search to restore after
6780
6800
  this.nodesExpandedDuringSearch = []; // Track nodes we expanded during search
6781
- this.isInitializing = false; // Flag to track if we're in initialization phase
6782
6801
  /** Datasource callback for tree-view component. */
6783
6802
  this.datasource = async (id) => {
6784
6803
  if (!this.treeData || !this.treeConfig) {
@@ -6825,31 +6844,48 @@ class AXPEntityCategoryTreeSelectorComponent extends AXBasePageComponent {
6825
6844
  }
6826
6845
  });
6827
6846
  }
6828
- // Mark pre-selected leaf nodes as selected in the child nodes
6829
- const selectedIds = this.selectedNodeIds();
6830
- childNodes.forEach((node) => {
6831
- const nodeId = String(node['id'] ?? '');
6832
- if (nodeId && selectedIds.includes(nodeId)) {
6833
- node['selected'] = true;
6834
- }
6835
- });
6836
- // After children load, programmatically select pre-selected nodes in tree component
6837
- const treeComponent = this.tree();
6838
- if (treeComponent && selectedIds.length > 0) {
6839
- // Use setTimeout to ensure nodes are in tree structure before selecting
6840
- setTimeout(() => {
6841
- childNodes.forEach((node) => {
6842
- const nodeId = String(node['id'] ?? '');
6843
- if (nodeId && selectedIds.includes(nodeId) && nodeId !== 'all') {
6844
- try {
6845
- treeComponent.selectNode(nodeId);
6846
- }
6847
- catch {
6848
- // Node might not be in tree yet
6849
- }
6847
+ // Mark pre-selected nodes as selected in the child nodes (for visual display)
6848
+ // ONLY do this during initial load, NOT during user selection changes
6849
+ if (!this.isUpdatingSelection) {
6850
+ const selectedIds = this.selectedNodeIds();
6851
+ childNodes.forEach((node) => {
6852
+ const nodeId = String(node['id'] ?? '');
6853
+ if (nodeId && selectedIds.includes(nodeId)) {
6854
+ node['selected'] = true;
6855
+ }
6856
+ });
6857
+ // After children load, programmatically select pre-selected nodes in tree component
6858
+ // This ensures the tree's internal state matches our selectedNodeIds
6859
+ // Skip this if we're in the middle of a selection update (user is selecting/deselecting)
6860
+ const treeComponent = this.tree();
6861
+ if (treeComponent && selectedIds.length > 0) {
6862
+ // Use setTimeout to ensure nodes are in tree structure before selecting
6863
+ setTimeout(() => {
6864
+ // Double-check we're not in a selection update when timeout fires
6865
+ if (this.isUpdatingSelection) {
6866
+ return;
6850
6867
  }
6851
- });
6852
- }, 10);
6868
+ this.isUpdatingSelection = true;
6869
+ try {
6870
+ // Re-check selectedNodeIds at this point (might have changed)
6871
+ const currentSelectedIds = this.selectedNodeIds();
6872
+ childNodes.forEach((node) => {
6873
+ const nodeId = String(node['id'] ?? '');
6874
+ if (nodeId && currentSelectedIds.includes(nodeId) && nodeId !== 'all') {
6875
+ try {
6876
+ treeComponent.selectNode(nodeId);
6877
+ }
6878
+ catch {
6879
+ // Node might not be in tree yet
6880
+ }
6881
+ }
6882
+ });
6883
+ }
6884
+ finally {
6885
+ this.isUpdatingSelection = false;
6886
+ }
6887
+ }, 10);
6888
+ }
6853
6889
  }
6854
6890
  return childNodes;
6855
6891
  };
@@ -6868,7 +6904,6 @@ class AXPEntityCategoryTreeSelectorComponent extends AXBasePageComponent {
6868
6904
  return;
6869
6905
  }
6870
6906
  this.loading.set(true);
6871
- this.isInitializing = true; // Mark that we're in initialization phase
6872
6907
  try {
6873
6908
  this.treeConfig = {
6874
6909
  entityKey: this.entityKey(),
@@ -6878,7 +6913,6 @@ class AXPEntityCategoryTreeSelectorComponent extends AXBasePageComponent {
6878
6913
  this.treeData = await this.categoryTreeService.initializeCategoryTree(this.treeConfig);
6879
6914
  if (!this.treeData) {
6880
6915
  this.loading.set(false);
6881
- this.isInitializing = false;
6882
6916
  return;
6883
6917
  }
6884
6918
  // Get parentKey from entity definition
@@ -6924,7 +6958,6 @@ class AXPEntityCategoryTreeSelectorComponent extends AXBasePageComponent {
6924
6958
  console.error('Error syncing selection after tree render:', error);
6925
6959
  }
6926
6960
  }
6927
- this.isInitializing = false; // Mark initialization as complete
6928
6961
  }
6929
6962
  //#endregion
6930
6963
  //#region ---- Public Methods ----
@@ -7346,6 +7379,8 @@ class AXPEntityCategoryTreeSelectorComponent extends AXBasePageComponent {
7346
7379
  this.nodesExpandedDuringSearch = [];
7347
7380
  return;
7348
7381
  }
7382
+ // Store current selected IDs before reload
7383
+ const selectedIds = this.selectedNodeIds();
7349
7384
  // Reload tree to show all nodes (no filtering)
7350
7385
  await treeComponent.reloadData();
7351
7386
  // Collapse nodes that were expanded during search (in reverse order - leaves first)
@@ -7364,88 +7399,136 @@ class AXPEntityCategoryTreeSelectorComponent extends AXBasePageComponent {
7364
7399
  }
7365
7400
  // Clear the stored expanded nodes
7366
7401
  this.expandedNodesBeforeSearch = [];
7402
+ // Restore selection state after tree reload
7403
+ if (selectedIds.length > 0) {
7404
+ // Wait for tree to stabilize after reload
7405
+ await new Promise((resolve) => setTimeout(resolve, 100));
7406
+ // Re-sync selection with tree
7407
+ await this.restoreSelectionAfterReload(selectedIds);
7408
+ }
7367
7409
  }
7368
- async onNodeToggle(event) {
7369
- // Tree component handles lazy loading via datasource callback
7410
+ /**
7411
+ * Restores selection state after tree reload.
7412
+ * Expands ancestor nodes and selects the previously selected leaf nodes.
7413
+ */
7414
+ async restoreSelectionAfterReload(selectedIds) {
7415
+ const treeComponent = this.tree();
7416
+ if (!treeComponent || selectedIds.length === 0) {
7417
+ return;
7418
+ }
7419
+ this.isUpdatingSelection = true;
7420
+ try {
7421
+ // Build ancestor chains for selected nodes
7422
+ const ancestorChains = await this.buildAncestorChains(selectedIds);
7423
+ // Expand ancestor nodes to make selected nodes visible
7424
+ await this.expandAncestorNodesInOrder(ancestorChains);
7425
+ // Wait for tree to render expanded nodes
7426
+ await new Promise((resolve) => setTimeout(resolve, 50));
7427
+ // Select the nodes visually in the tree
7428
+ for (const id of selectedIds) {
7429
+ try {
7430
+ treeComponent.selectNode(id);
7431
+ }
7432
+ catch {
7433
+ // Node might not be in tree yet
7434
+ }
7435
+ }
7436
+ }
7437
+ finally {
7438
+ this.isUpdatingSelection = false;
7439
+ }
7370
7440
  }
7371
7441
  async onNodeSelect(event) {
7372
7442
  const node = event.node;
7373
7443
  const nodeId = String(node['id'] ?? '');
7374
- if (!node || nodeId === 'all') {
7444
+ if (!node) {
7375
7445
  return;
7376
7446
  }
7377
- // Cache node data for getSelectedItems
7378
- const nodeData = node['data'];
7379
- if (nodeData && typeof nodeData === 'object' && nodeData !== null && !Array.isArray(nodeData)) {
7380
- this.nodeDataCache.set(nodeId, nodeData);
7447
+ // Only process USER interactions, not programmatic selections
7448
+ // This prevents infinite loops when datasource callback syncs selection
7449
+ if (!event.isUserInteraction) {
7450
+ return;
7381
7451
  }
7382
- // NOTE: We do NOT call loadAndSelectChildrenRecursively here
7383
- // The recursive selection should only happen when user explicitly selects a node via checkbox
7384
- // (handled in handleCheckboxChange). This prevents intermediate parent states from
7385
- // triggering unwanted sibling selections.
7386
- }
7387
- async onSelectionChange(event) {
7388
- // Don't process during initialization or batch updates - let those handle it
7389
- if (this.isInitializing || this.isUpdatingSelection) {
7452
+ // Don't process if we're already updating selection
7453
+ if (this.isUpdatingSelection) {
7390
7454
  return;
7391
7455
  }
7392
- // The tree component fires selection changes, but we only want to track LEAF nodes
7393
- // Parent nodes are auto-selected/deselected by the tree's intermediate-nested behavior
7394
- const selectedNodes = event.selectedNodes || [];
7395
- // Cache node data for all selected nodes first
7396
- selectedNodes.forEach((node) => {
7397
- const nodeId = String(node['id'] ?? '');
7456
+ // Cache node data for getSelectedItems (except for 'all' node)
7457
+ if (nodeId !== 'all') {
7398
7458
  const nodeData = node['data'];
7399
- if (nodeData && nodeId && typeof nodeData === 'object') {
7459
+ if (nodeData && typeof nodeData === 'object' && nodeData !== null && !Array.isArray(nodeData)) {
7400
7460
  this.nodeDataCache.set(nodeId, nodeData);
7401
7461
  }
7402
- });
7403
- // Filter to only leaf nodes (nodes with no children or childrenCount === 0)
7404
- const leafNodeIds = [];
7405
- for (const node of selectedNodes) {
7406
- const nodeId = String(node['id'] ?? '');
7407
- if (!nodeId || nodeId === 'all') {
7408
- continue;
7409
- }
7410
- // Check if this is a leaf node
7462
+ }
7463
+ // Check if this node is being selected or deselected
7464
+ const isSelected = node['selected'] === true;
7465
+ if (this.allowMultiple()) {
7411
7466
  const children = node['children'];
7412
7467
  const childrenCount = node['childrenCount'];
7413
- const isLeaf = (!children || children.length === 0) && (childrenCount === undefined || childrenCount === 0);
7414
- if (isLeaf) {
7415
- leafNodeIds.push(nodeId);
7416
- }
7417
- }
7418
- // Update selectedNodeIds with only leaf nodes
7419
- this.selectedNodeIds.set(leafNodeIds);
7420
- }
7421
- // protected handleNodeClick(event: any): void {
7422
- // // Extract node from event - could be { node: AXTreeNode } or just AXTreeNode
7423
- // const node: AXTreeViewNode = event?.node || event;
7424
- // if (!node || node.id === 'all') {
7425
- // return;
7426
- // }
7427
- // // Cache node data for getSelectedItems
7428
- // if (node.data) {
7429
- // this.nodeDataCache.set(node.id, node.data);
7430
- // }
7431
- // if (this.allowMultiple()) {
7432
- // // In multiple mode with checkboxes, clicking the node should toggle the checkbox
7433
- // // The checkbox state is handled by onNodeSelect event, so we just sync the selection here
7434
- // const currentIds = this.selectedNodeIds();
7435
- // const isSelected = currentIds.includes(node.id);
7436
- // // Toggle selection state
7437
- // // Note: The actual checkbox toggle is handled by the tree component's onNodeSelect event
7438
- // if (isSelected) {
7439
- // this.selectedNodeIds.set(currentIds.filter((id) => id !== node.id));
7440
- // } else {
7441
- // this.selectedNodeIds.set([...currentIds, node.id]);
7442
- // }
7443
- // } else {
7444
- // // Single selection - auto-confirm on click
7445
- // this.selectedNodeIds.set([node.id]);
7446
- // this.close({ selected: this.getSelectedItems() });
7447
- // }
7448
- // }
7468
+ // Determine if this node has children (is a parent node)
7469
+ // A node has children if: childrenCount > 0, or has non-empty children array
7470
+ // Note: childrenCount === undefined means "unknown" - we need to check via datasource
7471
+ const hasLoadedChildren = children && children.length > 0;
7472
+ const hasChildrenCount = childrenCount !== undefined && childrenCount > 0;
7473
+ const isDefinitelyLeaf = childrenCount === 0 && (!children || children.length === 0);
7474
+ if (isSelected) {
7475
+ // SELECTION: Only add LEAF nodes to selectedNodeIds
7476
+ this.isUpdatingSelection = true;
7477
+ try {
7478
+ if (nodeId === 'all') {
7479
+ // "All Items" selected - recursively select all leaf descendants
7480
+ await this.selectAllLeafDescendants(nodeId);
7481
+ }
7482
+ else if (isDefinitelyLeaf) {
7483
+ // This is definitely a leaf node - add it directly
7484
+ const currentSelected = new Set(this.selectedNodeIds());
7485
+ currentSelected.add(nodeId);
7486
+ this.selectedNodeIds.set(Array.from(currentSelected));
7487
+ }
7488
+ else if (hasLoadedChildren || hasChildrenCount) {
7489
+ // This node has children - only add its leaf descendants
7490
+ await this.selectAllLeafDescendants(nodeId);
7491
+ }
7492
+ else {
7493
+ // childrenCount is undefined - need to check if it has children
7494
+ const childNodes = await this.datasource(nodeId);
7495
+ if (!childNodes || childNodes.length === 0) {
7496
+ // No children - this is a leaf node, add it
7497
+ const currentSelected = new Set(this.selectedNodeIds());
7498
+ currentSelected.add(nodeId);
7499
+ this.selectedNodeIds.set(Array.from(currentSelected));
7500
+ }
7501
+ else {
7502
+ // Has children - only add leaf descendants
7503
+ await this.selectAllLeafDescendants(nodeId);
7504
+ }
7505
+ }
7506
+ }
7507
+ finally {
7508
+ this.isUpdatingSelection = false;
7509
+ }
7510
+ }
7511
+ else {
7512
+ // DESELECTION: Remove node (if leaf) and all leaf descendants from selectedNodeIds
7513
+ this.isUpdatingSelection = true;
7514
+ try {
7515
+ await this.deselectAllLeafDescendants(nodeId);
7516
+ }
7517
+ finally {
7518
+ this.isUpdatingSelection = false;
7519
+ }
7520
+ }
7521
+ }
7522
+ else {
7523
+ // Single selection mode: just update selectedNodeIds
7524
+ if (isSelected) {
7525
+ this.selectedNodeIds.set([nodeId]);
7526
+ }
7527
+ else {
7528
+ this.selectedNodeIds.set([]);
7529
+ }
7530
+ }
7531
+ }
7449
7532
  async onConfirm() {
7450
7533
  // Use internal selectedNodeIds state which is kept in sync via onNodeSelect events
7451
7534
  const selectedItems = await this.getSelectedItems();
@@ -7458,21 +7541,11 @@ class AXPEntityCategoryTreeSelectorComponent extends AXBasePageComponent {
7458
7541
  * Clears all selected items
7459
7542
  */
7460
7543
  onClearAll() {
7544
+ this.selectedNodeIds.set([]);
7461
7545
  const treeComponent = this.tree();
7462
7546
  if (treeComponent) {
7463
- // Deselect all nodes in tree component
7464
- const currentSelected = this.selectedNodeIds();
7465
- for (const id of currentSelected) {
7466
- try {
7467
- treeComponent.deselectNode(id);
7468
- }
7469
- catch {
7470
- // Node might not be in tree
7471
- }
7472
- }
7547
+ treeComponent.deselectAll();
7473
7548
  }
7474
- // Clear the selection
7475
- this.selectedNodeIds.set([]);
7476
7549
  }
7477
7550
  /**
7478
7551
  * Checks if a node matches the current search term
@@ -7491,543 +7564,207 @@ class AXPEntityCategoryTreeSelectorComponent extends AXBasePageComponent {
7491
7564
  return this.selectedNodeIds().includes(id);
7492
7565
  }
7493
7566
  /**
7494
- * Handles checkbox change event to toggle node selection
7495
- * In multiple mode: recursively selects/deselects LEAF children only
7496
- * Parent states are calculated based on leaf descendants
7567
+ * Expands parent nodes, collects all LEAF descendants, and selects them visually.
7568
+ * If the parent itself is a leaf (no children), it will be added.
7497
7569
  */
7498
- async handleCheckboxChange(nodeId, checked) {
7499
- if (!nodeId || nodeId === 'all') {
7570
+ async selectAllLeafDescendants(parentId) {
7571
+ if (!this.treeData || !this.treeConfig) {
7500
7572
  return;
7501
7573
  }
7502
- const id = String(nodeId);
7503
7574
  const treeComponent = this.tree();
7504
- if (!treeComponent) {
7505
- return;
7506
- }
7507
- if (checked) {
7508
- // Select all descendant LEAF nodes
7509
- await this.selectLeafDescendants(id);
7510
- }
7511
- else {
7512
- // Deselect all descendant LEAF nodes
7513
- await this.deselectLeafDescendants(id);
7514
- }
7515
- // Update parent states after selection change
7516
- await this.refreshParentStatesInTree();
7517
- }
7518
- /**
7519
- * Selects all leaf descendants of a node (and the node itself if it's a leaf)
7520
- */
7521
- async selectLeafDescendants(nodeId) {
7522
- this.isUpdatingSelection = true;
7523
7575
  try {
7524
- // Collect all leaf descendants
7525
- const leafNodes = new Set();
7526
- await this.collectLeafDescendants(nodeId, leafNodes);
7527
- if (leafNodes.size === 0) {
7528
- // Node itself is a leaf
7529
- leafNodes.add(nodeId);
7530
- }
7531
- // Update selectedNodeIds with leaf nodes only
7576
+ const leafIds = new Set();
7577
+ // Expand and collect leaf nodes simultaneously
7578
+ await this.collectLeafNodes(parentId, leafIds, treeComponent);
7579
+ // Also check if the parent itself is a leaf (has no children)
7580
+ if (parentId !== 'all') {
7581
+ const isLeaf = await this.isLeafNodeCheck(parentId);
7582
+ if (isLeaf) {
7583
+ leafIds.add(parentId);
7584
+ }
7585
+ }
7586
+ if (leafIds.size === 0) {
7587
+ return;
7588
+ }
7589
+ // Update our internal state
7532
7590
  const currentSelected = new Set(this.selectedNodeIds());
7533
- leafNodes.forEach((id) => currentSelected.add(id));
7591
+ for (const leafId of leafIds) {
7592
+ currentSelected.add(leafId);
7593
+ }
7534
7594
  this.selectedNodeIds.set(Array.from(currentSelected));
7535
- // Sync with tree component
7536
- const treeComponent = this.tree();
7595
+ // Select all leaf nodes visually in the tree
7537
7596
  if (treeComponent) {
7538
- for (const leafId of leafNodes) {
7597
+ for (const leafId of leafIds) {
7539
7598
  try {
7540
- const node = treeComponent.findNode(leafId);
7541
- if (node) {
7542
- treeComponent.selectNode(leafId);
7543
- }
7599
+ treeComponent.selectNode(leafId);
7544
7600
  }
7545
7601
  catch {
7546
- // Node might not be in tree
7602
+ // Node might not be in tree yet
7547
7603
  }
7548
7604
  }
7549
7605
  }
7550
7606
  }
7551
- finally {
7552
- this.isUpdatingSelection = false;
7553
- }
7554
- }
7555
- /**
7556
- * Deselects all leaf descendants of a node (and the node itself if it's a leaf)
7557
- */
7558
- async deselectLeafDescendants(nodeId) {
7559
- this.isUpdatingSelection = true;
7560
- try {
7561
- // Collect all leaf descendants
7562
- const leafNodes = new Set();
7563
- await this.collectLeafDescendants(nodeId, leafNodes);
7564
- if (leafNodes.size === 0) {
7565
- // Node itself is a leaf
7566
- leafNodes.add(nodeId);
7567
- }
7568
- // Remove leaf nodes from selectedNodeIds
7569
- const currentSelected = this.selectedNodeIds();
7570
- const newSelected = currentSelected.filter((id) => !leafNodes.has(id));
7571
- this.selectedNodeIds.set(newSelected);
7572
- // Sync with tree component
7573
- const treeComponent = this.tree();
7574
- if (treeComponent) {
7575
- for (const leafId of leafNodes) {
7576
- try {
7577
- const node = treeComponent.findNode(leafId);
7578
- if (node) {
7579
- treeComponent.deselectNode(leafId);
7580
- }
7581
- }
7582
- catch {
7583
- // Node might not be in tree
7584
- }
7585
- }
7586
- }
7587
- }
7588
- finally {
7589
- this.isUpdatingSelection = false;
7607
+ catch (error) {
7608
+ console.error(`Error selecting leaf descendants for node ${parentId}:`, error);
7590
7609
  }
7591
7610
  }
7592
7611
  /**
7593
- * Collects all LEAF descendant node IDs recursively
7594
- * A leaf node is one that has no children
7612
+ * Removes all LEAF descendants from selectedNodeIds.
7613
+ * For 'all' node: clears everything and uses tree's deselectAll().
7614
+ * For other nodes: tree handles visual state via user click.
7595
7615
  */
7596
- async collectLeafDescendants(parentId, collection) {
7597
- if (!this.treeData || !this.treeConfig || !parentId || parentId === 'all') {
7616
+ async deselectAllLeafDescendants(parentId) {
7617
+ if (!this.treeData || !this.treeConfig) {
7598
7618
  return;
7599
7619
  }
7600
7620
  try {
7601
- const childNodes = await this.datasource(parentId);
7602
- if (!childNodes || childNodes.length === 0) {
7603
- // No children means this node is a leaf - add the parent itself
7604
- // But only if it wasn't already added through the parent call
7621
+ // Special case: deselecting 'all' clears everything
7622
+ if (parentId === 'all') {
7623
+ this.selectedNodeIds.set([]);
7624
+ // Use tree's deselectAll() to clear all visual selections
7625
+ const treeComponent = this.tree();
7626
+ if (treeComponent) {
7627
+ treeComponent.deselectAll();
7628
+ }
7605
7629
  return;
7606
7630
  }
7607
- for (const childNode of childNodes) {
7608
- const childId = String(childNode['id'] ?? '');
7609
- if (!childId || childId === 'all') {
7610
- continue;
7611
- }
7612
- // Cache node data
7613
- const nodeData = childNode['data'];
7614
- if (nodeData && typeof nodeData === 'object') {
7615
- this.nodeDataCache.set(childId, nodeData);
7616
- }
7617
- // Check if this child is a leaf by trying to load its children
7618
- const grandchildNodes = await this.datasource(childId);
7619
- if (!grandchildNodes || grandchildNodes.length === 0) {
7620
- // This child is a leaf node
7621
- collection.add(childId);
7622
- }
7623
- else {
7624
- // This child has children, recurse
7625
- await this.collectLeafDescendants(childId, collection);
7626
- }
7631
+ const leafIds = new Set();
7632
+ await this.collectLeafNodes(parentId, leafIds);
7633
+ // Also check if the parent itself is a leaf
7634
+ const isLeaf = await this.isLeafNodeCheck(parentId);
7635
+ if (isLeaf) {
7636
+ leafIds.add(parentId);
7627
7637
  }
7628
- }
7629
- catch (error) {
7630
- console.error(`Error collecting leaf descendants for node ${parentId}:`, error);
7631
- }
7632
- }
7633
- /**
7634
- * Refreshes parent states in the tree based on leaf selection
7635
- */
7636
- async refreshParentStatesInTree() {
7637
- const treeComponent = this.tree();
7638
- if (!treeComponent) {
7639
- return;
7640
- }
7641
- // The tree component with intermediate-nested behavior should handle this
7642
- // But we need to force a refresh by reloading data
7643
- // For now, we rely on the tree component's built-in behavior
7644
- }
7645
- /**
7646
- * Selects a node and recursively selects all its children
7647
- */
7648
- async selectNodeAndChildren(nodeId) {
7649
- const treeComponent = this.tree();
7650
- if (!treeComponent) {
7651
- return;
7652
- }
7653
- // Set flag to prevent recursive updates during batch operation
7654
- this.isUpdatingSelection = true;
7655
- try {
7656
- // Collect all nodes to select (node + all descendants)
7657
- const nodesToSelect = new Set([nodeId]);
7658
- await this.collectAllDescendants(nodeId, nodesToSelect);
7659
- // Batch update selectedNodeIds
7660
- const currentSelected = this.selectedNodeIds();
7661
- const newSelected = [...currentSelected];
7662
- for (const id of nodesToSelect) {
7663
- if (!newSelected.includes(id)) {
7664
- newSelected.push(id);
7665
- }
7638
+ if (leafIds.size === 0) {
7639
+ return;
7666
7640
  }
7641
+ // Only update our internal state - tree handles visual state
7642
+ const currentSelected = this.selectedNodeIds();
7643
+ const newSelected = currentSelected.filter((id) => !leafIds.has(id));
7667
7644
  this.selectedNodeIds.set(newSelected);
7668
- // Batch select in tree component (with small delays to prevent glitches)
7669
- for (const id of nodesToSelect) {
7670
- try {
7671
- const node = treeComponent.findNode(id);
7672
- if (node) {
7673
- treeComponent.selectNode(id);
7674
- // Small delay to prevent overwhelming the tree component
7675
- await new Promise((resolve) => setTimeout(resolve, 5));
7676
- }
7677
- }
7678
- catch {
7679
- // Node might not be in tree yet
7680
- }
7681
- }
7682
7645
  }
7683
- finally {
7684
- this.isUpdatingSelection = false;
7646
+ catch (error) {
7647
+ console.error(`Error deselecting leaf descendants for node ${parentId}:`, error);
7685
7648
  }
7686
7649
  }
7687
7650
  /**
7688
- * Collects all descendant node IDs recursively
7651
+ * Recursively expands parent nodes and collects LEAF node IDs.
7652
+ * When treeComponent is provided, expands each parent node before loading children.
7689
7653
  */
7690
- async collectAllDescendants(parentId, collection) {
7691
- if (!this.treeData || !this.treeConfig || !parentId || parentId === 'all') {
7654
+ async collectLeafNodes(parentId, collection, treeComponent) {
7655
+ if (!this.treeData || !this.treeConfig) {
7692
7656
  return;
7693
7657
  }
7694
7658
  try {
7695
- const childNodes = await this.datasource(parentId);
7696
- if (!childNodes || childNodes.length === 0) {
7697
- return;
7698
- }
7699
- for (const childNode of childNodes) {
7700
- const childId = String(childNode['id'] ?? '');
7701
- if (childId && childId !== 'all' && !collection.has(childId)) {
7702
- collection.add(childId);
7703
- // Cache node data
7704
- const nodeData = childNode['data'];
7705
- if (nodeData && typeof nodeData === 'object') {
7706
- this.nodeDataCache.set(childId, nodeData);
7659
+ let childNodes;
7660
+ if (parentId === 'all') {
7661
+ // Expand 'all' node if tree component provided
7662
+ if (treeComponent) {
7663
+ try {
7664
+ treeComponent.expandNode('all');
7707
7665
  }
7708
- else if (childNode && typeof childNode === 'object') {
7709
- const valueField = this.treeConfig.valueField || 'id';
7710
- const textField = this.treeConfig.textField || 'title';
7711
- const dataObj = {
7712
- [valueField]: childId,
7713
- [textField]: childNode['title'] ?? '',
7714
- ...childNode,
7715
- };
7716
- this.nodeDataCache.set(childId, dataObj);
7666
+ catch {
7667
+ // Ignore expand errors
7717
7668
  }
7718
- // Recursively collect descendants
7719
- await this.collectAllDescendants(childId, collection);
7720
7669
  }
7670
+ // Get root node's children
7671
+ const rootNodes = await this.datasource();
7672
+ if (!rootNodes || rootNodes.length === 0) {
7673
+ return;
7674
+ }
7675
+ const rootNode = rootNodes[0];
7676
+ childNodes = rootNode['children'] || [];
7721
7677
  }
7722
- }
7723
- catch (error) {
7724
- console.error(`Error collecting descendants for node ${parentId}:`, error);
7725
- }
7726
- }
7727
- /**
7728
- * Deselects a node and recursively deselects all its children
7729
- */
7730
- async deselectNodeAndChildren(nodeId) {
7731
- const treeComponent = this.tree();
7732
- if (!treeComponent) {
7733
- return;
7734
- }
7735
- // Set flag to prevent recursive updates during batch operation
7736
- this.isUpdatingSelection = true;
7737
- try {
7738
- // Collect all nodes to deselect (node + all descendants)
7739
- const nodesToDeselect = new Set([nodeId]);
7740
- await this.collectAllDescendants(nodeId, nodesToDeselect);
7741
- // Batch update selectedNodeIds
7742
- const currentSelected = this.selectedNodeIds();
7743
- const newSelected = currentSelected.filter((id) => !nodesToDeselect.has(id));
7744
- this.selectedNodeIds.set(newSelected);
7745
- // Batch deselect in tree component (with small delays to prevent glitches)
7746
- for (const id of nodesToDeselect) {
7747
- try {
7748
- const node = treeComponent.findNode(id);
7749
- if (node) {
7750
- treeComponent.deselectNode(id);
7751
- // Small delay to prevent overwhelming the tree component
7752
- await new Promise((resolve) => setTimeout(resolve, 5));
7678
+ else {
7679
+ // Expand this parent node if tree component provided
7680
+ if (treeComponent) {
7681
+ try {
7682
+ treeComponent.expandNode(parentId);
7683
+ }
7684
+ catch {
7685
+ // Ignore expand errors
7753
7686
  }
7754
7687
  }
7755
- catch {
7756
- // Node might not be in tree
7757
- }
7688
+ // Load children via datasource
7689
+ childNodes = await this.datasource(parentId);
7758
7690
  }
7759
- }
7760
- finally {
7761
- this.isUpdatingSelection = false;
7762
- }
7763
- }
7764
- /**
7765
- * Recursively deselects all children of a parent node
7766
- */
7767
- async loadAndDeselectChildrenRecursively(parentId) {
7768
- if (!this.treeData || !this.treeConfig || !parentId || parentId === 'all') {
7769
- return;
7770
- }
7771
- const treeComponent = this.tree();
7772
- if (!treeComponent) {
7773
- return;
7774
- }
7775
- try {
7776
- // Load children using datasource
7777
- const childNodes = await this.datasource(parentId);
7778
7691
  if (!childNodes || childNodes.length === 0) {
7779
7692
  return;
7780
7693
  }
7781
- const childIdsToDeselect = [];
7782
- // Collect all child IDs
7694
+ // Process each child
7783
7695
  for (const childNode of childNodes) {
7784
7696
  const childId = String(childNode['id'] ?? '');
7785
- if (childId && childId !== 'all') {
7786
- childIdsToDeselect.push(childId);
7697
+ if (!childId || childId === 'all' || collection.has(childId)) {
7698
+ continue;
7787
7699
  }
7788
- }
7789
- // Remove children from selectedNodeIds
7790
- const currentSelected = this.selectedNodeIds();
7791
- const updatedSelected = currentSelected.filter((id) => !childIdsToDeselect.includes(id));
7792
- this.selectedNodeIds.set(updatedSelected);
7793
- // Deselect in tree component
7794
- for (const childId of childIdsToDeselect) {
7795
- try {
7796
- treeComponent.deselectNode(childId);
7700
+ // Cache node data
7701
+ this.cacheNodeFromTreeNode(childNode);
7702
+ // Check if this child has children
7703
+ const hasChildren = await this.nodeHasChildren(childId, childNode);
7704
+ if (!hasChildren) {
7705
+ // This is a LEAF node - add it
7706
+ collection.add(childId);
7797
7707
  }
7798
- catch {
7799
- // Node might not be in tree
7708
+ else {
7709
+ // Has children - expand and recurse (pass tree component to expand children too)
7710
+ await this.collectLeafNodes(childId, collection, treeComponent);
7800
7711
  }
7801
7712
  }
7802
- // Recursively deselect children of each child
7803
- await Promise.all(childIdsToDeselect.map((childId) => this.loadAndDeselectChildrenRecursively(childId)));
7804
7713
  }
7805
7714
  catch (error) {
7806
- console.error(`Error deselecting children for node ${parentId}:`, error);
7715
+ console.error(`Error collecting leaf nodes for ${parentId}:`, error);
7807
7716
  }
7808
7717
  }
7809
7718
  /**
7810
- * Updates parent states based on children selection (select/intermediate)
7811
- * Called after a node is selected/deselected to update parent checkbox states
7719
+ * Checks if a node has children
7812
7720
  */
7813
- async updateParentStates(changedNodeId) {
7814
- if (!this.treeData || !this.treeConfig || !this.allowMultiple()) {
7815
- return;
7816
- }
7817
- const parentKey = this.treeData.categoryEntityDef?.parentKey;
7818
- if (!parentKey) {
7819
- return; // No parent key means flat structure
7820
- }
7821
- const treeComponent = this.tree();
7822
- if (!treeComponent) {
7823
- return;
7824
- }
7825
- // Start from the changed node's parent and work up the tree
7826
- const processedParents = new Set();
7827
- let currentId = changedNodeId;
7828
- // Process all parents up to root
7829
- while (currentId && currentId !== 'all') {
7830
- const nodeData = this.nodeDataCache.get(currentId);
7831
- if (!nodeData) {
7832
- break;
7833
- }
7834
- const parentId = nodeData[parentKey];
7835
- const parentIdStr = String(parentId);
7836
- if (!parentId || parentId === 'all' || parentId === currentId || processedParents.has(parentIdStr)) {
7837
- break;
7721
+ async nodeHasChildren(nodeId, node) {
7722
+ // First check node properties if available
7723
+ if (node) {
7724
+ const children = node['children'];
7725
+ const childrenCount = node['childrenCount'];
7726
+ if (children && children.length > 0) {
7727
+ return true;
7838
7728
  }
7839
- processedParents.add(parentIdStr);
7840
- // Load all children of this parent
7841
- try {
7842
- const childNodes = await this.datasource(parentIdStr);
7843
- if (!childNodes || childNodes.length === 0) {
7844
- currentId = parentIdStr;
7845
- continue;
7846
- }
7847
- // Get current selection state (updated after each parent change)
7848
- const currentSelected = this.selectedNodeIds();
7849
- const selectedSet = new Set(currentSelected);
7850
- let selectedCount = 0;
7851
- let totalCount = 0;
7852
- // Count selected children
7853
- for (const childNode of childNodes) {
7854
- const childId = String(childNode['id'] ?? '');
7855
- if (childId && childId !== 'all') {
7856
- totalCount++;
7857
- if (selectedSet.has(childId)) {
7858
- selectedCount++;
7859
- }
7860
- }
7861
- }
7862
- // Update parent selection state
7863
- const isParentSelected = currentSelected.includes(parentIdStr);
7864
- if (totalCount > 0) {
7865
- if (selectedCount === totalCount) {
7866
- // All children selected - select parent
7867
- if (!isParentSelected) {
7868
- this.selectedNodeIds.set([...currentSelected, parentIdStr]);
7869
- try {
7870
- treeComponent.selectNode(parentIdStr);
7871
- }
7872
- catch {
7873
- // Parent might not be in tree
7874
- }
7875
- }
7876
- }
7877
- else if (selectedCount > 0) {
7878
- // Some children selected - parent should be in intermediate state
7879
- // The tree component handles intermediate state automatically via selectionBehavior
7880
- // We just need to ensure parent is not fully selected
7881
- if (isParentSelected) {
7882
- // Deselect parent to show intermediate state
7883
- this.selectedNodeIds.set(currentSelected.filter((id) => id !== parentIdStr));
7884
- try {
7885
- treeComponent.deselectNode(parentIdStr);
7886
- }
7887
- catch {
7888
- // Parent might not be in tree
7889
- }
7890
- }
7891
- }
7892
- else {
7893
- // No children selected - deselect parent
7894
- if (isParentSelected) {
7895
- this.selectedNodeIds.set(currentSelected.filter((id) => id !== parentIdStr));
7896
- try {
7897
- treeComponent.deselectNode(parentIdStr);
7898
- }
7899
- catch {
7900
- // Parent might not be in tree
7901
- }
7902
- }
7903
- }
7904
- }
7905
- // Cache parent data if not already cached
7906
- if (!this.nodeDataCache.has(parentIdStr)) {
7907
- const parentData = await this.fetchItemById(parentIdStr);
7908
- if (parentData) {
7909
- this.nodeDataCache.set(parentIdStr, parentData);
7910
- }
7911
- }
7912
- currentId = parentIdStr;
7729
+ if (childrenCount !== undefined && childrenCount > 0) {
7730
+ return true;
7913
7731
  }
7914
- catch (error) {
7915
- console.error(`Error updating parent state for ${parentIdStr}:`, error);
7916
- break;
7732
+ if (childrenCount === 0) {
7733
+ return false;
7917
7734
  }
7918
7735
  }
7736
+ // Query datasource to check
7737
+ const childNodes = await this.datasource(nodeId);
7738
+ return childNodes && childNodes.length > 0;
7919
7739
  }
7920
7740
  /**
7921
- * Recursively loads and selects all children of a parent node using the datasource
7922
- * This method directly calls datasource without expanding/collapsing nodes to avoid UI glitches
7741
+ * Checks if a node is a leaf (has no children)
7923
7742
  */
7924
- async loadAndSelectChildrenRecursively(parentId) {
7925
- if (!this.treeData || !this.treeConfig || !parentId || parentId === 'all') {
7926
- return;
7927
- }
7928
- const treeComponent = this.tree();
7929
- if (!treeComponent) {
7930
- return;
7931
- }
7932
- try {
7933
- // Directly call datasource to get children data without expanding the node
7934
- // This avoids UI glitches from expand/collapse operations
7935
- // If node has no children, datasource will return empty array
7936
- const childNodes = await this.datasource(parentId);
7937
- if (!childNodes || childNodes.length === 0) {
7938
- return; // No children to process
7939
- }
7940
- // Collect all child IDs to add to selectedNodeIds
7941
- const childIdsToSelect = [];
7942
- // Process all children and cache their data
7943
- for (const childNode of childNodes) {
7944
- const childId = String(childNode['id'] ?? '');
7945
- if (childId && childId !== 'all') {
7946
- childIdsToSelect.push(childId);
7947
- // Cache node data for getSelectedItems
7948
- // Try to get data from 'data' property first, then fallback to node itself
7949
- let nodeData = childNode['data'];
7950
- // If no data property, try to extract data from the node
7951
- if (!nodeData || typeof nodeData !== 'object') {
7952
- // Create a data object from the node properties
7953
- const valueField = this.treeConfig.valueField || 'id';
7954
- const textField = this.treeConfig.textField || 'title';
7955
- nodeData = {
7956
- [valueField]: childId,
7957
- [textField]: childNode['title'] ?? '',
7958
- ...childNode,
7959
- };
7960
- }
7961
- // Cache the node data
7962
- if (nodeData && typeof nodeData === 'object') {
7963
- this.nodeDataCache.set(childId, nodeData);
7964
- }
7965
- }
7966
- }
7967
- if (childIdsToSelect.length === 0) {
7968
- return; // No valid children to select
7969
- }
7970
- // Update selectedNodeIds to include all children
7971
- const currentSelected = this.selectedNodeIds();
7972
- const newSelected = [...currentSelected];
7973
- let hasNewSelections = false;
7974
- for (const childId of childIdsToSelect) {
7975
- if (!newSelected.includes(childId)) {
7976
- newSelected.push(childId);
7977
- hasNewSelections = true;
7978
- }
7979
- }
7980
- if (hasNewSelections) {
7981
- this.selectedNodeIds.set(newSelected);
7982
- }
7983
- // Try to select children in tree component if they're already loaded
7984
- // If not loaded yet, they'll be selected when the tree loads them (via markNodeAsSelectedIfNeeded)
7985
- for (const childId of childIdsToSelect) {
7986
- try {
7987
- // Only try to select if node exists in tree (might not be loaded if parent isn't expanded)
7988
- const node = treeComponent.findNode(childId);
7989
- if (node) {
7990
- treeComponent.selectNode(childId);
7991
- }
7992
- }
7993
- catch {
7994
- // If selection fails, it's okay - we've already added to selectedNodeIds
7995
- // The tree will sync selection when the node is loaded via datasource callback
7996
- }
7997
- }
7998
- // Recursively load and select children of each child
7999
- // Use Promise.all for parallel processing to improve performance
8000
- await Promise.all(childIdsToSelect.map((childId) => this.loadAndSelectChildrenRecursively(childId)));
8001
- }
8002
- catch (error) {
8003
- console.error(`Error loading children for node ${parentId}:`, error);
8004
- }
7743
+ async isLeafNodeCheck(nodeId) {
7744
+ const hasChildren = await this.nodeHasChildren(nodeId);
7745
+ return !hasChildren;
8005
7746
  }
8006
- //#endregion
8007
- async updateSelectedNodes(selectedIds) {
8008
- if (!selectedIds || selectedIds.length === 0) {
8009
- this.selectedNodeIds.set([]);
7747
+ /**
7748
+ * Caches node data from a tree node
7749
+ */
7750
+ cacheNodeFromTreeNode(node) {
7751
+ const nodeId = String(node['id'] ?? '');
7752
+ if (!nodeId || !this.treeConfig) {
8010
7753
  return;
8011
7754
  }
8012
- const ids = selectedIds.filter((id) => id && id !== 'all');
8013
- // Set flag to prevent recursive updates during initialization
8014
- this.isUpdatingSelection = true;
8015
- try {
8016
- // Step 1: Fetch node data for pre-selected items that aren't in the cache
8017
- await this.loadMissingNodeData(ids);
8018
- // Step 2: Build complete ancestor chains for all selected nodes
8019
- const ancestorChains = await this.buildAncestorChains(ids);
8020
- // Step 3: Set selected node IDs (these should be leaf nodes only)
8021
- this.selectedNodeIds.set(ids);
8022
- // Step 4: Expand ancestor nodes in order (root to leaf) to load them into tree
8023
- await this.expandAncestorNodesInOrder(ancestorChains);
8024
- // Step 5: Wait for tree to render and sync selection
8025
- await this.syncSelectionWithTree(ids);
8026
- }
8027
- finally {
8028
- this.isUpdatingSelection = false;
7755
+ let nodeData = node['data'];
7756
+ if (!nodeData || typeof nodeData !== 'object') {
7757
+ const valueField = this.treeConfig.valueField || 'id';
7758
+ const textField = this.treeConfig.textField || 'title';
7759
+ nodeData = {
7760
+ [valueField]: nodeId,
7761
+ [textField]: node['title'] ?? '',
7762
+ ...node,
7763
+ };
8029
7764
  }
7765
+ this.nodeDataCache.set(nodeId, nodeData);
8030
7766
  }
7767
+ //#endregion
8031
7768
  /**
8032
7769
  * Builds complete ancestor chains for all selected node IDs.
8033
7770
  * Returns a Map where key is the selected node ID and value is array of ancestor IDs from root to parent.
@@ -8408,42 +8145,38 @@ class AXPEntityCategoryTreeSelectorComponent extends AXBasePageComponent {
8408
8145
  const treeComponent = this.tree();
8409
8146
  if (treeComponent) {
8410
8147
  setTimeout(() => {
8411
- const selectedIds = this.selectedNodeIds();
8412
- selectedIds.forEach((id) => {
8413
- if (id && id !== 'all') {
8414
- treeComponent.selectNode(id);
8415
- }
8416
- });
8148
+ this.isUpdatingSelection = true;
8149
+ try {
8150
+ const selectedIds = this.selectedNodeIds();
8151
+ selectedIds.forEach((id) => {
8152
+ if (id && id !== 'all') {
8153
+ try {
8154
+ treeComponent.selectNode(id);
8155
+ }
8156
+ catch {
8157
+ // Node might not be in tree yet
8158
+ }
8159
+ }
8160
+ });
8161
+ }
8162
+ finally {
8163
+ this.isUpdatingSelection = false;
8164
+ }
8417
8165
  }, 0);
8418
8166
  }
8419
8167
  }
8420
8168
  /**
8421
- * Processes child nodes: marks excluded as disabled, marks selected, and syncs selection
8169
+ * Processes child nodes: marks excluded as disabled
8170
+ * Selection marking is handled in datasource callback ONLY during initial load
8422
8171
  */
8423
8172
  processChildNodes(childNodes) {
8424
8173
  const excludedId = this.excludedNodeId();
8425
- const selectedIds = this.selectedNodeIds();
8426
8174
  childNodes.forEach((node) => {
8427
8175
  const nodeId = String(node['id'] ?? '');
8428
8176
  if (excludedId && nodeId === excludedId) {
8429
8177
  node['disabled'] = true;
8430
8178
  }
8431
- if (nodeId && selectedIds.includes(nodeId)) {
8432
- node['selected'] = true;
8433
- }
8434
8179
  });
8435
- // Sync selection with tree component
8436
- const treeComponent = this.tree();
8437
- if (treeComponent) {
8438
- setTimeout(() => {
8439
- childNodes.forEach((node) => {
8440
- const nodeId = String(node['id'] ?? '');
8441
- if (nodeId && selectedIds.includes(nodeId) && nodeId !== 'all') {
8442
- treeComponent.selectNode(nodeId);
8443
- }
8444
- });
8445
- }, 0);
8446
- }
8447
8180
  }
8448
8181
  /**
8449
8182
  * Caches node data from items array
@@ -8587,67 +8320,6 @@ class AXPEntityCategoryTreeSelectorComponent extends AXBasePageComponent {
8587
8320
  const textField = this.treeConfig.textField || 'title';
8588
8321
  return String(nodeData[textField] ?? '');
8589
8322
  }
8590
- /**
8591
- * Checks if a node is a leaf node (has no children)
8592
- */
8593
- async isLeafNode(nodeId, treeComponent) {
8594
- if (!treeComponent) {
8595
- // If no tree component, check if node has children by querying
8596
- return await this.checkIfNodeHasChildren(nodeId);
8597
- }
8598
- try {
8599
- const node = treeComponent.findNode(nodeId);
8600
- if (!node) {
8601
- // Node not found in tree, check via query
8602
- return await this.checkIfNodeHasChildren(nodeId);
8603
- }
8604
- // Check if node has children
8605
- const children = node['children'];
8606
- const childrenCount = node['childrenCount'];
8607
- // If children are loaded, check the array
8608
- if (children !== undefined) {
8609
- return !children || children.length === 0;
8610
- }
8611
- // If childrenCount is available, use it
8612
- if (childrenCount !== undefined) {
8613
- return childrenCount === 0;
8614
- }
8615
- // If neither is available, try to check via query
8616
- return await this.checkIfNodeHasChildren(nodeId);
8617
- }
8618
- catch {
8619
- // If findNode fails, check via query
8620
- return await this.checkIfNodeHasChildren(nodeId);
8621
- }
8622
- }
8623
- /**
8624
- * Checks if a node has children by querying the data source
8625
- */
8626
- async checkIfNodeHasChildren(nodeId) {
8627
- if (!this.treeData?.categoryEntityQueryFunc || !this.treeConfig) {
8628
- return true; // Assume leaf if we can't check
8629
- }
8630
- try {
8631
- const parentKey = this.treeData.categoryEntityDef?.parentKey;
8632
- if (!parentKey) {
8633
- return true; // No parent key means flat structure, all nodes are leaves
8634
- }
8635
- const event = {
8636
- ...this.treeData.basicQueryEvent,
8637
- filter: {
8638
- field: parentKey,
8639
- value: nodeId,
8640
- operator: { type: 'equal' },
8641
- },
8642
- take: 1, // Only need to check if any children exist
8643
- };
8644
- const res = await this.treeData.categoryEntityQueryFunc(event);
8645
- return !res?.items || res.items.length === 0;
8646
- }
8647
- catch {
8648
- return true; // Assume leaf on error
8649
- }
8650
- }
8651
8323
  async getSelectedItems() {
8652
8324
  // selectedNodeIds now only contains LEAF nodes (already filtered)
8653
8325
  const selectedIds = this.selectedNodeIds();
@@ -8747,8 +8419,6 @@ class AXPEntityCategoryTreeSelectorComponent extends AXBasePageComponent {
8747
8419
  [titleField]="textField()"
8748
8420
  [idField]="valueField()"
8749
8421
  (onNodeSelect)="onNodeSelect($event)"
8750
- (onSelectionChange)="onSelectionChange($event)"
8751
- (onNodeToggle)="onNodeToggle($event)"
8752
8422
  [nodeTemplate]="itemTemplate"
8753
8423
  #tree
8754
8424
  >
@@ -8878,8 +8548,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
8878
8548
  [titleField]="textField()"
8879
8549
  [idField]="valueField()"
8880
8550
  (onNodeSelect)="onNodeSelect($event)"
8881
- (onSelectionChange)="onSelectionChange($event)"
8882
- (onNodeToggle)="onNodeToggle($event)"
8883
8551
  [nodeTemplate]="itemTemplate"
8884
8552
  #tree
8885
8553
  >
@@ -10742,6 +10410,7 @@ class AXPEntityListWidgetViewComponent extends AXPValueWidgetComponent {
10742
10410
  this.deviceService = inject(AXPDeviceService);
10743
10411
  this.commandService = inject(AXPCommandService);
10744
10412
  this.eventService = inject(AXPBroadcastEventService);
10413
+ this.expressionEvaluator = inject(AXPExpressionEvaluatorService);
10745
10414
  this.isMounted = signal(false, ...(ngDevMode ? [{ debugName: "isMounted" }] : []));
10746
10415
  this.entity = signal(null, ...(ngDevMode ? [{ debugName: "entity" }] : []));
10747
10416
  this.listNode = signal(null, ...(ngDevMode ? [{ debugName: "listNode" }] : []));
@@ -10870,7 +10539,32 @@ class AXPEntityListWidgetViewComponent extends AXPValueWidgetComponent {
10870
10539
  c.scope == AXPEntityCommandScope.TypeLevel));
10871
10540
  });
10872
10541
  const command = commandName.split('&')[0];
10873
- // const options = await this.evaluateExpressions(action?.options, data);
10542
+ // Get current context from contextService for lazy evaluation
10543
+ const currentContext = this.contextService.initial();
10544
+ console.log('currentContext', currentContext);
10545
+ // Evaluate action options with current context if they contain expressions
10546
+ let evaluatedOptions = action?.options;
10547
+ if (action?.options && currentContext) {
10548
+ try {
10549
+ const scope = {
10550
+ context: {
10551
+ eval: (path) => {
10552
+ console.log({ path });
10553
+ return get(currentContext, path);
10554
+ },
10555
+ },
10556
+ };
10557
+ evaluatedOptions = await this.expressionEvaluator.evaluate(action.options, scope);
10558
+ }
10559
+ catch {
10560
+ // Keep original options if evaluation fails
10561
+ evaluatedOptions = action?.options;
10562
+ }
10563
+ }
10564
+ debugger;
10565
+ const actionData = action?.scope == AXPEntityCommandScope.Selected
10566
+ ? this.selectedItems()
10567
+ : evaluatedOptions?.['process']?.data || null;
10874
10568
  if (this.commandService.exists(command)) {
10875
10569
  await this.commandService.execute(command, {
10876
10570
  __context__: {
@@ -10882,10 +10576,8 @@ class AXPEntityListWidgetViewComponent extends AXPValueWidgetComponent {
10882
10576
  parentKey: this.entity()?.parentKey,
10883
10577
  source: `${this.entity()?.module}.${this.entity()?.name}`,
10884
10578
  },
10885
- data: action?.scope == AXPEntityCommandScope.Selected
10886
- ? this.selectedItems()
10887
- : action?.options?.['process']?.data || null,
10888
- options: action?.options,
10579
+ data: actionData,
10580
+ options: evaluatedOptions,
10889
10581
  metadata: action?.metadata,
10890
10582
  },
10891
10583
  });
@@ -10900,10 +10592,8 @@ class AXPEntityListWidgetViewComponent extends AXPValueWidgetComponent {
10900
10592
  parentKey: this.entity()?.parentKey,
10901
10593
  source: `${this.entity()?.module}.${this.entity()?.name}`,
10902
10594
  },
10903
- data: action?.scope == AXPEntityCommandScope.Selected
10904
- ? this.selectedItems()
10905
- : action?.options?.['process']?.data || null,
10906
- options: action?.options,
10595
+ data: actionData,
10596
+ options: evaluatedOptions,
10907
10597
  metadata: action?.metadata,
10908
10598
  });
10909
10599
  }
@@ -12401,7 +12091,7 @@ class AXPLookupWidgetEditComponent extends AXPValueWidgetComponent {
12401
12091
  //#region ---- Computed Properties ----
12402
12092
  this.expose = computed(() => this.options()['expose'], ...(ngDevMode ? [{ debugName: "expose" }] : []));
12403
12093
  this.entity = computed(() => this.options()['entity'], ...(ngDevMode ? [{ debugName: "entity" }] : []));
12404
- this.disabled = computed(() => this.options()['disabled'], ...(ngDevMode ? [{ debugName: "disabled" }] : []));
12094
+ this.disabled = computed(() => this.filterMode() ? false : this.options()['disabled'], ...(ngDevMode ? [{ debugName: "disabled" }] : []));
12405
12095
  this.columns = computed(() => this.options()['columns'] ?? [], ...(ngDevMode ? [{ debugName: "columns" }] : []));
12406
12096
  this.textField = computed(() => this.options()['textField'] ?? '', ...(ngDevMode ? [{ debugName: "textField" }] : []));
12407
12097
  this.hasClearButton = computed(() => this.options()['hasClearButton'] ?? false, ...(ngDevMode ? [{ debugName: "hasClearButton" }] : []));
@@ -12485,7 +12175,7 @@ class AXPLookupWidgetEditComponent extends AXPValueWidgetComponent {
12485
12175
  value: newValue,
12486
12176
  displayText: text,
12487
12177
  operation: {
12488
- type: this.multiple() ? 'in' : 'contains',
12178
+ type: this.multiple() ? 'in' : 'equal',
12489
12179
  },
12490
12180
  });
12491
12181
  }
@@ -15043,6 +14733,7 @@ function entityDetailsCreateActions(parentId) {
15043
14733
  canCreateNewOne: true,
15044
14734
  data: {
15045
14735
  [parentId]: '{{context.eval("id")}}',
14736
+ notes: '{{context.eval("displayName")}}',
15046
14737
  },
15047
14738
  },
15048
14739
  },