@acorex/platform 21.0.0-next.10 → 21.0.0-next.11

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.
Files changed (24) hide show
  1. package/core/index.d.ts +109 -3
  2. package/fesm2022/acorex-platform-core.mjs +3 -0
  3. package/fesm2022/acorex-platform-core.mjs.map +1 -1
  4. package/fesm2022/acorex-platform-layout-builder.mjs +0 -2
  5. package/fesm2022/acorex-platform-layout-builder.mjs.map +1 -1
  6. package/fesm2022/acorex-platform-layout-components.mjs +6 -6
  7. package/fesm2022/acorex-platform-layout-components.mjs.map +1 -1
  8. package/fesm2022/acorex-platform-layout-entity.mjs +619 -466
  9. package/fesm2022/acorex-platform-layout-entity.mjs.map +1 -1
  10. package/fesm2022/acorex-platform-layout-views.mjs +2 -2
  11. package/fesm2022/acorex-platform-layout-views.mjs.map +1 -1
  12. package/fesm2022/acorex-platform-layout-widget-core.mjs +3 -4
  13. package/fesm2022/acorex-platform-layout-widget-core.mjs.map +1 -1
  14. package/fesm2022/acorex-platform-layout-widgets.mjs +50 -55
  15. package/fesm2022/acorex-platform-layout-widgets.mjs.map +1 -1
  16. package/fesm2022/{acorex-platform-themes-default-entity-master-list-view.component-CIV6YDDZ.mjs → acorex-platform-themes-default-entity-master-list-view.component-CD4Q_UIG.mjs} +3 -3
  17. package/fesm2022/acorex-platform-themes-default-entity-master-list-view.component-CD4Q_UIG.mjs.map +1 -0
  18. package/fesm2022/acorex-platform-themes-default.mjs +2 -2
  19. package/fesm2022/acorex-platform-workflow.mjs.map +1 -1
  20. package/layout/entity/index.d.ts +47 -16
  21. package/layout/widgets/index.d.ts +4 -6
  22. package/package.json +9 -9
  23. package/workflow/index.d.ts +4 -4
  24. package/fesm2022/acorex-platform-themes-default-entity-master-list-view.component-CIV6YDDZ.mjs.map +0 -1
@@ -5,7 +5,7 @@ import * as i4$1 from '@acorex/platform/common';
5
5
  import { AXPSettingsService, AXPFilterOperatorMiddlewareService, AXPEntityCommandScope, getEntityInfo, AXPRefreshEvent, AXPReloadEvent, AXPCommonSettings, AXPEntityQueryType, AXPCleanNestedFilters, AXPWorkflowNavigateAction, AXPToastAction, AXP_SEARCH_DEFINITION_PROVIDER } from '@acorex/platform/common';
6
6
  import { AXPDeviceService, AXPBroadcastEventService, resolveActionLook, AXPExpressionEvaluatorService, AXPDistributedEventListenerService, AXPPlatformScope, AXPColumnWidthService, AXHighlightService, extractValue, setSmart, getChangedPaths, defaultColumnWidthProvider, AXP_COLUMN_WIDTH_PROVIDER, AXPSystemActionType } from '@acorex/platform/core';
7
7
  import * as i0 from '@angular/core';
8
- import { InjectionToken, inject, Injector, runInInjectionContext, Injectable, input, viewChild, signal, ElementRef, ChangeDetectionStrategy, Component, ApplicationRef, EnvironmentInjector, createComponent, computed, effect, Input, afterNextRender, untracked, ViewEncapsulation, ChangeDetectorRef, viewChildren, linkedSignal, HostBinding, output, NgModule } from '@angular/core';
8
+ 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';
9
9
  import { Subject, takeUntil } from 'rxjs';
10
10
  import { AXPLayoutBuilderService } from '@acorex/platform/layout/builder';
11
11
  import { merge, castArray, get, cloneDeep, set, orderBy, isNil, isEmpty, isEqual } from 'lodash-es';
@@ -44,9 +44,11 @@ import { AXSearchBoxModule, AXSearchBoxComponent } from '@acorex/components/sear
44
44
  import * as i6$1 from '@acorex/components/skeleton';
45
45
  import { AXSkeletonModule } from '@acorex/components/skeleton';
46
46
  import { AXTreeViewComponent } from '@acorex/components/tree-view';
47
- import { AXPStateMessageComponent, AXPDataSelectorService } from '@acorex/platform/layout/components';
47
+ import { AXPStateMessageComponent, AXPColumnItemListComponent, AXPDataSelectorService } from '@acorex/platform/layout/components';
48
48
  import * as i1 from '@angular/forms';
49
49
  import { FormsModule } from '@angular/forms';
50
+ import * as i3$4 from '@acorex/components/tooltip';
51
+ import { AXTooltipModule } from '@acorex/components/tooltip';
50
52
  import * as i5$2 from '@acorex/components/form';
51
53
  import { AXFormModule } from '@acorex/components/form';
52
54
  import * as i6$2 from '@acorex/components/tag-box';
@@ -6339,6 +6341,7 @@ class AXPEntityCategoryTreeSelectorComponent extends AXBasePageComponent {
6339
6341
  this.categoryTreeService = inject(AXPCategoryTreeService);
6340
6342
  this.translationService = inject(AXTranslationService);
6341
6343
  this.highlightService = inject(AXHighlightService);
6344
+ this.changeDetectorRef = inject(ChangeDetectorRef);
6342
6345
  //#endregion
6343
6346
  //#region ---- Properties (Set by popup service) ----
6344
6347
  this.entityKey = signal('', ...(ngDevMode ? [{ debugName: "entityKey" }] : []));
@@ -6430,43 +6433,27 @@ class AXPEntityCategoryTreeSelectorComponent extends AXBasePageComponent {
6430
6433
  }
6431
6434
  });
6432
6435
  }
6433
- // Mark pre-selected nodes as selected in the child nodes
6436
+ // Mark pre-selected leaf nodes as selected in the child nodes
6437
+ const selectedIds = this.selectedNodeIds();
6434
6438
  childNodes.forEach((node) => {
6435
- this.markNodeAsSelectedIfNeeded(node);
6439
+ const nodeId = String(node['id'] ?? '');
6440
+ if (nodeId && selectedIds.includes(nodeId)) {
6441
+ node['selected'] = true;
6442
+ }
6436
6443
  });
6437
- // After children load, programmatically select pre-selected nodes
6438
- // Use a small delay to ensure nodes are fully added to tree structure
6444
+ // After children load, programmatically select pre-selected nodes in tree component
6439
6445
  const treeComponent = this.tree();
6440
- if (treeComponent) {
6446
+ if (treeComponent && selectedIds.length > 0) {
6441
6447
  // Use setTimeout to ensure nodes are in tree structure before selecting
6442
6448
  setTimeout(() => {
6443
- const selectedIds = this.selectedNodeIds();
6444
6449
  childNodes.forEach((node) => {
6445
6450
  const nodeId = String(node['id'] ?? '');
6446
6451
  if (nodeId && selectedIds.includes(nodeId) && nodeId !== 'all') {
6447
6452
  try {
6448
- // Try to find and select the node
6449
- const treeNode = treeComponent.findNode(nodeId);
6450
- if (treeNode) {
6451
- treeComponent.selectNode(nodeId);
6452
- }
6453
- else {
6454
- // If node not found, try again after a short delay (might still be loading)
6455
- setTimeout(() => {
6456
- try {
6457
- const retryNode = treeComponent.findNode(nodeId);
6458
- if (retryNode) {
6459
- treeComponent.selectNode(nodeId);
6460
- }
6461
- }
6462
- catch {
6463
- // Node still not found, will be selected when it loads
6464
- }
6465
- }, 50);
6466
- }
6453
+ treeComponent.selectNode(nodeId);
6467
6454
  }
6468
- catch (error) {
6469
- // Node might not be in tree yet, will be selected when it loads
6455
+ catch {
6456
+ // Node might not be in tree yet
6470
6457
  }
6471
6458
  }
6472
6459
  });
@@ -6988,14 +6975,10 @@ class AXPEntityCategoryTreeSelectorComponent extends AXBasePageComponent {
6988
6975
  if (this.isInitializing || this.isUpdatingSelection) {
6989
6976
  return;
6990
6977
  }
6991
- // Update selected node IDs from the tree component's selection state
6992
- // The tree component with intermediate-nested behavior automatically manages parent states
6978
+ // The tree component fires selection changes, but we only want to track LEAF nodes
6979
+ // Parent nodes are auto-selected/deselected by the tree's intermediate-nested behavior
6993
6980
  const selectedNodes = event.selectedNodes || [];
6994
- const selectedIds = selectedNodes.map((node) => String(node['id'] ?? '')).filter((id) => id && id !== 'all');
6995
- // Sync with tree component's selection state
6996
- // This includes parents that are automatically selected when all children are selected
6997
- this.selectedNodeIds.set(selectedIds);
6998
- // Cache node data for all selected nodes
6981
+ // Cache node data for all selected nodes first
6999
6982
  selectedNodes.forEach((node) => {
7000
6983
  const nodeId = String(node['id'] ?? '');
7001
6984
  const nodeData = node['data'];
@@ -7003,6 +6986,23 @@ class AXPEntityCategoryTreeSelectorComponent extends AXBasePageComponent {
7003
6986
  this.nodeDataCache.set(nodeId, nodeData);
7004
6987
  }
7005
6988
  });
6989
+ // Filter to only leaf nodes (nodes with no children or childrenCount === 0)
6990
+ const leafNodeIds = [];
6991
+ for (const node of selectedNodes) {
6992
+ const nodeId = String(node['id'] ?? '');
6993
+ if (!nodeId || nodeId === 'all') {
6994
+ continue;
6995
+ }
6996
+ // Check if this is a leaf node
6997
+ const children = node['children'];
6998
+ const childrenCount = node['childrenCount'];
6999
+ const isLeaf = (!children || children.length === 0) && (childrenCount === undefined || childrenCount === 0);
7000
+ if (isLeaf) {
7001
+ leafNodeIds.push(nodeId);
7002
+ }
7003
+ }
7004
+ // Update selectedNodeIds with only leaf nodes
7005
+ this.selectedNodeIds.set(leafNodeIds);
7006
7006
  }
7007
7007
  // protected handleNodeClick(event: any): void {
7008
7008
  // // Extract node from event - could be { node: AXTreeNode } or just AXTreeNode
@@ -7040,6 +7040,26 @@ class AXPEntityCategoryTreeSelectorComponent extends AXBasePageComponent {
7040
7040
  async onCancel() {
7041
7041
  await this.close();
7042
7042
  }
7043
+ /**
7044
+ * Clears all selected items
7045
+ */
7046
+ onClearAll() {
7047
+ const treeComponent = this.tree();
7048
+ if (treeComponent) {
7049
+ // Deselect all nodes in tree component
7050
+ const currentSelected = this.selectedNodeIds();
7051
+ for (const id of currentSelected) {
7052
+ try {
7053
+ treeComponent.deselectNode(id);
7054
+ }
7055
+ catch {
7056
+ // Node might not be in tree
7057
+ }
7058
+ }
7059
+ }
7060
+ // Clear the selection
7061
+ this.selectedNodeIds.set([]);
7062
+ }
7043
7063
  /**
7044
7064
  * Checks if a node matches the current search term
7045
7065
  */
@@ -7058,8 +7078,8 @@ class AXPEntityCategoryTreeSelectorComponent extends AXBasePageComponent {
7058
7078
  }
7059
7079
  /**
7060
7080
  * Handles checkbox change event to toggle node selection
7061
- * In multiple mode: recursively selects/deselects children
7062
- * Parent states are handled automatically by tree component's intermediate-nested behavior
7081
+ * In multiple mode: recursively selects/deselects LEAF children only
7082
+ * Parent states are calculated based on leaf descendants
7063
7083
  */
7064
7084
  async handleCheckboxChange(nodeId, checked) {
7065
7085
  if (!nodeId || nodeId === 'all') {
@@ -7071,15 +7091,142 @@ class AXPEntityCategoryTreeSelectorComponent extends AXBasePageComponent {
7071
7091
  return;
7072
7092
  }
7073
7093
  if (checked) {
7074
- // Select the node and recursively select all its children
7075
- await this.selectNodeAndChildren(id);
7094
+ // Select all descendant LEAF nodes
7095
+ await this.selectLeafDescendants(id);
7076
7096
  }
7077
7097
  else {
7078
- // Deselect the node and recursively deselect all its children
7079
- await this.deselectNodeAndChildren(id);
7098
+ // Deselect all descendant LEAF nodes
7099
+ await this.deselectLeafDescendants(id);
7100
+ }
7101
+ // Update parent states after selection change
7102
+ await this.refreshParentStatesInTree();
7103
+ }
7104
+ /**
7105
+ * Selects all leaf descendants of a node (and the node itself if it's a leaf)
7106
+ */
7107
+ async selectLeafDescendants(nodeId) {
7108
+ this.isUpdatingSelection = true;
7109
+ try {
7110
+ // Collect all leaf descendants
7111
+ const leafNodes = new Set();
7112
+ await this.collectLeafDescendants(nodeId, leafNodes);
7113
+ if (leafNodes.size === 0) {
7114
+ // Node itself is a leaf
7115
+ leafNodes.add(nodeId);
7116
+ }
7117
+ // Update selectedNodeIds with leaf nodes only
7118
+ const currentSelected = new Set(this.selectedNodeIds());
7119
+ leafNodes.forEach((id) => currentSelected.add(id));
7120
+ this.selectedNodeIds.set(Array.from(currentSelected));
7121
+ // Sync with tree component
7122
+ const treeComponent = this.tree();
7123
+ if (treeComponent) {
7124
+ for (const leafId of leafNodes) {
7125
+ try {
7126
+ const node = treeComponent.findNode(leafId);
7127
+ if (node) {
7128
+ treeComponent.selectNode(leafId);
7129
+ }
7130
+ }
7131
+ catch {
7132
+ // Node might not be in tree
7133
+ }
7134
+ }
7135
+ }
7136
+ }
7137
+ finally {
7138
+ this.isUpdatingSelection = false;
7139
+ }
7140
+ }
7141
+ /**
7142
+ * Deselects all leaf descendants of a node (and the node itself if it's a leaf)
7143
+ */
7144
+ async deselectLeafDescendants(nodeId) {
7145
+ this.isUpdatingSelection = true;
7146
+ try {
7147
+ // Collect all leaf descendants
7148
+ const leafNodes = new Set();
7149
+ await this.collectLeafDescendants(nodeId, leafNodes);
7150
+ if (leafNodes.size === 0) {
7151
+ // Node itself is a leaf
7152
+ leafNodes.add(nodeId);
7153
+ }
7154
+ // Remove leaf nodes from selectedNodeIds
7155
+ const currentSelected = this.selectedNodeIds();
7156
+ const newSelected = currentSelected.filter((id) => !leafNodes.has(id));
7157
+ this.selectedNodeIds.set(newSelected);
7158
+ // Sync with tree component
7159
+ const treeComponent = this.tree();
7160
+ if (treeComponent) {
7161
+ for (const leafId of leafNodes) {
7162
+ try {
7163
+ const node = treeComponent.findNode(leafId);
7164
+ if (node) {
7165
+ treeComponent.deselectNode(leafId);
7166
+ }
7167
+ }
7168
+ catch {
7169
+ // Node might not be in tree
7170
+ }
7171
+ }
7172
+ }
7173
+ }
7174
+ finally {
7175
+ this.isUpdatingSelection = false;
7176
+ }
7177
+ }
7178
+ /**
7179
+ * Collects all LEAF descendant node IDs recursively
7180
+ * A leaf node is one that has no children
7181
+ */
7182
+ async collectLeafDescendants(parentId, collection) {
7183
+ if (!this.treeData || !this.treeConfig || !parentId || parentId === 'all') {
7184
+ return;
7185
+ }
7186
+ try {
7187
+ const childNodes = await this.datasource(parentId);
7188
+ if (!childNodes || childNodes.length === 0) {
7189
+ // No children means this node is a leaf - add the parent itself
7190
+ // But only if it wasn't already added through the parent call
7191
+ return;
7192
+ }
7193
+ for (const childNode of childNodes) {
7194
+ const childId = String(childNode['id'] ?? '');
7195
+ if (!childId || childId === 'all') {
7196
+ continue;
7197
+ }
7198
+ // Cache node data
7199
+ const nodeData = childNode['data'];
7200
+ if (nodeData && typeof nodeData === 'object') {
7201
+ this.nodeDataCache.set(childId, nodeData);
7202
+ }
7203
+ // Check if this child is a leaf by trying to load its children
7204
+ const grandchildNodes = await this.datasource(childId);
7205
+ if (!grandchildNodes || grandchildNodes.length === 0) {
7206
+ // This child is a leaf node
7207
+ collection.add(childId);
7208
+ }
7209
+ else {
7210
+ // This child has children, recurse
7211
+ await this.collectLeafDescendants(childId, collection);
7212
+ }
7213
+ }
7214
+ }
7215
+ catch (error) {
7216
+ console.error(`Error collecting leaf descendants for node ${parentId}:`, error);
7217
+ }
7218
+ }
7219
+ /**
7220
+ * Refreshes parent states in the tree based on leaf selection
7221
+ */
7222
+ async refreshParentStatesInTree() {
7223
+ const treeComponent = this.tree();
7224
+ if (!treeComponent) {
7225
+ return;
7080
7226
  }
7081
- // Note: Parent states (select/intermediate) are handled automatically by the tree component
7082
- // with selectionBehavior: 'intermediate-nested'. We don't need to manually update them.
7227
+ // The tree component with intermediate-nested behavior should handle this
7228
+ // But we need to force a refresh by reloading data
7229
+ // For now, we rely on the tree component's built-in behavior
7083
7230
  }
7084
7231
  /**
7085
7232
  * Selects a node and recursively selects all its children
@@ -7452,357 +7599,296 @@ class AXPEntityCategoryTreeSelectorComponent extends AXBasePageComponent {
7452
7599
  // Set flag to prevent recursive updates during initialization
7453
7600
  this.isUpdatingSelection = true;
7454
7601
  try {
7455
- // Fetch node data for pre-selected items that aren't in the cache
7602
+ // Step 1: Fetch node data for pre-selected items that aren't in the cache
7456
7603
  await this.loadMissingNodeData(ids);
7457
- // Collect all nodes to select (pre-selected + all their descendants)
7458
- const allNodesToSelect = new Set(ids);
7459
- for (const nodeId of ids) {
7460
- await this.collectAllDescendants(nodeId, allNodesToSelect);
7461
- }
7462
- // Set all selected nodes at once
7463
- this.selectedNodeIds.set(Array.from(allNodesToSelect));
7464
- // Expand parents of pre-selected nodes so they're visible in the tree
7465
- await this.expandParentsOfSelectedNodes(Array.from(allNodesToSelect));
7466
- // Sync selection with tree component - wait for tree to be ready
7467
- await this.syncSelectionWithTree(Array.from(allNodesToSelect));
7604
+ // Step 2: Build complete ancestor chains for all selected nodes
7605
+ const ancestorChains = await this.buildAncestorChains(ids);
7606
+ // Step 3: Set selected node IDs (these should be leaf nodes only)
7607
+ this.selectedNodeIds.set(ids);
7608
+ // Step 4: Expand ancestor nodes in order (root to leaf) to load them into tree
7609
+ await this.expandAncestorNodesInOrder(ancestorChains);
7610
+ // Step 5: Wait for tree to render and sync selection
7611
+ await this.syncSelectionWithTree(ids);
7468
7612
  }
7469
7613
  finally {
7470
7614
  this.isUpdatingSelection = false;
7471
7615
  }
7472
7616
  }
7473
7617
  /**
7474
- * Updates all parent states based on their children's selection
7475
- * Used during initialization to ensure proper parent states (select/intermediate)
7618
+ * Builds complete ancestor chains for all selected node IDs.
7619
+ * Returns a Map where key is the selected node ID and value is array of ancestor IDs from root to parent.
7476
7620
  */
7477
- async updateAllParentStates(selectedIds) {
7478
- if (!this.treeData || !this.treeConfig || !this.allowMultiple() || selectedIds.length === 0) {
7479
- return;
7621
+ async buildAncestorChains(selectedIds) {
7622
+ const ancestorChains = new Map();
7623
+ if (!this.treeData || !this.treeConfig) {
7624
+ return ancestorChains;
7480
7625
  }
7481
7626
  const parentKey = this.treeData.categoryEntityDef?.parentKey;
7482
7627
  if (!parentKey) {
7483
- return;
7484
- }
7485
- const treeComponent = this.tree();
7486
- if (!treeComponent) {
7487
- return;
7628
+ // No parent key means flat structure
7629
+ return ancestorChains;
7488
7630
  }
7489
- const selectedSet = new Set(selectedIds);
7490
- const processedParents = new Set();
7491
- // Collect all unique parent IDs
7492
- for (const selectedId of selectedIds) {
7493
- if (selectedId === 'all') {
7494
- continue;
7495
- }
7496
- const nodeData = this.nodeDataCache.get(selectedId);
7497
- if (!nodeData) {
7631
+ for (const nodeId of selectedIds) {
7632
+ const chain = [];
7633
+ let currentData = this.nodeDataCache.get(nodeId);
7634
+ if (!currentData) {
7498
7635
  continue;
7499
7636
  }
7500
- let currentId = selectedId;
7501
- while (currentId && currentId !== 'all') {
7502
- const currentNodeData = this.nodeDataCache.get(currentId);
7503
- if (!currentNodeData) {
7637
+ // Traverse up to root, collecting ancestor IDs
7638
+ const visited = new Set();
7639
+ while (currentData) {
7640
+ const parentId = currentData[parentKey];
7641
+ if (!parentId || parentId === 'all' || visited.has(String(parentId))) {
7504
7642
  break;
7505
7643
  }
7506
- const parentId = currentNodeData[parentKey];
7507
7644
  const parentIdStr = String(parentId);
7508
- if (!parentId || parentId === 'all' || parentId === currentId || processedParents.has(parentIdStr)) {
7509
- break;
7510
- }
7511
- processedParents.add(parentIdStr);
7512
- currentId = parentIdStr;
7513
- }
7514
- }
7515
- // Process parents multiple times to handle cascading parent selections
7516
- // (when a parent is selected, its parent might also need to be selected)
7517
- let hasChanges = true;
7518
- let iterations = 0;
7519
- const maxIterations = 10; // Prevent infinite loops
7520
- while (hasChanges && iterations < maxIterations) {
7521
- iterations++;
7522
- hasChanges = false;
7523
- // Update selectedSet with current selectedNodeIds
7524
- const currentSelected = this.selectedNodeIds();
7525
- currentSelected.forEach((id) => selectedSet.add(id));
7526
- // Update each parent's state
7527
- for (const parentId of processedParents) {
7528
- try {
7529
- const childNodes = await this.datasource(parentId);
7530
- if (!childNodes || childNodes.length === 0) {
7531
- continue;
7532
- }
7533
- let selectedCount = 0;
7534
- let totalCount = 0;
7535
- for (const childNode of childNodes) {
7536
- const childId = String(childNode['id'] ?? '');
7537
- if (childId && childId !== 'all') {
7538
- totalCount++;
7539
- if (selectedSet.has(childId)) {
7540
- selectedCount++;
7541
- }
7542
- }
7645
+ visited.add(parentIdStr);
7646
+ chain.unshift(parentIdStr); // Add to beginning (root first)
7647
+ // Load parent data if not cached
7648
+ if (!this.nodeDataCache.has(parentIdStr)) {
7649
+ const parentData = await this.fetchItemById(parentIdStr);
7650
+ if (parentData) {
7651
+ this.nodeDataCache.set(parentIdStr, parentData);
7652
+ currentData = parentData;
7543
7653
  }
7544
- const isParentSelected = currentSelected.includes(parentId);
7545
- if (totalCount > 0) {
7546
- if (selectedCount === totalCount) {
7547
- // All children selected - select parent
7548
- if (!isParentSelected) {
7549
- this.selectedNodeIds.set([...currentSelected, parentId]);
7550
- selectedSet.add(parentId);
7551
- hasChanges = true;
7552
- try {
7553
- treeComponent.selectNode(parentId);
7554
- }
7555
- catch {
7556
- // Parent might not be in tree
7557
- }
7558
- }
7559
- }
7560
- else if (selectedCount > 0) {
7561
- // Some children selected - parent should be in intermediate state
7562
- if (isParentSelected) {
7563
- // Deselect parent to show intermediate state
7564
- this.selectedNodeIds.set(currentSelected.filter((id) => id !== parentId));
7565
- selectedSet.delete(parentId);
7566
- hasChanges = true;
7567
- try {
7568
- treeComponent.deselectNode(parentId);
7569
- }
7570
- catch {
7571
- // Parent might not be in tree
7572
- }
7573
- }
7574
- }
7575
- else {
7576
- // No children selected - deselect parent
7577
- if (isParentSelected) {
7578
- this.selectedNodeIds.set(currentSelected.filter((id) => id !== parentId));
7579
- selectedSet.delete(parentId);
7580
- hasChanges = true;
7581
- try {
7582
- treeComponent.deselectNode(parentId);
7583
- }
7584
- catch {
7585
- // Parent might not be in tree
7586
- }
7587
- }
7588
- }
7654
+ else {
7655
+ break;
7589
7656
  }
7590
7657
  }
7591
- catch (error) {
7592
- console.error(`Error updating parent state for ${parentId}:`, error);
7658
+ else {
7659
+ currentData = this.nodeDataCache.get(parentIdStr);
7593
7660
  }
7594
7661
  }
7662
+ ancestorChains.set(nodeId, chain);
7595
7663
  }
7664
+ return ancestorChains;
7596
7665
  }
7597
7666
  /**
7598
- * Expands parents of selected nodes so they're visible when popup opens
7667
+ * Expands ancestor nodes in order from root to leaf.
7668
+ * This ensures parent nodes are loaded into the tree before selecting children.
7599
7669
  */
7600
- async expandParentsOfSelectedNodes(selectedIds) {
7601
- if (!this.treeData || !this.treeConfig || selectedIds.length === 0) {
7602
- return;
7603
- }
7604
- const parentKey = this.treeData.categoryEntityDef?.parentKey;
7605
- if (!parentKey) {
7606
- return; // No parent key means flat structure
7607
- }
7670
+ async expandAncestorNodesInOrder(ancestorChains) {
7608
7671
  const treeComponent = this.tree();
7609
7672
  if (!treeComponent) {
7610
7673
  return;
7611
7674
  }
7612
- // Collect all unique parent IDs that need to be expanded
7613
- const parentsToExpand = new Set();
7614
- for (const selectedId of selectedIds) {
7615
- if (selectedId === 'all') {
7616
- continue;
7617
- }
7618
- const nodeData = this.nodeDataCache.get(selectedId);
7619
- if (!nodeData) {
7620
- continue;
7621
- }
7622
- const parentId = nodeData[parentKey];
7623
- if (parentId && parentId !== 'all') {
7624
- const parentIdStr = String(parentId);
7625
- parentsToExpand.add(parentIdStr);
7626
- // Also expand grandparents, etc.
7627
- let currentParentId = parentIdStr;
7628
- while (currentParentId) {
7629
- const parentData = this.nodeDataCache.get(currentParentId);
7630
- if (!parentData) {
7631
- break;
7632
- }
7633
- const grandParentId = parentData[parentKey];
7634
- if (grandParentId && grandParentId !== 'all' && grandParentId !== currentParentId) {
7635
- parentsToExpand.add(String(grandParentId));
7636
- currentParentId = String(grandParentId);
7637
- }
7638
- else {
7639
- break;
7640
- }
7675
+ // Collect all unique ancestors and sort by depth (shallowest first)
7676
+ const allAncestors = new Set();
7677
+ const ancestorDepths = new Map();
7678
+ ancestorChains.forEach((chain) => {
7679
+ chain.forEach((ancestorId, depth) => {
7680
+ allAncestors.add(ancestorId);
7681
+ // Keep the minimum depth if ancestor appears in multiple chains
7682
+ const existingDepth = ancestorDepths.get(ancestorId);
7683
+ if (existingDepth === undefined || depth < existingDepth) {
7684
+ ancestorDepths.set(ancestorId, depth);
7641
7685
  }
7642
- }
7643
- }
7644
- // Expand all parents (starting from root to leaves)
7645
- const sortedParents = Array.from(parentsToExpand).sort((a, b) => {
7646
- // Simple sort - expand root nodes first
7647
- return a.localeCompare(b);
7686
+ });
7648
7687
  });
7649
- for (const parentId of sortedParents) {
7650
- try {
7651
- if (!treeComponent.isNodeExpanded(parentId)) {
7652
- await treeComponent.expandNode(parentId);
7653
- // Small delay to ensure children are loaded
7654
- await new Promise((resolve) => setTimeout(resolve, 50));
7655
- }
7656
- }
7657
- catch {
7658
- // Parent might not be in tree yet, ignore
7688
+ // Sort by depth (root ancestors first)
7689
+ const sortedAncestors = Array.from(allAncestors).sort((a, b) => {
7690
+ return (ancestorDepths.get(a) ?? 0) - (ancestorDepths.get(b) ?? 0);
7691
+ });
7692
+ // First expand root node if not expanded
7693
+ try {
7694
+ if (!treeComponent.isNodeExpanded('all')) {
7695
+ await treeComponent.expandNode('all');
7696
+ await new Promise((resolve) => setTimeout(resolve, 50));
7659
7697
  }
7660
7698
  }
7661
- }
7662
- /**
7663
- * Selects parents whose all children are selected
7664
- * This ensures that when all children of a parent are selected, the parent also appears selected
7665
- */
7666
- async selectParentsWithAllChildrenSelected(selectedIds) {
7667
- if (!this.treeData || !this.treeConfig || selectedIds.length === 0) {
7668
- return;
7669
- }
7670
- const parentKey = this.treeData.categoryEntityDef?.parentKey;
7671
- if (!parentKey) {
7672
- return; // No parent key means flat structure
7699
+ catch {
7700
+ // Root might not exist
7673
7701
  }
7674
- // Use current selectedNodeIds signal value (may have been updated)
7675
- const currentSelectedIds = this.selectedNodeIds();
7676
- const selectedSet = new Set(currentSelectedIds);
7677
- const processedParents = new Set();
7678
- const parentsToSelect = new Set();
7679
- // For each selected node, check its parent
7680
- for (const selectedId of currentSelectedIds) {
7681
- if (selectedId === 'all') {
7682
- continue;
7683
- }
7684
- // Get parent ID from cached data
7685
- const nodeData = this.nodeDataCache.get(selectedId);
7686
- if (!nodeData) {
7687
- continue;
7688
- }
7689
- const parentId = nodeData[parentKey];
7690
- if (!parentId || parentId === 'all' || processedParents.has(String(parentId))) {
7691
- continue;
7692
- }
7693
- const parentIdStr = String(parentId);
7694
- processedParents.add(parentIdStr);
7695
- // Load all children of this parent
7702
+ // Expand ancestors in order - this loads them into the tree via datasource callback
7703
+ for (const ancestorId of sortedAncestors) {
7696
7704
  try {
7697
- const childNodes = await this.datasource(parentIdStr);
7698
- if (!childNodes || childNodes.length === 0) {
7699
- continue;
7700
- }
7701
- // Check if all children are selected
7702
- let allChildrenSelected = true;
7703
- let hasChildren = false;
7704
- for (const childNode of childNodes) {
7705
- const childId = String(childNode['id'] ?? '');
7706
- if (childId && childId !== 'all') {
7707
- hasChildren = true;
7708
- if (!selectedSet.has(childId)) {
7709
- allChildrenSelected = false;
7710
- break;
7711
- }
7712
- }
7713
- }
7714
- // If parent has children and all are selected, mark parent for selection
7715
- if (hasChildren && allChildrenSelected) {
7716
- parentsToSelect.add(parentIdStr);
7717
- // Cache parent data if not already cached
7718
- if (!this.nodeDataCache.has(parentIdStr)) {
7719
- const parentData = await this.fetchItemById(parentIdStr);
7720
- if (parentData) {
7721
- this.nodeDataCache.set(parentIdStr, parentData);
7722
- }
7723
- }
7724
- }
7725
- }
7726
- catch (error) {
7727
- console.error(`Error checking parent ${parentIdStr}:`, error);
7728
- }
7729
- }
7730
- // Add parents to selectedNodeIds
7731
- if (parentsToSelect.size > 0) {
7732
- const currentSelected = this.selectedNodeIds();
7733
- const newSelected = [...currentSelected];
7734
- let hasNewSelections = false;
7735
- for (const parentId of parentsToSelect) {
7736
- if (!newSelected.includes(parentId)) {
7737
- newSelected.push(parentId);
7738
- hasNewSelections = true;
7705
+ if (!treeComponent.isNodeExpanded(ancestorId)) {
7706
+ await treeComponent.expandNode(ancestorId);
7707
+ // Small delay to allow tree to process the expansion
7708
+ await new Promise((resolve) => setTimeout(resolve, 30));
7739
7709
  }
7740
7710
  }
7741
- if (hasNewSelections) {
7742
- this.selectedNodeIds.set(newSelected);
7743
- // Recursively check parents of these parents
7744
- const updatedSelected = this.selectedNodeIds();
7745
- await this.selectParentsWithAllChildrenSelected(updatedSelected);
7711
+ catch {
7712
+ // Ancestor might not be in tree yet, continue
7746
7713
  }
7747
7714
  }
7748
7715
  }
7749
7716
  /**
7750
- * Syncs selection state with the tree component
7751
- * Handles cases where nodes might not be in the tree structure yet
7717
+ * Syncs selection state with the tree component.
7718
+ * Selects leaf nodes and manually updates parent states (indeterminate/selected).
7752
7719
  */
7753
7720
  async syncSelectionWithTree(selectedIds) {
7754
7721
  const treeComponent = this.tree();
7755
7722
  if (!treeComponent || selectedIds.length === 0) {
7756
7723
  return;
7757
7724
  }
7758
- // Wait for tree to be fully initialized and rendered
7759
- await new Promise((resolve) => setTimeout(resolve, 300));
7725
+ // Wait for tree to be fully initialized after ancestor expansion
7726
+ await new Promise((resolve) => setTimeout(resolve, 200));
7727
+ // Keep track of successfully synced nodes
7728
+ const syncedNodes = new Set();
7760
7729
  // Try multiple times to sync selection (nodes might load progressively)
7761
- for (let attempt = 0; attempt < 3; attempt++) {
7762
- let syncedCount = 0;
7763
- selectedIds.forEach((id) => {
7764
- if (id && id !== 'all') {
7730
+ for (let attempt = 0; attempt < 5; attempt++) {
7731
+ for (const id of selectedIds) {
7732
+ if (id && id !== 'all' && !syncedNodes.has(id)) {
7765
7733
  try {
7766
7734
  const node = treeComponent.findNode(id);
7767
7735
  if (node) {
7768
- // Node exists in tree, select it
7769
- treeComponent.selectNode(id);
7770
- syncedCount++;
7736
+ // Node exists in tree - mark it as selected using both methods
7737
+ // 1. Direct property assignment for immediate visual feedback
7738
+ node['selected'] = true;
7739
+ node['indeterminate'] = false;
7740
+ // 2. Use selectNode API for tree's internal state tracking
7741
+ try {
7742
+ treeComponent.selectNode(id);
7743
+ }
7744
+ catch {
7745
+ // selectNode might fail but direct assignment should work
7746
+ }
7747
+ syncedNodes.add(id);
7771
7748
  }
7772
7749
  }
7773
7750
  catch {
7774
- // Node not in tree yet - will be selected when loaded via datasource callback
7751
+ // Node not in tree yet
7775
7752
  }
7776
7753
  }
7777
- });
7754
+ }
7778
7755
  // If all nodes are synced, we're done
7779
- if (syncedCount === selectedIds.length) {
7756
+ if (syncedNodes.size === selectedIds.length) {
7780
7757
  break;
7781
7758
  }
7782
- // Wait a bit before next attempt
7783
- if (attempt < 2) {
7784
- await new Promise((resolve) => setTimeout(resolve, 100));
7759
+ // Wait before next attempt
7760
+ await new Promise((resolve) => setTimeout(resolve, 100));
7761
+ }
7762
+ // Now update parent states (indeterminate/selected) based on children
7763
+ // This uses bottom-up traversal with datasource fallback for loading children
7764
+ await this.updateParentStatesAfterSelection(selectedIds);
7765
+ // Refresh tree to apply changes
7766
+ treeComponent.refresh();
7767
+ // Trigger change detection to update UI
7768
+ this.changeDetectorRef.markForCheck();
7769
+ }
7770
+ /**
7771
+ * Updates parent node states (selected/indeterminate) based on children selection.
7772
+ * Called after leaf nodes are selected to properly show parent states.
7773
+ * Works bottom-up from deepest parents to root, tracking states in a Map.
7774
+ */
7775
+ async updateParentStatesAfterSelection(selectedLeafIds) {
7776
+ if (!this.treeData || !this.treeConfig || selectedLeafIds.length === 0) {
7777
+ return;
7778
+ }
7779
+ const parentKey = this.treeData.categoryEntityDef?.parentKey;
7780
+ if (!parentKey) {
7781
+ return;
7782
+ }
7783
+ const treeComponent = this.tree();
7784
+ if (!treeComponent) {
7785
+ return;
7786
+ }
7787
+ // Track node states: { selected: boolean, indeterminate: boolean }
7788
+ // This allows us to reference child states when processing parents
7789
+ const nodeStates = new Map();
7790
+ // Initialize with selected leaf nodes
7791
+ for (const leafId of selectedLeafIds) {
7792
+ nodeStates.set(leafId, { selected: true, indeterminate: false });
7793
+ }
7794
+ // Collect all unique parent IDs from selected nodes
7795
+ const parentsToUpdate = new Set();
7796
+ for (const leafId of selectedLeafIds) {
7797
+ const nodeData = this.nodeDataCache.get(leafId);
7798
+ if (!nodeData) {
7799
+ continue;
7800
+ }
7801
+ // Traverse up the parent chain
7802
+ let currentData = nodeData;
7803
+ while (currentData) {
7804
+ const parentId = currentData[parentKey];
7805
+ if (!parentId || parentId === 'all') {
7806
+ break;
7807
+ }
7808
+ parentsToUpdate.add(String(parentId));
7809
+ currentData = this.nodeDataCache.get(String(parentId));
7785
7810
  }
7786
7811
  }
7787
- // After syncing, read back from tree component to get any parents that were automatically selected
7788
- // (due to intermediate-nested behavior when all children are selected)
7789
- if (this.allowMultiple()) {
7812
+ // Sort parents by depth (deepest first so we update bottom-up)
7813
+ const sortedParents = await this.sortParentsByDepth(Array.from(parentsToUpdate));
7814
+ const reversedParents = [...sortedParents].reverse(); // Deepest first
7815
+ for (const parentId of reversedParents) {
7790
7816
  try {
7791
- const treeSelectedNodes = treeComponent.getSelectedNodes();
7792
- const treeSelectedIds = treeSelectedNodes
7793
- .map((node) => String(node['id'] ?? ''))
7794
- .filter((id) => id && id !== 'all');
7795
- // Update selectedNodeIds to match tree component's state (includes auto-selected parents)
7796
- this.selectedNodeIds.set(treeSelectedIds);
7817
+ const parentNode = treeComponent.findNode(parentId);
7818
+ // Get all direct children of this parent
7819
+ // First try from tree node, then fall back to datasource
7820
+ let childrenList = [];
7821
+ const treeChildren = parentNode?.['children'];
7822
+ if (treeChildren && treeChildren.length > 0) {
7823
+ childrenList = treeChildren.map((c) => ({ id: String(c['id'] ?? '') }));
7824
+ }
7825
+ else {
7826
+ // Children not in tree node - load via datasource
7827
+ try {
7828
+ const dsChildren = await this.datasource(parentId);
7829
+ if (dsChildren && dsChildren.length > 0) {
7830
+ childrenList = dsChildren.map((c) => ({ id: String(c['id'] ?? '') }));
7831
+ }
7832
+ }
7833
+ catch {
7834
+ // Datasource failed, skip this parent
7835
+ continue;
7836
+ }
7837
+ }
7838
+ if (childrenList.length === 0) {
7839
+ continue;
7840
+ }
7841
+ // Count child states using our tracked nodeStates Map
7842
+ let fullySelectedCount = 0;
7843
+ let indeterminateCount = 0;
7844
+ let totalCount = 0;
7845
+ for (const child of childrenList) {
7846
+ if (!child.id || child.id === 'all') {
7847
+ continue;
7848
+ }
7849
+ totalCount++;
7850
+ const childState = nodeStates.get(child.id);
7851
+ if (childState) {
7852
+ if (childState.selected && !childState.indeterminate) {
7853
+ // Child is fully selected
7854
+ fullySelectedCount++;
7855
+ }
7856
+ else if (childState.indeterminate) {
7857
+ // Child is indeterminate
7858
+ indeterminateCount++;
7859
+ }
7860
+ // If neither, child is not selected at all
7861
+ }
7862
+ // If childState is undefined, child is not in our tracking = not selected
7863
+ }
7864
+ // Determine parent state based on children
7865
+ let parentSelected = false;
7866
+ let parentIndeterminate = false;
7867
+ if (totalCount > 0) {
7868
+ if (fullySelectedCount === totalCount && indeterminateCount === 0) {
7869
+ // All children are fully selected - parent is fully selected
7870
+ parentSelected = true;
7871
+ parentIndeterminate = false;
7872
+ }
7873
+ else if (fullySelectedCount > 0 || indeterminateCount > 0) {
7874
+ // Some children selected or indeterminate - parent is indeterminate
7875
+ parentSelected = true;
7876
+ parentIndeterminate = true;
7877
+ }
7878
+ // else: no children selected - parent stays unselected
7879
+ }
7880
+ // Store state in our tracking Map (for use by parent's parent)
7881
+ nodeStates.set(parentId, { selected: parentSelected, indeterminate: parentIndeterminate });
7882
+ // Update tree node if it exists
7883
+ if (parentNode) {
7884
+ parentNode['selected'] = parentSelected;
7885
+ parentNode['indeterminate'] = parentIndeterminate;
7886
+ }
7797
7887
  }
7798
- catch {
7799
- // Tree component might not be ready yet
7888
+ catch (error) {
7889
+ console.error(`Error updating parent state for ${parentId}:`, error);
7800
7890
  }
7801
7891
  }
7802
- // Note: Selection state is maintained in selectedNodeIds signal
7803
- // The tree component will sync selection state when nodes are loaded via datasource
7804
- // We mark nodes as selected in the datasource callback by setting node.selected = true
7805
- // Nodes that aren't in the tree yet will be selected when their parent is expanded
7806
7892
  }
7807
7893
  /**
7808
7894
  * Loads node data for IDs that are selected but not yet in the cache.
@@ -7970,7 +8056,7 @@ class AXPEntityCategoryTreeSelectorComponent extends AXBasePageComponent {
7970
8056
  }
7971
8057
  /**
7972
8058
  * Calculate the full path from root to the selected node.
7973
- * Returns an array of strings like ["C", "B"] for node B under parent C.
8059
+ * Returns an array of strings like ["Operations", "IT Operations"] for node "IT Operations" under parent "Operations".
7974
8060
  * Uses tree-view's getNodePath() API when available, otherwise falls back to manual calculation.
7975
8061
  */
7976
8062
  async calculateNodePath(nodeId, nodeData) {
@@ -7982,6 +8068,7 @@ class AXPEntityCategoryTreeSelectorComponent extends AXBasePageComponent {
7982
8068
  if (treeComponent) {
7983
8069
  try {
7984
8070
  // Use tree-view's getNodePath() to get ID path, then convert to titles
8071
+ // Note: getNodePath() returns the path INCLUDING the node itself
7985
8072
  const idPath = treeComponent.getNodePath(nodeId);
7986
8073
  if (idPath && idPath.length > 0) {
7987
8074
  const titlePath = [];
@@ -8006,12 +8093,8 @@ class AXPEntityCategoryTreeSelectorComponent extends AXBasePageComponent {
8006
8093
  }
8007
8094
  }
8008
8095
  }
8009
- // Add current node title
8010
- const currentTitle = this.getNodeTitle(nodeData);
8011
- if (currentTitle) {
8012
- titlePath.push(currentTitle);
8013
- }
8014
- return titlePath.length > 0 ? titlePath : currentTitle ? [currentTitle] : [];
8096
+ // DO NOT add current node title again - it's already included in idPath
8097
+ return titlePath.length > 0 ? titlePath : this.getNodeTitle(nodeData) ? [this.getNodeTitle(nodeData)] : [];
8015
8098
  }
8016
8099
  }
8017
8100
  catch (error) {
@@ -8152,15 +8235,13 @@ class AXPEntityCategoryTreeSelectorComponent extends AXBasePageComponent {
8152
8235
  }
8153
8236
  }
8154
8237
  async getSelectedItems() {
8155
- // Always use selectedNodeIds as the source of truth
8156
- // This ensures we get all selected nodes, even if they're not in the tree structure yet
8238
+ // selectedNodeIds now only contains LEAF nodes (already filtered)
8157
8239
  const selectedIds = this.selectedNodeIds();
8158
8240
  if (selectedIds.length === 0) {
8159
8241
  return [];
8160
8242
  }
8161
8243
  const treeComponent = this.tree();
8162
8244
  // Get node data from cache and calculate paths
8163
- // For nodes not in cache, try to get from tree component
8164
8245
  const items = await Promise.all(selectedIds.map(async (id) => {
8165
8246
  // First try to get from cache
8166
8247
  let nodeData = this.nodeDataCache.get(id);
@@ -8177,20 +8258,22 @@ class AXPEntityCategoryTreeSelectorComponent extends AXBasePageComponent {
8177
8258
  }
8178
8259
  }
8179
8260
  catch {
8180
- // Node not found in tree, continue with cache lookup
8261
+ // Node not found in tree
8262
+ }
8263
+ }
8264
+ // If still no data, try to fetch from server
8265
+ if (!nodeData) {
8266
+ const fetchedData = await this.fetchItemById(id);
8267
+ if (fetchedData) {
8268
+ nodeData = fetchedData;
8269
+ this.nodeDataCache.set(id, fetchedData);
8181
8270
  }
8182
8271
  }
8183
8272
  // If still no data, skip this node
8184
8273
  if (!nodeData) {
8274
+ console.warn(`Could not find data for selected node: ${id}`);
8185
8275
  return null;
8186
8276
  }
8187
- // When selectionBehavior is intermediate-nested, only return leaf nodes
8188
- if (this.allowMultiple()) {
8189
- const isLeaf = await this.isLeafNode(id, treeComponent ?? null);
8190
- if (!isLeaf) {
8191
- return null; // Skip non-leaf nodes
8192
- }
8193
- }
8194
8277
  const path = await this.calculateNodePath(id, nodeData);
8195
8278
  return {
8196
8279
  ...nodeData,
@@ -8301,6 +8384,13 @@ class AXPEntityCategoryTreeSelectorComponent extends AXBasePageComponent {
8301
8384
  <span class="ax-text-base ax-font-medium ax-text-text-primary">
8302
8385
  {{ '@general:terms.interface.items-selected' | translate | async }}
8303
8386
  </span>
8387
+ <ax-button
8388
+ look="blank"
8389
+ color="ghost"
8390
+ class="ax-text-sm"
8391
+ [text]="'@general:terms.interface.unselect-all' | translate | async"
8392
+ (onClick)="onClearAll()"
8393
+ ></ax-button>
8304
8394
  </div>
8305
8395
  </ax-prefix>
8306
8396
  }
@@ -8425,6 +8515,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImpo
8425
8515
  <span class="ax-text-base ax-font-medium ax-text-text-primary">
8426
8516
  {{ '@general:terms.interface.items-selected' | translate | async }}
8427
8517
  </span>
8518
+ <ax-button
8519
+ look="blank"
8520
+ color="ghost"
8521
+ class="ax-text-sm"
8522
+ [text]="'@general:terms.interface.unselect-all' | translate | async"
8523
+ (onClick)="onClearAll()"
8524
+ ></ax-button>
8428
8525
  </div>
8429
8526
  </ax-prefix>
8430
8527
  }
@@ -8526,6 +8623,13 @@ class AXPEntityCategoryWidgetColumnComponent extends AXPColumnWidgetComponent {
8526
8623
  //#endregion
8527
8624
  //#region ---- Computed Properties (Derived) ----
8528
8625
  this.visibleItems = computed(() => this.displayItems(), ...(ngDevMode ? [{ debugName: "visibleItems" }] : []));
8626
+ this.columnItems = computed(() => {
8627
+ return this.visibleItems().map((item) => ({
8628
+ id: this.getItemId(item),
8629
+ content: this.getItemText(item),
8630
+ originalItem: item, // Store original item for template access
8631
+ }));
8632
+ }, ...(ngDevMode ? [{ debugName: "columnItems" }] : []));
8529
8633
  }
8530
8634
  set rawValueInput(value) {
8531
8635
  this.rawValueSignal.set(value);
@@ -8544,15 +8648,19 @@ class AXPEntityCategoryWidgetColumnComponent extends AXPColumnWidgetComponent {
8544
8648
  textField: this.textField(),
8545
8649
  valueField: this.valueField(),
8546
8650
  item,
8547
- breadcrumb: this.joinPath(this.getItemPath(item)),
8651
+ breadcrumb: this.getBreadcrumbPath(item),
8548
8652
  });
8549
8653
  }
8550
8654
  }
8551
- handleItemClick(index) {
8655
+ handleItemClick(listItem) {
8656
+ const originalItem = listItem.originalItem;
8657
+ if (!originalItem) {
8658
+ return;
8659
+ }
8552
8660
  const items = this.visibleItems();
8553
- if (index < items.length) {
8554
- const item = items[index];
8555
- this.showItemDetail(item, index);
8661
+ const itemIndex = items.findIndex((item) => this.getItemId(item) === listItem.id);
8662
+ if (itemIndex >= 0) {
8663
+ this.showItemDetail(originalItem, itemIndex);
8556
8664
  }
8557
8665
  }
8558
8666
  getItemPath(item) {
@@ -8588,6 +8696,13 @@ class AXPEntityCategoryWidgetColumnComponent extends AXPColumnWidgetComponent {
8588
8696
  }
8589
8697
  return String(get(item, this.valueField()) ?? '');
8590
8698
  }
8699
+ getBreadcrumbPath(item) {
8700
+ const path = this.getItemPath(item);
8701
+ if (path.length === 0) {
8702
+ return '';
8703
+ }
8704
+ return this.joinPath(path);
8705
+ }
8591
8706
  //#endregion
8592
8707
  //#region ---- Private Methods ----
8593
8708
  async extractItemWithPath(item) {
@@ -8703,23 +8818,38 @@ class AXPEntityCategoryWidgetColumnComponent extends AXPColumnWidgetComponent {
8703
8818
  }
8704
8819
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: AXPEntityCategoryWidgetColumnComponent, deps: null, target: i0.ɵɵFactoryTarget.Component }); }
8705
8820
  static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.15", type: AXPEntityCategoryWidgetColumnComponent, isStandalone: true, selector: "axp-entity-category-widget-column", inputs: { rawValue: "rawValue", rowData: "rowData", rawValueInput: ["rawValue", "rawValueInput"] }, host: { classAttribute: "ax-w-full" }, usesInheritance: true, ngImport: i0, template: `
8706
- <div class="ax-flex ax-flex-col ax-gap-1.5">
8707
- @for (item of visibleItems(); track getItemId(item)) {
8708
- <div class=" ax-cursor-pointer" (click)="handleItemClick($index)">
8709
- @if (hasParent(item)) {
8710
- <ax-badge [color]="'primary'" [look]="'twotone'" class="ax-p-0.5" [text]="getItemText(item)">
8711
- <ax-prefix>
8712
- <i class="fal fa-ellipsis-h ax-text-on-primary ax-text-xs ax-ps-1"></i>
8713
- <i class="fal fa-chevron-right rtl:ax-rotate-180 ax-text-on-primary ax-text-xs ax-px-1"></i>
8714
- </ax-prefix>
8715
- </ax-badge>
8716
- } @else {
8717
- <ax-badge [color]="'primary'" [look]="'twotone'" class="ax-p-0.5" [text]="getItemText(item)"> </ax-badge>
8718
- }
8719
- </div>
8720
- }
8721
- </div>
8722
- `, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: AXBadgeModule }, { kind: "component", type: i2$1.AXBadgeComponent, selector: "ax-badge", inputs: ["color", "look", "text"] }, { kind: "ngmodule", type: AXDecoratorModule }, { kind: "component", type: i3$3.AXDecoratorGenericComponent, selector: "ax-footer, ax-header, ax-content, ax-divider, ax-form-hint, ax-prefix, ax-suffix, ax-text, ax-title, ax-subtitle, ax-placeholder, ax-overlay" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
8821
+ <axp-column-item-list [items]="columnItems()" [itemTemplate]="itemTemplate" />
8822
+
8823
+ <!-- Item Template -->
8824
+ <ng-template #itemTemplate let-item>
8825
+ <div>
8826
+ @if (hasParent(item.originalItem)) {
8827
+ <ax-badge
8828
+ [color]="'primary'"
8829
+ [look]="'twotone'"
8830
+ class="ax-p-0.5"
8831
+ [text]="getItemText(item.originalItem)"
8832
+ [axTooltip]="getBreadcrumbPath(item.originalItem)"
8833
+ [axTooltipPlacement]="'top'"
8834
+ >
8835
+ <ax-prefix>
8836
+ <i class="fal fa-ellipsis-h ax-text-on-primary ax-text-xs ax-ps-1"></i>
8837
+ <i class="fal fa-chevron-right rtl:ax-rotate-180 ax-text-on-primary ax-text-xs ax-px-1"></i>
8838
+ </ax-prefix>
8839
+ </ax-badge>
8840
+ } @else {
8841
+ <ax-badge
8842
+ [color]="'primary'"
8843
+ [look]="'twotone'"
8844
+ class="ax-p-0.5"
8845
+ [text]="getItemText(item.originalItem)"
8846
+ [axTooltip]="getBreadcrumbPath(item.originalItem)"
8847
+ [axTooltipPlacement]="'top'"
8848
+ ></ax-badge>
8849
+ }
8850
+ </div>
8851
+ </ng-template>
8852
+ `, isInline: true, dependencies: [{ kind: "component", type: AXPColumnItemListComponent, selector: "axp-column-item-list", inputs: ["items", "itemTemplate"], outputs: ["itemClick"] }, { kind: "ngmodule", type: AXBadgeModule }, { kind: "component", type: i2$1.AXBadgeComponent, selector: "ax-badge", inputs: ["color", "look", "text"] }, { kind: "ngmodule", type: AXDecoratorModule }, { kind: "component", type: i3$3.AXDecoratorGenericComponent, selector: "ax-footer, ax-header, ax-content, ax-divider, ax-form-hint, ax-prefix, ax-suffix, ax-text, ax-title, ax-subtitle, ax-placeholder, ax-overlay" }, { kind: "ngmodule", type: AXTooltipModule }, { kind: "directive", type: i3$4.AXTooltipDirective, selector: "[axTooltip]", inputs: ["axTooltipDisabled", "axTooltip", "axTooltipContext", "axTooltipPlacement", "axTooltipOffsetX", "axTooltipOffsetY", "axTooltipOpenAfter", "axTooltipCloseAfter"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
8723
8853
  }
8724
8854
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: AXPEntityCategoryWidgetColumnComponent, decorators: [{
8725
8855
  type: Component,
@@ -8729,25 +8859,40 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImpo
8729
8859
  class: 'ax-w-full',
8730
8860
  },
8731
8861
  template: `
8732
- <div class="ax-flex ax-flex-col ax-gap-1.5">
8733
- @for (item of visibleItems(); track getItemId(item)) {
8734
- <div class=" ax-cursor-pointer" (click)="handleItemClick($index)">
8735
- @if (hasParent(item)) {
8736
- <ax-badge [color]="'primary'" [look]="'twotone'" class="ax-p-0.5" [text]="getItemText(item)">
8737
- <ax-prefix>
8738
- <i class="fal fa-ellipsis-h ax-text-on-primary ax-text-xs ax-ps-1"></i>
8739
- <i class="fal fa-chevron-right rtl:ax-rotate-180 ax-text-on-primary ax-text-xs ax-px-1"></i>
8740
- </ax-prefix>
8741
- </ax-badge>
8742
- } @else {
8743
- <ax-badge [color]="'primary'" [look]="'twotone'" class="ax-p-0.5" [text]="getItemText(item)"> </ax-badge>
8744
- }
8745
- </div>
8746
- }
8747
- </div>
8862
+ <axp-column-item-list [items]="columnItems()" [itemTemplate]="itemTemplate" />
8863
+
8864
+ <!-- Item Template -->
8865
+ <ng-template #itemTemplate let-item>
8866
+ <div>
8867
+ @if (hasParent(item.originalItem)) {
8868
+ <ax-badge
8869
+ [color]="'primary'"
8870
+ [look]="'twotone'"
8871
+ class="ax-p-0.5"
8872
+ [text]="getItemText(item.originalItem)"
8873
+ [axTooltip]="getBreadcrumbPath(item.originalItem)"
8874
+ [axTooltipPlacement]="'top'"
8875
+ >
8876
+ <ax-prefix>
8877
+ <i class="fal fa-ellipsis-h ax-text-on-primary ax-text-xs ax-ps-1"></i>
8878
+ <i class="fal fa-chevron-right rtl:ax-rotate-180 ax-text-on-primary ax-text-xs ax-px-1"></i>
8879
+ </ax-prefix>
8880
+ </ax-badge>
8881
+ } @else {
8882
+ <ax-badge
8883
+ [color]="'primary'"
8884
+ [look]="'twotone'"
8885
+ class="ax-p-0.5"
8886
+ [text]="getItemText(item.originalItem)"
8887
+ [axTooltip]="getBreadcrumbPath(item.originalItem)"
8888
+ [axTooltipPlacement]="'top'"
8889
+ ></ax-badge>
8890
+ }
8891
+ </div>
8892
+ </ng-template>
8748
8893
  `,
8749
8894
  changeDetection: ChangeDetectionStrategy.OnPush,
8750
- imports: [CommonModule, AXBadgeModule, AXDecoratorModule],
8895
+ imports: [AXPColumnItemListComponent, AXBadgeModule, AXDecoratorModule, AXTooltipModule],
8751
8896
  inputs: ['rawValue', 'rowData'],
8752
8897
  }]
8753
8898
  }], propDecorators: { rawValueInput: [{
@@ -9011,6 +9156,11 @@ class AXPEntityCategoryWidgetEditComponent extends AXPValueWidgetComponent {
9011
9156
  //#endregion
9012
9157
  //#region ---- Private Properties ----
9013
9158
  this.entityDef = signal(null, ...(ngDevMode ? [{ debugName: "entityDef" }] : []));
9159
+ /**
9160
+ * Flag to prevent the value effect from running during internal updates.
9161
+ * This prevents circular dependency when setItems() calls setValue().
9162
+ */
9163
+ this.isInternalUpdate = false;
9014
9164
  //#endregion
9015
9165
  //#region ---- Effects ----
9016
9166
  this.#efEntity = effect(async () => {
@@ -9027,6 +9177,11 @@ class AXPEntityCategoryWidgetEditComponent extends AXPValueWidgetComponent {
9027
9177
  if (!entity) {
9028
9178
  return;
9029
9179
  }
9180
+ // Skip if this is an internal update (e.g., from setItems -> setValue)
9181
+ // This prevents circular dependency
9182
+ if (this.isInternalUpdate) {
9183
+ return;
9184
+ }
9030
9185
  if (value) {
9031
9186
  this.findByValue();
9032
9187
  }
@@ -9144,44 +9299,22 @@ class AXPEntityCategoryWidgetEditComponent extends AXPValueWidgetComponent {
9144
9299
  },
9145
9300
  });
9146
9301
  if (result?.data?.selected && Array.isArray(result.data.selected)) {
9147
- if (this.multiple()) {
9148
- // In multiple mode, merge with existing selection
9149
- const existingItems = this.selectedItems();
9150
- const newItems = result.data.selected;
9151
- // Create a map of existing items by their ID
9152
- const existingItemsMap = new Map();
9153
- existingItems.forEach((item) => {
9154
- const id = String(get(item, this.valueField()) ?? '');
9155
- if (id) {
9156
- existingItemsMap.set(id, item);
9157
- }
9158
- });
9159
- // Merge items, preferring new items (with paths) over existing ones
9160
- const mergedItems = [];
9161
- const processedIds = new Set();
9162
- // First, add all new items (they have paths)
9163
- newItems.forEach((item) => {
9164
- const id = String(get(item, this.valueField()) ?? '');
9165
- if (id && !processedIds.has(id)) {
9166
- mergedItems.push(item);
9167
- processedIds.add(id);
9168
- }
9169
- });
9170
- // Then, add existing items that weren't in new items (preserve them)
9171
- existingItems.forEach((item) => {
9172
- const id = String(get(item, this.valueField()) ?? '');
9173
- if (id && !processedIds.has(id)) {
9174
- mergedItems.push(item);
9175
- processedIds.add(id);
9176
- }
9177
- });
9178
- await this.setItems(mergedItems);
9302
+ // Use the popup result directly as the source of truth
9303
+ // This ensures that deselected items are properly removed
9304
+ const newItems = result.data.selected;
9305
+ if (newItems.length === 0) {
9306
+ // All items were deselected
9307
+ this.clear();
9179
9308
  }
9180
9309
  else {
9181
- // In single mode, replace selection
9182
- await this.setItems(result.data.selected);
9310
+ // Set the new selection (replaces existing selection entirely)
9311
+ await this.setItems(newItems);
9183
9312
  }
9184
9313
  }
9314
+ else if (result?.data?.selected === undefined && result?.data !== undefined) {
9315
+ // User cancelled or closed popup without confirming - don't change anything
9316
+ // This case is handled by the catch block or no result.data.selected
9317
+ }
9185
9318
  }
9186
9319
  catch (error) {
9187
9320
  console.error('Error opening category tree selector:', error);
@@ -9192,10 +9325,20 @@ class AXPEntityCategoryWidgetEditComponent extends AXPValueWidgetComponent {
9192
9325
  }
9193
9326
  }
9194
9327
  clear() {
9195
- this.setValue(null);
9196
- this.clearInput();
9197
- this.selectedItems.set([]);
9198
- this.cdr.markForCheck();
9328
+ // Set flag to prevent circular dependency with the value effect
9329
+ this.isInternalUpdate = true;
9330
+ try {
9331
+ this.setValue(null);
9332
+ this.clearInput();
9333
+ this.selectedItems.set([]);
9334
+ this.cdr.markForCheck();
9335
+ }
9336
+ finally {
9337
+ // Reset flag after a short delay to ensure effect has time to skip
9338
+ setTimeout(() => {
9339
+ this.isInternalUpdate = false;
9340
+ }, 0);
9341
+ }
9199
9342
  }
9200
9343
  //#endregion
9201
9344
  //#region ---- Private Methods ----
@@ -9268,51 +9411,61 @@ class AXPEntityCategoryWidgetEditComponent extends AXPValueWidgetComponent {
9268
9411
  }
9269
9412
  }
9270
9413
  async setItems(items) {
9271
- if (!items || items.length == 0) {
9272
- this.selectedItems.set([]);
9273
- this.setValue(null);
9414
+ // Set flag to prevent circular dependency with the value effect
9415
+ this.isInternalUpdate = true;
9416
+ try {
9417
+ if (!items || items.length == 0) {
9418
+ this.selectedItems.set([]);
9419
+ this.setValue(null);
9420
+ this.cdr.markForCheck();
9421
+ return;
9422
+ }
9423
+ items = castArray(items);
9424
+ this.clearInput();
9425
+ // Ensure all items have paths
9426
+ const itemsWithPaths = await Promise.all(items.map(async (item) => {
9427
+ // If item already has a path array, return it as is
9428
+ if (item.path && Array.isArray(item.path) && item.path.length > 0) {
9429
+ return item;
9430
+ }
9431
+ // Otherwise, calculate the path
9432
+ return await this.calculateItemPath(item);
9433
+ }));
9434
+ this.selectedItems.set(itemsWithPaths);
9435
+ const keys = itemsWithPaths.map((item) => get(item, this.valueField()));
9436
+ // Extract data from valueField and set context by expose path
9437
+ if (this.expose()) {
9438
+ const exposeValue = castArray(this.expose());
9439
+ const itemToExpose = {};
9440
+ exposeValue.forEach((i) => {
9441
+ if (typeof i == 'string') {
9442
+ const values = itemsWithPaths.map((item) => set({}, i, get(item, i)));
9443
+ setSmart(itemToExpose, i, this.singleOrMultiple(values));
9444
+ }
9445
+ else {
9446
+ // Extract data from item by source path and set context by target path
9447
+ const values = this.multiple()
9448
+ ? itemsWithPaths.map((item) => set({}, i.source, get(item, i.source)))
9449
+ : itemsWithPaths.map((item) => get(item, i.source));
9450
+ setSmart(itemToExpose, i.target, this.singleOrMultiple(values));
9451
+ }
9452
+ });
9453
+ this.contextService.patch(itemToExpose, true);
9454
+ // Fire triggers
9455
+ this.setValue(this.singleOrMultiple(keys));
9456
+ }
9457
+ else {
9458
+ this.setValue(this.singleOrMultiple(keys));
9459
+ }
9460
+ // Trigger change detection
9274
9461
  this.cdr.markForCheck();
9275
- return;
9276
- }
9277
- items = castArray(items);
9278
- this.clearInput();
9279
- // Ensure all items have paths
9280
- const itemsWithPaths = await Promise.all(items.map(async (item) => {
9281
- // If item already has a path array, return it as is
9282
- if (item.path && Array.isArray(item.path) && item.path.length > 0) {
9283
- return item;
9284
- }
9285
- // Otherwise, calculate the path
9286
- return await this.calculateItemPath(item);
9287
- }));
9288
- this.selectedItems.set(itemsWithPaths);
9289
- const keys = itemsWithPaths.map((item) => get(item, this.valueField()));
9290
- // Extract data from valueField and set context by expose path
9291
- if (this.expose()) {
9292
- const exposeValue = castArray(this.expose());
9293
- const itemToExpose = {};
9294
- exposeValue.forEach((i) => {
9295
- if (typeof i == 'string') {
9296
- const values = itemsWithPaths.map((item) => set({}, i, get(item, i)));
9297
- setSmart(itemToExpose, i, this.singleOrMultiple(values));
9298
- }
9299
- else {
9300
- // Extract data from item by source path and set context by target path
9301
- const values = this.multiple()
9302
- ? itemsWithPaths.map((item) => set({}, i.source, get(item, i.source)))
9303
- : itemsWithPaths.map((item) => get(item, i.source));
9304
- setSmart(itemToExpose, i.target, this.singleOrMultiple(values));
9305
- }
9306
- });
9307
- this.contextService.patch(itemToExpose, true);
9308
- // Fire triggers
9309
- this.setValue(this.singleOrMultiple(keys));
9310
9462
  }
9311
- else {
9312
- this.setValue(this.singleOrMultiple(keys));
9463
+ finally {
9464
+ // Reset flag after a short delay to ensure effect has time to skip
9465
+ setTimeout(() => {
9466
+ this.isInternalUpdate = false;
9467
+ }, 0);
9313
9468
  }
9314
- // Trigger change detection
9315
- this.cdr.markForCheck();
9316
9469
  }
9317
9470
  singleOrMultiple(values) {
9318
9471
  return this.multiple() ? values : values[0];