@acorex/platform 20.7.8 → 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' }); }
@@ -6640,11 +6627,22 @@ class AXPCategoryTreeService {
6640
6627
  convertToTreeNode(item, config) {
6641
6628
  const textField = config.textField ?? 'title';
6642
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
+ }
6643
6641
  return {
6644
6642
  id: String(item[valueField] ?? ''),
6645
6643
  title: String(item[textField] ?? ''),
6646
6644
  icon: 'fa-solid fa-folder',
6647
- childrenCount: item['childrenCount'] || item['children']?.length || 0,
6645
+ childrenCount,
6648
6646
  expanded: false,
6649
6647
  data: item,
6650
6648
  };
@@ -6800,7 +6798,6 @@ class AXPEntityCategoryTreeSelectorComponent extends AXBasePageComponent {
6800
6798
  this.relevantNodeIds = new Set(); // For search filtering
6801
6799
  this.expandedNodesBeforeSearch = []; // Store expanded nodes before search to restore after
6802
6800
  this.nodesExpandedDuringSearch = []; // Track nodes we expanded during search
6803
- this.isInitializing = false; // Flag to track if we're in initialization phase
6804
6801
  /** Datasource callback for tree-view component. */
6805
6802
  this.datasource = async (id) => {
6806
6803
  if (!this.treeData || !this.treeConfig) {
@@ -6847,31 +6844,48 @@ class AXPEntityCategoryTreeSelectorComponent extends AXBasePageComponent {
6847
6844
  }
6848
6845
  });
6849
6846
  }
6850
- // Mark pre-selected leaf nodes as selected in the child nodes
6851
- const selectedIds = this.selectedNodeIds();
6852
- childNodes.forEach((node) => {
6853
- const nodeId = String(node['id'] ?? '');
6854
- if (nodeId && selectedIds.includes(nodeId)) {
6855
- node['selected'] = true;
6856
- }
6857
- });
6858
- // After children load, programmatically select pre-selected nodes in tree component
6859
- const treeComponent = this.tree();
6860
- if (treeComponent && selectedIds.length > 0) {
6861
- // Use setTimeout to ensure nodes are in tree structure before selecting
6862
- setTimeout(() => {
6863
- childNodes.forEach((node) => {
6864
- const nodeId = String(node['id'] ?? '');
6865
- if (nodeId && selectedIds.includes(nodeId) && nodeId !== 'all') {
6866
- try {
6867
- treeComponent.selectNode(nodeId);
6868
- }
6869
- catch {
6870
- // Node might not be in tree yet
6871
- }
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;
6872
6867
  }
6873
- });
6874
- }, 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
+ }
6875
6889
  }
6876
6890
  return childNodes;
6877
6891
  };
@@ -6890,7 +6904,6 @@ class AXPEntityCategoryTreeSelectorComponent extends AXBasePageComponent {
6890
6904
  return;
6891
6905
  }
6892
6906
  this.loading.set(true);
6893
- this.isInitializing = true; // Mark that we're in initialization phase
6894
6907
  try {
6895
6908
  this.treeConfig = {
6896
6909
  entityKey: this.entityKey(),
@@ -6900,7 +6913,6 @@ class AXPEntityCategoryTreeSelectorComponent extends AXBasePageComponent {
6900
6913
  this.treeData = await this.categoryTreeService.initializeCategoryTree(this.treeConfig);
6901
6914
  if (!this.treeData) {
6902
6915
  this.loading.set(false);
6903
- this.isInitializing = false;
6904
6916
  return;
6905
6917
  }
6906
6918
  // Get parentKey from entity definition
@@ -6946,7 +6958,6 @@ class AXPEntityCategoryTreeSelectorComponent extends AXBasePageComponent {
6946
6958
  console.error('Error syncing selection after tree render:', error);
6947
6959
  }
6948
6960
  }
6949
- this.isInitializing = false; // Mark initialization as complete
6950
6961
  }
6951
6962
  //#endregion
6952
6963
  //#region ---- Public Methods ----
@@ -7368,6 +7379,8 @@ class AXPEntityCategoryTreeSelectorComponent extends AXBasePageComponent {
7368
7379
  this.nodesExpandedDuringSearch = [];
7369
7380
  return;
7370
7381
  }
7382
+ // Store current selected IDs before reload
7383
+ const selectedIds = this.selectedNodeIds();
7371
7384
  // Reload tree to show all nodes (no filtering)
7372
7385
  await treeComponent.reloadData();
7373
7386
  // Collapse nodes that were expanded during search (in reverse order - leaves first)
@@ -7386,88 +7399,136 @@ class AXPEntityCategoryTreeSelectorComponent extends AXBasePageComponent {
7386
7399
  }
7387
7400
  // Clear the stored expanded nodes
7388
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
+ }
7389
7409
  }
7390
- async onNodeToggle(event) {
7391
- // 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
+ }
7392
7440
  }
7393
7441
  async onNodeSelect(event) {
7394
7442
  const node = event.node;
7395
7443
  const nodeId = String(node['id'] ?? '');
7396
- if (!node || nodeId === 'all') {
7444
+ if (!node) {
7397
7445
  return;
7398
7446
  }
7399
- // Cache node data for getSelectedItems
7400
- const nodeData = node['data'];
7401
- if (nodeData && typeof nodeData === 'object' && nodeData !== null && !Array.isArray(nodeData)) {
7402
- 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;
7403
7451
  }
7404
- // NOTE: We do NOT call loadAndSelectChildrenRecursively here
7405
- // The recursive selection should only happen when user explicitly selects a node via checkbox
7406
- // (handled in handleCheckboxChange). This prevents intermediate parent states from
7407
- // triggering unwanted sibling selections.
7408
- }
7409
- async onSelectionChange(event) {
7410
- // Don't process during initialization or batch updates - let those handle it
7411
- if (this.isInitializing || this.isUpdatingSelection) {
7452
+ // Don't process if we're already updating selection
7453
+ if (this.isUpdatingSelection) {
7412
7454
  return;
7413
7455
  }
7414
- // The tree component fires selection changes, but we only want to track LEAF nodes
7415
- // Parent nodes are auto-selected/deselected by the tree's intermediate-nested behavior
7416
- const selectedNodes = event.selectedNodes || [];
7417
- // Cache node data for all selected nodes first
7418
- selectedNodes.forEach((node) => {
7419
- const nodeId = String(node['id'] ?? '');
7456
+ // Cache node data for getSelectedItems (except for 'all' node)
7457
+ if (nodeId !== 'all') {
7420
7458
  const nodeData = node['data'];
7421
- if (nodeData && nodeId && typeof nodeData === 'object') {
7459
+ if (nodeData && typeof nodeData === 'object' && nodeData !== null && !Array.isArray(nodeData)) {
7422
7460
  this.nodeDataCache.set(nodeId, nodeData);
7423
7461
  }
7424
- });
7425
- // Filter to only leaf nodes (nodes with no children or childrenCount === 0)
7426
- const leafNodeIds = [];
7427
- for (const node of selectedNodes) {
7428
- const nodeId = String(node['id'] ?? '');
7429
- if (!nodeId || nodeId === 'all') {
7430
- continue;
7431
- }
7432
- // 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()) {
7433
7466
  const children = node['children'];
7434
7467
  const childrenCount = node['childrenCount'];
7435
- const isLeaf = (!children || children.length === 0) && (childrenCount === undefined || childrenCount === 0);
7436
- if (isLeaf) {
7437
- leafNodeIds.push(nodeId);
7438
- }
7439
- }
7440
- // Update selectedNodeIds with only leaf nodes
7441
- this.selectedNodeIds.set(leafNodeIds);
7442
- }
7443
- // protected handleNodeClick(event: any): void {
7444
- // // Extract node from event - could be { node: AXTreeNode } or just AXTreeNode
7445
- // const node: AXTreeViewNode = event?.node || event;
7446
- // if (!node || node.id === 'all') {
7447
- // return;
7448
- // }
7449
- // // Cache node data for getSelectedItems
7450
- // if (node.data) {
7451
- // this.nodeDataCache.set(node.id, node.data);
7452
- // }
7453
- // if (this.allowMultiple()) {
7454
- // // In multiple mode with checkboxes, clicking the node should toggle the checkbox
7455
- // // The checkbox state is handled by onNodeSelect event, so we just sync the selection here
7456
- // const currentIds = this.selectedNodeIds();
7457
- // const isSelected = currentIds.includes(node.id);
7458
- // // Toggle selection state
7459
- // // Note: The actual checkbox toggle is handled by the tree component's onNodeSelect event
7460
- // if (isSelected) {
7461
- // this.selectedNodeIds.set(currentIds.filter((id) => id !== node.id));
7462
- // } else {
7463
- // this.selectedNodeIds.set([...currentIds, node.id]);
7464
- // }
7465
- // } else {
7466
- // // Single selection - auto-confirm on click
7467
- // this.selectedNodeIds.set([node.id]);
7468
- // this.close({ selected: this.getSelectedItems() });
7469
- // }
7470
- // }
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
+ }
7471
7532
  async onConfirm() {
7472
7533
  // Use internal selectedNodeIds state which is kept in sync via onNodeSelect events
7473
7534
  const selectedItems = await this.getSelectedItems();
@@ -7480,21 +7541,11 @@ class AXPEntityCategoryTreeSelectorComponent extends AXBasePageComponent {
7480
7541
  * Clears all selected items
7481
7542
  */
7482
7543
  onClearAll() {
7544
+ this.selectedNodeIds.set([]);
7483
7545
  const treeComponent = this.tree();
7484
7546
  if (treeComponent) {
7485
- // Deselect all nodes in tree component
7486
- const currentSelected = this.selectedNodeIds();
7487
- for (const id of currentSelected) {
7488
- try {
7489
- treeComponent.deselectNode(id);
7490
- }
7491
- catch {
7492
- // Node might not be in tree
7493
- }
7494
- }
7547
+ treeComponent.deselectAll();
7495
7548
  }
7496
- // Clear the selection
7497
- this.selectedNodeIds.set([]);
7498
7549
  }
7499
7550
  /**
7500
7551
  * Checks if a node matches the current search term
@@ -7513,543 +7564,207 @@ class AXPEntityCategoryTreeSelectorComponent extends AXBasePageComponent {
7513
7564
  return this.selectedNodeIds().includes(id);
7514
7565
  }
7515
7566
  /**
7516
- * Handles checkbox change event to toggle node selection
7517
- * In multiple mode: recursively selects/deselects LEAF children only
7518
- * 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.
7519
7569
  */
7520
- async handleCheckboxChange(nodeId, checked) {
7521
- if (!nodeId || nodeId === 'all') {
7570
+ async selectAllLeafDescendants(parentId) {
7571
+ if (!this.treeData || !this.treeConfig) {
7522
7572
  return;
7523
7573
  }
7524
- const id = String(nodeId);
7525
7574
  const treeComponent = this.tree();
7526
- if (!treeComponent) {
7527
- return;
7528
- }
7529
- if (checked) {
7530
- // Select all descendant LEAF nodes
7531
- await this.selectLeafDescendants(id);
7532
- }
7533
- else {
7534
- // Deselect all descendant LEAF nodes
7535
- await this.deselectLeafDescendants(id);
7536
- }
7537
- // Update parent states after selection change
7538
- await this.refreshParentStatesInTree();
7539
- }
7540
- /**
7541
- * Selects all leaf descendants of a node (and the node itself if it's a leaf)
7542
- */
7543
- async selectLeafDescendants(nodeId) {
7544
- this.isUpdatingSelection = true;
7545
7575
  try {
7546
- // Collect all leaf descendants
7547
- const leafNodes = new Set();
7548
- await this.collectLeafDescendants(nodeId, leafNodes);
7549
- if (leafNodes.size === 0) {
7550
- // Node itself is a leaf
7551
- leafNodes.add(nodeId);
7552
- }
7553
- // Update selectedNodeIds with leaf nodes only
7554
- const currentSelected = new Set(this.selectedNodeIds());
7555
- leafNodes.forEach((id) => currentSelected.add(id));
7556
- this.selectedNodeIds.set(Array.from(currentSelected));
7557
- // Sync with tree component
7558
- const treeComponent = this.tree();
7559
- if (treeComponent) {
7560
- for (const leafId of leafNodes) {
7561
- try {
7562
- const node = treeComponent.findNode(leafId);
7563
- if (node) {
7564
- treeComponent.selectNode(leafId);
7565
- }
7566
- }
7567
- catch {
7568
- // Node might not be in tree
7569
- }
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);
7570
7584
  }
7571
7585
  }
7572
- }
7573
- finally {
7574
- this.isUpdatingSelection = false;
7575
- }
7576
- }
7577
- /**
7578
- * Deselects all leaf descendants of a node (and the node itself if it's a leaf)
7579
- */
7580
- async deselectLeafDescendants(nodeId) {
7581
- this.isUpdatingSelection = true;
7582
- try {
7583
- // Collect all leaf descendants
7584
- const leafNodes = new Set();
7585
- await this.collectLeafDescendants(nodeId, leafNodes);
7586
- if (leafNodes.size === 0) {
7587
- // Node itself is a leaf
7588
- leafNodes.add(nodeId);
7589
- }
7590
- // Remove leaf nodes from selectedNodeIds
7591
- const currentSelected = this.selectedNodeIds();
7592
- const newSelected = currentSelected.filter((id) => !leafNodes.has(id));
7593
- this.selectedNodeIds.set(newSelected);
7594
- // Sync with tree component
7595
- const treeComponent = this.tree();
7586
+ if (leafIds.size === 0) {
7587
+ return;
7588
+ }
7589
+ // Update our internal state
7590
+ const currentSelected = new Set(this.selectedNodeIds());
7591
+ for (const leafId of leafIds) {
7592
+ currentSelected.add(leafId);
7593
+ }
7594
+ this.selectedNodeIds.set(Array.from(currentSelected));
7595
+ // Select all leaf nodes visually in the tree
7596
7596
  if (treeComponent) {
7597
- for (const leafId of leafNodes) {
7597
+ for (const leafId of leafIds) {
7598
7598
  try {
7599
- const node = treeComponent.findNode(leafId);
7600
- if (node) {
7601
- treeComponent.deselectNode(leafId);
7602
- }
7599
+ treeComponent.selectNode(leafId);
7603
7600
  }
7604
7601
  catch {
7605
- // Node might not be in tree
7602
+ // Node might not be in tree yet
7606
7603
  }
7607
7604
  }
7608
7605
  }
7609
7606
  }
7610
- finally {
7611
- this.isUpdatingSelection = false;
7607
+ catch (error) {
7608
+ console.error(`Error selecting leaf descendants for node ${parentId}:`, error);
7612
7609
  }
7613
7610
  }
7614
7611
  /**
7615
- * Collects all LEAF descendant node IDs recursively
7616
- * 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.
7617
7615
  */
7618
- async collectLeafDescendants(parentId, collection) {
7619
- if (!this.treeData || !this.treeConfig || !parentId || parentId === 'all') {
7616
+ async deselectAllLeafDescendants(parentId) {
7617
+ if (!this.treeData || !this.treeConfig) {
7620
7618
  return;
7621
7619
  }
7622
7620
  try {
7623
- const childNodes = await this.datasource(parentId);
7624
- if (!childNodes || childNodes.length === 0) {
7625
- // No children means this node is a leaf - add the parent itself
7626
- // 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
+ }
7627
7629
  return;
7628
7630
  }
7629
- for (const childNode of childNodes) {
7630
- const childId = String(childNode['id'] ?? '');
7631
- if (!childId || childId === 'all') {
7632
- continue;
7633
- }
7634
- // Cache node data
7635
- const nodeData = childNode['data'];
7636
- if (nodeData && typeof nodeData === 'object') {
7637
- this.nodeDataCache.set(childId, nodeData);
7638
- }
7639
- // Check if this child is a leaf by trying to load its children
7640
- const grandchildNodes = await this.datasource(childId);
7641
- if (!grandchildNodes || grandchildNodes.length === 0) {
7642
- // This child is a leaf node
7643
- collection.add(childId);
7644
- }
7645
- else {
7646
- // This child has children, recurse
7647
- await this.collectLeafDescendants(childId, collection);
7648
- }
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);
7649
7637
  }
7650
- }
7651
- catch (error) {
7652
- console.error(`Error collecting leaf descendants for node ${parentId}:`, error);
7653
- }
7654
- }
7655
- /**
7656
- * Refreshes parent states in the tree based on leaf selection
7657
- */
7658
- async refreshParentStatesInTree() {
7659
- const treeComponent = this.tree();
7660
- if (!treeComponent) {
7661
- return;
7662
- }
7663
- // The tree component with intermediate-nested behavior should handle this
7664
- // But we need to force a refresh by reloading data
7665
- // For now, we rely on the tree component's built-in behavior
7666
- }
7667
- /**
7668
- * Selects a node and recursively selects all its children
7669
- */
7670
- async selectNodeAndChildren(nodeId) {
7671
- const treeComponent = this.tree();
7672
- if (!treeComponent) {
7673
- return;
7674
- }
7675
- // Set flag to prevent recursive updates during batch operation
7676
- this.isUpdatingSelection = true;
7677
- try {
7678
- // Collect all nodes to select (node + all descendants)
7679
- const nodesToSelect = new Set([nodeId]);
7680
- await this.collectAllDescendants(nodeId, nodesToSelect);
7681
- // Batch update selectedNodeIds
7682
- const currentSelected = this.selectedNodeIds();
7683
- const newSelected = [...currentSelected];
7684
- for (const id of nodesToSelect) {
7685
- if (!newSelected.includes(id)) {
7686
- newSelected.push(id);
7687
- }
7638
+ if (leafIds.size === 0) {
7639
+ return;
7688
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));
7689
7644
  this.selectedNodeIds.set(newSelected);
7690
- // Batch select in tree component (with small delays to prevent glitches)
7691
- for (const id of nodesToSelect) {
7692
- try {
7693
- const node = treeComponent.findNode(id);
7694
- if (node) {
7695
- treeComponent.selectNode(id);
7696
- // Small delay to prevent overwhelming the tree component
7697
- await new Promise((resolve) => setTimeout(resolve, 5));
7698
- }
7699
- }
7700
- catch {
7701
- // Node might not be in tree yet
7702
- }
7703
- }
7704
7645
  }
7705
- finally {
7706
- this.isUpdatingSelection = false;
7646
+ catch (error) {
7647
+ console.error(`Error deselecting leaf descendants for node ${parentId}:`, error);
7707
7648
  }
7708
7649
  }
7709
7650
  /**
7710
- * 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.
7711
7653
  */
7712
- async collectAllDescendants(parentId, collection) {
7713
- if (!this.treeData || !this.treeConfig || !parentId || parentId === 'all') {
7654
+ async collectLeafNodes(parentId, collection, treeComponent) {
7655
+ if (!this.treeData || !this.treeConfig) {
7714
7656
  return;
7715
7657
  }
7716
7658
  try {
7717
- const childNodes = await this.datasource(parentId);
7718
- if (!childNodes || childNodes.length === 0) {
7719
- return;
7720
- }
7721
- for (const childNode of childNodes) {
7722
- const childId = String(childNode['id'] ?? '');
7723
- if (childId && childId !== 'all' && !collection.has(childId)) {
7724
- collection.add(childId);
7725
- // Cache node data
7726
- const nodeData = childNode['data'];
7727
- if (nodeData && typeof nodeData === 'object') {
7728
- 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');
7729
7665
  }
7730
- else if (childNode && typeof childNode === 'object') {
7731
- const valueField = this.treeConfig.valueField || 'id';
7732
- const textField = this.treeConfig.textField || 'title';
7733
- const dataObj = {
7734
- [valueField]: childId,
7735
- [textField]: childNode['title'] ?? '',
7736
- ...childNode,
7737
- };
7738
- this.nodeDataCache.set(childId, dataObj);
7666
+ catch {
7667
+ // Ignore expand errors
7739
7668
  }
7740
- // Recursively collect descendants
7741
- await this.collectAllDescendants(childId, collection);
7742
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'] || [];
7743
7677
  }
7744
- }
7745
- catch (error) {
7746
- console.error(`Error collecting descendants for node ${parentId}:`, error);
7747
- }
7748
- }
7749
- /**
7750
- * Deselects a node and recursively deselects all its children
7751
- */
7752
- async deselectNodeAndChildren(nodeId) {
7753
- const treeComponent = this.tree();
7754
- if (!treeComponent) {
7755
- return;
7756
- }
7757
- // Set flag to prevent recursive updates during batch operation
7758
- this.isUpdatingSelection = true;
7759
- try {
7760
- // Collect all nodes to deselect (node + all descendants)
7761
- const nodesToDeselect = new Set([nodeId]);
7762
- await this.collectAllDescendants(nodeId, nodesToDeselect);
7763
- // Batch update selectedNodeIds
7764
- const currentSelected = this.selectedNodeIds();
7765
- const newSelected = currentSelected.filter((id) => !nodesToDeselect.has(id));
7766
- this.selectedNodeIds.set(newSelected);
7767
- // Batch deselect in tree component (with small delays to prevent glitches)
7768
- for (const id of nodesToDeselect) {
7769
- try {
7770
- const node = treeComponent.findNode(id);
7771
- if (node) {
7772
- treeComponent.deselectNode(id);
7773
- // Small delay to prevent overwhelming the tree component
7774
- 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
7775
7686
  }
7776
7687
  }
7777
- catch {
7778
- // Node might not be in tree
7779
- }
7688
+ // Load children via datasource
7689
+ childNodes = await this.datasource(parentId);
7780
7690
  }
7781
- }
7782
- finally {
7783
- this.isUpdatingSelection = false;
7784
- }
7785
- }
7786
- /**
7787
- * Recursively deselects all children of a parent node
7788
- */
7789
- async loadAndDeselectChildrenRecursively(parentId) {
7790
- if (!this.treeData || !this.treeConfig || !parentId || parentId === 'all') {
7791
- return;
7792
- }
7793
- const treeComponent = this.tree();
7794
- if (!treeComponent) {
7795
- return;
7796
- }
7797
- try {
7798
- // Load children using datasource
7799
- const childNodes = await this.datasource(parentId);
7800
7691
  if (!childNodes || childNodes.length === 0) {
7801
7692
  return;
7802
7693
  }
7803
- const childIdsToDeselect = [];
7804
- // Collect all child IDs
7694
+ // Process each child
7805
7695
  for (const childNode of childNodes) {
7806
7696
  const childId = String(childNode['id'] ?? '');
7807
- if (childId && childId !== 'all') {
7808
- childIdsToDeselect.push(childId);
7697
+ if (!childId || childId === 'all' || collection.has(childId)) {
7698
+ continue;
7809
7699
  }
7810
- }
7811
- // Remove children from selectedNodeIds
7812
- const currentSelected = this.selectedNodeIds();
7813
- const updatedSelected = currentSelected.filter((id) => !childIdsToDeselect.includes(id));
7814
- this.selectedNodeIds.set(updatedSelected);
7815
- // Deselect in tree component
7816
- for (const childId of childIdsToDeselect) {
7817
- try {
7818
- 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);
7819
7707
  }
7820
- catch {
7821
- // 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);
7822
7711
  }
7823
7712
  }
7824
- // Recursively deselect children of each child
7825
- await Promise.all(childIdsToDeselect.map((childId) => this.loadAndDeselectChildrenRecursively(childId)));
7826
7713
  }
7827
7714
  catch (error) {
7828
- console.error(`Error deselecting children for node ${parentId}:`, error);
7715
+ console.error(`Error collecting leaf nodes for ${parentId}:`, error);
7829
7716
  }
7830
7717
  }
7831
7718
  /**
7832
- * Updates parent states based on children selection (select/intermediate)
7833
- * Called after a node is selected/deselected to update parent checkbox states
7719
+ * Checks if a node has children
7834
7720
  */
7835
- async updateParentStates(changedNodeId) {
7836
- if (!this.treeData || !this.treeConfig || !this.allowMultiple()) {
7837
- return;
7838
- }
7839
- const parentKey = this.treeData.categoryEntityDef?.parentKey;
7840
- if (!parentKey) {
7841
- return; // No parent key means flat structure
7842
- }
7843
- const treeComponent = this.tree();
7844
- if (!treeComponent) {
7845
- return;
7846
- }
7847
- // Start from the changed node's parent and work up the tree
7848
- const processedParents = new Set();
7849
- let currentId = changedNodeId;
7850
- // Process all parents up to root
7851
- while (currentId && currentId !== 'all') {
7852
- const nodeData = this.nodeDataCache.get(currentId);
7853
- if (!nodeData) {
7854
- break;
7855
- }
7856
- const parentId = nodeData[parentKey];
7857
- const parentIdStr = String(parentId);
7858
- if (!parentId || parentId === 'all' || parentId === currentId || processedParents.has(parentIdStr)) {
7859
- 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;
7860
7728
  }
7861
- processedParents.add(parentIdStr);
7862
- // Load all children of this parent
7863
- try {
7864
- const childNodes = await this.datasource(parentIdStr);
7865
- if (!childNodes || childNodes.length === 0) {
7866
- currentId = parentIdStr;
7867
- continue;
7868
- }
7869
- // Get current selection state (updated after each parent change)
7870
- const currentSelected = this.selectedNodeIds();
7871
- const selectedSet = new Set(currentSelected);
7872
- let selectedCount = 0;
7873
- let totalCount = 0;
7874
- // Count selected children
7875
- for (const childNode of childNodes) {
7876
- const childId = String(childNode['id'] ?? '');
7877
- if (childId && childId !== 'all') {
7878
- totalCount++;
7879
- if (selectedSet.has(childId)) {
7880
- selectedCount++;
7881
- }
7882
- }
7883
- }
7884
- // Update parent selection state
7885
- const isParentSelected = currentSelected.includes(parentIdStr);
7886
- if (totalCount > 0) {
7887
- if (selectedCount === totalCount) {
7888
- // All children selected - select parent
7889
- if (!isParentSelected) {
7890
- this.selectedNodeIds.set([...currentSelected, parentIdStr]);
7891
- try {
7892
- treeComponent.selectNode(parentIdStr);
7893
- }
7894
- catch {
7895
- // Parent might not be in tree
7896
- }
7897
- }
7898
- }
7899
- else if (selectedCount > 0) {
7900
- // Some children selected - parent should be in intermediate state
7901
- // The tree component handles intermediate state automatically via selectionBehavior
7902
- // We just need to ensure parent is not fully selected
7903
- if (isParentSelected) {
7904
- // Deselect parent to show intermediate state
7905
- this.selectedNodeIds.set(currentSelected.filter((id) => id !== parentIdStr));
7906
- try {
7907
- treeComponent.deselectNode(parentIdStr);
7908
- }
7909
- catch {
7910
- // Parent might not be in tree
7911
- }
7912
- }
7913
- }
7914
- else {
7915
- // No children selected - deselect parent
7916
- if (isParentSelected) {
7917
- this.selectedNodeIds.set(currentSelected.filter((id) => id !== parentIdStr));
7918
- try {
7919
- treeComponent.deselectNode(parentIdStr);
7920
- }
7921
- catch {
7922
- // Parent might not be in tree
7923
- }
7924
- }
7925
- }
7926
- }
7927
- // Cache parent data if not already cached
7928
- if (!this.nodeDataCache.has(parentIdStr)) {
7929
- const parentData = await this.fetchItemById(parentIdStr);
7930
- if (parentData) {
7931
- this.nodeDataCache.set(parentIdStr, parentData);
7932
- }
7933
- }
7934
- currentId = parentIdStr;
7729
+ if (childrenCount !== undefined && childrenCount > 0) {
7730
+ return true;
7935
7731
  }
7936
- catch (error) {
7937
- console.error(`Error updating parent state for ${parentIdStr}:`, error);
7938
- break;
7732
+ if (childrenCount === 0) {
7733
+ return false;
7939
7734
  }
7940
7735
  }
7736
+ // Query datasource to check
7737
+ const childNodes = await this.datasource(nodeId);
7738
+ return childNodes && childNodes.length > 0;
7941
7739
  }
7942
7740
  /**
7943
- * Recursively loads and selects all children of a parent node using the datasource
7944
- * This method directly calls datasource without expanding/collapsing nodes to avoid UI glitches
7741
+ * Checks if a node is a leaf (has no children)
7945
7742
  */
7946
- async loadAndSelectChildrenRecursively(parentId) {
7947
- if (!this.treeData || !this.treeConfig || !parentId || parentId === 'all') {
7948
- return;
7949
- }
7950
- const treeComponent = this.tree();
7951
- if (!treeComponent) {
7952
- return;
7953
- }
7954
- try {
7955
- // Directly call datasource to get children data without expanding the node
7956
- // This avoids UI glitches from expand/collapse operations
7957
- // If node has no children, datasource will return empty array
7958
- const childNodes = await this.datasource(parentId);
7959
- if (!childNodes || childNodes.length === 0) {
7960
- return; // No children to process
7961
- }
7962
- // Collect all child IDs to add to selectedNodeIds
7963
- const childIdsToSelect = [];
7964
- // Process all children and cache their data
7965
- for (const childNode of childNodes) {
7966
- const childId = String(childNode['id'] ?? '');
7967
- if (childId && childId !== 'all') {
7968
- childIdsToSelect.push(childId);
7969
- // Cache node data for getSelectedItems
7970
- // Try to get data from 'data' property first, then fallback to node itself
7971
- let nodeData = childNode['data'];
7972
- // If no data property, try to extract data from the node
7973
- if (!nodeData || typeof nodeData !== 'object') {
7974
- // Create a data object from the node properties
7975
- const valueField = this.treeConfig.valueField || 'id';
7976
- const textField = this.treeConfig.textField || 'title';
7977
- nodeData = {
7978
- [valueField]: childId,
7979
- [textField]: childNode['title'] ?? '',
7980
- ...childNode,
7981
- };
7982
- }
7983
- // Cache the node data
7984
- if (nodeData && typeof nodeData === 'object') {
7985
- this.nodeDataCache.set(childId, nodeData);
7986
- }
7987
- }
7988
- }
7989
- if (childIdsToSelect.length === 0) {
7990
- return; // No valid children to select
7991
- }
7992
- // Update selectedNodeIds to include all children
7993
- const currentSelected = this.selectedNodeIds();
7994
- const newSelected = [...currentSelected];
7995
- let hasNewSelections = false;
7996
- for (const childId of childIdsToSelect) {
7997
- if (!newSelected.includes(childId)) {
7998
- newSelected.push(childId);
7999
- hasNewSelections = true;
8000
- }
8001
- }
8002
- if (hasNewSelections) {
8003
- this.selectedNodeIds.set(newSelected);
8004
- }
8005
- // Try to select children in tree component if they're already loaded
8006
- // If not loaded yet, they'll be selected when the tree loads them (via markNodeAsSelectedIfNeeded)
8007
- for (const childId of childIdsToSelect) {
8008
- try {
8009
- // Only try to select if node exists in tree (might not be loaded if parent isn't expanded)
8010
- const node = treeComponent.findNode(childId);
8011
- if (node) {
8012
- treeComponent.selectNode(childId);
8013
- }
8014
- }
8015
- catch {
8016
- // If selection fails, it's okay - we've already added to selectedNodeIds
8017
- // The tree will sync selection when the node is loaded via datasource callback
8018
- }
8019
- }
8020
- // Recursively load and select children of each child
8021
- // Use Promise.all for parallel processing to improve performance
8022
- await Promise.all(childIdsToSelect.map((childId) => this.loadAndSelectChildrenRecursively(childId)));
8023
- }
8024
- catch (error) {
8025
- console.error(`Error loading children for node ${parentId}:`, error);
8026
- }
7743
+ async isLeafNodeCheck(nodeId) {
7744
+ const hasChildren = await this.nodeHasChildren(nodeId);
7745
+ return !hasChildren;
8027
7746
  }
8028
- //#endregion
8029
- async updateSelectedNodes(selectedIds) {
8030
- if (!selectedIds || selectedIds.length === 0) {
8031
- 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) {
8032
7753
  return;
8033
7754
  }
8034
- const ids = selectedIds.filter((id) => id && id !== 'all');
8035
- // Set flag to prevent recursive updates during initialization
8036
- this.isUpdatingSelection = true;
8037
- try {
8038
- // Step 1: Fetch node data for pre-selected items that aren't in the cache
8039
- await this.loadMissingNodeData(ids);
8040
- // Step 2: Build complete ancestor chains for all selected nodes
8041
- const ancestorChains = await this.buildAncestorChains(ids);
8042
- // Step 3: Set selected node IDs (these should be leaf nodes only)
8043
- this.selectedNodeIds.set(ids);
8044
- // Step 4: Expand ancestor nodes in order (root to leaf) to load them into tree
8045
- await this.expandAncestorNodesInOrder(ancestorChains);
8046
- // Step 5: Wait for tree to render and sync selection
8047
- await this.syncSelectionWithTree(ids);
8048
- }
8049
- finally {
8050
- 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
+ };
8051
7764
  }
7765
+ this.nodeDataCache.set(nodeId, nodeData);
8052
7766
  }
7767
+ //#endregion
8053
7768
  /**
8054
7769
  * Builds complete ancestor chains for all selected node IDs.
8055
7770
  * Returns a Map where key is the selected node ID and value is array of ancestor IDs from root to parent.
@@ -8430,42 +8145,38 @@ class AXPEntityCategoryTreeSelectorComponent extends AXBasePageComponent {
8430
8145
  const treeComponent = this.tree();
8431
8146
  if (treeComponent) {
8432
8147
  setTimeout(() => {
8433
- const selectedIds = this.selectedNodeIds();
8434
- selectedIds.forEach((id) => {
8435
- if (id && id !== 'all') {
8436
- treeComponent.selectNode(id);
8437
- }
8438
- });
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
+ }
8439
8165
  }, 0);
8440
8166
  }
8441
8167
  }
8442
8168
  /**
8443
- * 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
8444
8171
  */
8445
8172
  processChildNodes(childNodes) {
8446
8173
  const excludedId = this.excludedNodeId();
8447
- const selectedIds = this.selectedNodeIds();
8448
8174
  childNodes.forEach((node) => {
8449
8175
  const nodeId = String(node['id'] ?? '');
8450
8176
  if (excludedId && nodeId === excludedId) {
8451
8177
  node['disabled'] = true;
8452
8178
  }
8453
- if (nodeId && selectedIds.includes(nodeId)) {
8454
- node['selected'] = true;
8455
- }
8456
8179
  });
8457
- // Sync selection with tree component
8458
- const treeComponent = this.tree();
8459
- if (treeComponent) {
8460
- setTimeout(() => {
8461
- childNodes.forEach((node) => {
8462
- const nodeId = String(node['id'] ?? '');
8463
- if (nodeId && selectedIds.includes(nodeId) && nodeId !== 'all') {
8464
- treeComponent.selectNode(nodeId);
8465
- }
8466
- });
8467
- }, 0);
8468
- }
8469
8180
  }
8470
8181
  /**
8471
8182
  * Caches node data from items array
@@ -8609,67 +8320,6 @@ class AXPEntityCategoryTreeSelectorComponent extends AXBasePageComponent {
8609
8320
  const textField = this.treeConfig.textField || 'title';
8610
8321
  return String(nodeData[textField] ?? '');
8611
8322
  }
8612
- /**
8613
- * Checks if a node is a leaf node (has no children)
8614
- */
8615
- async isLeafNode(nodeId, treeComponent) {
8616
- if (!treeComponent) {
8617
- // If no tree component, check if node has children by querying
8618
- return await this.checkIfNodeHasChildren(nodeId);
8619
- }
8620
- try {
8621
- const node = treeComponent.findNode(nodeId);
8622
- if (!node) {
8623
- // Node not found in tree, check via query
8624
- return await this.checkIfNodeHasChildren(nodeId);
8625
- }
8626
- // Check if node has children
8627
- const children = node['children'];
8628
- const childrenCount = node['childrenCount'];
8629
- // If children are loaded, check the array
8630
- if (children !== undefined) {
8631
- return !children || children.length === 0;
8632
- }
8633
- // If childrenCount is available, use it
8634
- if (childrenCount !== undefined) {
8635
- return childrenCount === 0;
8636
- }
8637
- // If neither is available, try to check via query
8638
- return await this.checkIfNodeHasChildren(nodeId);
8639
- }
8640
- catch {
8641
- // If findNode fails, check via query
8642
- return await this.checkIfNodeHasChildren(nodeId);
8643
- }
8644
- }
8645
- /**
8646
- * Checks if a node has children by querying the data source
8647
- */
8648
- async checkIfNodeHasChildren(nodeId) {
8649
- if (!this.treeData?.categoryEntityQueryFunc || !this.treeConfig) {
8650
- return true; // Assume leaf if we can't check
8651
- }
8652
- try {
8653
- const parentKey = this.treeData.categoryEntityDef?.parentKey;
8654
- if (!parentKey) {
8655
- return true; // No parent key means flat structure, all nodes are leaves
8656
- }
8657
- const event = {
8658
- ...this.treeData.basicQueryEvent,
8659
- filter: {
8660
- field: parentKey,
8661
- value: nodeId,
8662
- operator: { type: 'equal' },
8663
- },
8664
- take: 1, // Only need to check if any children exist
8665
- };
8666
- const res = await this.treeData.categoryEntityQueryFunc(event);
8667
- return !res?.items || res.items.length === 0;
8668
- }
8669
- catch {
8670
- return true; // Assume leaf on error
8671
- }
8672
- }
8673
8323
  async getSelectedItems() {
8674
8324
  // selectedNodeIds now only contains LEAF nodes (already filtered)
8675
8325
  const selectedIds = this.selectedNodeIds();
@@ -8769,8 +8419,6 @@ class AXPEntityCategoryTreeSelectorComponent extends AXBasePageComponent {
8769
8419
  [titleField]="textField()"
8770
8420
  [idField]="valueField()"
8771
8421
  (onNodeSelect)="onNodeSelect($event)"
8772
- (onSelectionChange)="onSelectionChange($event)"
8773
- (onNodeToggle)="onNodeToggle($event)"
8774
8422
  [nodeTemplate]="itemTemplate"
8775
8423
  #tree
8776
8424
  >
@@ -8900,8 +8548,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
8900
8548
  [titleField]="textField()"
8901
8549
  [idField]="valueField()"
8902
8550
  (onNodeSelect)="onNodeSelect($event)"
8903
- (onSelectionChange)="onSelectionChange($event)"
8904
- (onNodeToggle)="onNodeToggle($event)"
8905
8551
  [nodeTemplate]="itemTemplate"
8906
8552
  #tree
8907
8553
  >