@acorex/platform 20.7.8 → 20.7.10

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 (28) hide show
  1. package/common/index.d.ts +26 -5
  2. package/core/index.d.ts +13 -7
  3. package/fesm2022/acorex-platform-common.mjs +5 -1
  4. package/fesm2022/acorex-platform-common.mjs.map +1 -1
  5. package/fesm2022/acorex-platform-core.mjs +20 -11
  6. package/fesm2022/acorex-platform-core.mjs.map +1 -1
  7. package/fesm2022/acorex-platform-layout-builder.mjs +31 -4
  8. package/fesm2022/acorex-platform-layout-builder.mjs.map +1 -1
  9. package/fesm2022/{acorex-platform-layout-components-binding-expression-editor-popup.component-ZnTG7wlJ.mjs → acorex-platform-layout-components-binding-expression-editor-popup.component-Cb6Lk4Ch.mjs} +5 -5
  10. package/fesm2022/{acorex-platform-layout-components-binding-expression-editor-popup.component-ZnTG7wlJ.mjs.map → acorex-platform-layout-components-binding-expression-editor-popup.component-Cb6Lk4Ch.mjs.map} +1 -1
  11. package/fesm2022/acorex-platform-layout-components.mjs +913 -36
  12. package/fesm2022/acorex-platform-layout-components.mjs.map +1 -1
  13. package/fesm2022/acorex-platform-layout-entity.mjs +471 -789
  14. package/fesm2022/acorex-platform-layout-entity.mjs.map +1 -1
  15. package/fesm2022/{acorex-platform-layout-widgets-repeater-widget-column.component-fcCirNxz.mjs → acorex-platform-layout-widgets-repeater-widget-column.component-DnhR00cH.mjs} +2 -2
  16. package/fesm2022/acorex-platform-layout-widgets-repeater-widget-column.component-DnhR00cH.mjs.map +1 -0
  17. package/fesm2022/acorex-platform-layout-widgets.mjs +219 -327
  18. package/fesm2022/acorex-platform-layout-widgets.mjs.map +1 -1
  19. package/fesm2022/{acorex-platform-themes-default-entity-master-list-view.component-DzWjSMSK.mjs → acorex-platform-themes-default-entity-master-list-view.component-HBr-ZTSt.mjs} +20 -5
  20. package/fesm2022/acorex-platform-themes-default-entity-master-list-view.component-HBr-ZTSt.mjs.map +1 -0
  21. package/fesm2022/acorex-platform-themes-default.mjs +2 -2
  22. package/layout/builder/index.d.ts +25 -14
  23. package/layout/components/index.d.ts +295 -2
  24. package/layout/entity/index.d.ts +59 -58
  25. package/layout/widgets/index.d.ts +38 -32
  26. package/package.json +5 -5
  27. package/fesm2022/acorex-platform-layout-widgets-repeater-widget-column.component-fcCirNxz.mjs.map +0 -1
  28. package/fesm2022/acorex-platform-themes-default-entity-master-list-view.component-DzWjSMSK.mjs.map +0 -1
@@ -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
  }
@@ -1346,42 +1353,24 @@ function sortMergedSections(mainSections, extraSections, mainSectionIds) {
1346
1353
  return entityOrderDiff;
1347
1354
  return (a.order ?? 0) - (b.order ?? 0);
1348
1355
  });
1349
- // Build index map for main sections to preserve original order when no explicit order is specified
1350
- const mainSectionIndexMap = new Map();
1351
- mainSections.forEach((s, idx) => mainSectionIndexMap.set(s.id, idx));
1352
1356
  // Middle block: main sections + middle-only sections
1353
- // Sort by explicit order if present, otherwise use original array index for main sections
1354
- // Extra middle sections without order go after main sections
1355
- const middleBlock = [...mainSections, ...middleSections].sort((a, b) => {
1356
- const aHasOrder = a.order !== undefined;
1357
- const bHasOrder = b.order !== undefined;
1358
- // If both have explicit order, sort by order
1359
- if (aHasOrder && bHasOrder) {
1360
- return a.order - b.order;
1361
- }
1362
- // If neither has order, preserve original array position for main sections
1363
- if (!aHasOrder && !bHasOrder) {
1364
- const aIdx = mainSectionIndexMap.get(a.id) ?? Infinity;
1365
- const bIdx = mainSectionIndexMap.get(b.id) ?? Infinity;
1366
- return aIdx - bIdx;
1367
- }
1368
- // If only one has order, the one with order comes first (explicit ordering takes precedence)
1369
- if (aHasOrder)
1370
- return -1;
1371
- return 1;
1357
+ // Sort by order (undefined order is treated as 0)
1358
+ const middleBlock = [...mainSections, ...middleSections];
1359
+ const sortedMiddleBlock = middleBlock.sort((a, b) => {
1360
+ // Treat undefined order as 0 for sorting
1361
+ const aOrder = a.order ?? 0;
1362
+ const bOrder = b.order ?? 0;
1363
+ return aOrder - bOrder;
1372
1364
  });
1373
- return [...beforeSections, ...middleBlock, ...afterSections];
1365
+ return [...beforeSections, ...sortedMiddleBlock, ...afterSections];
1374
1366
  }
1375
1367
 
1376
1368
  class AXPCreateEntityCommand {
1377
1369
  constructor() {
1378
1370
  this.entityForm = inject(AXPEntityFormBuilderService);
1379
- this.settingsService = inject(AXPSettingsService);
1380
1371
  this.entityService = inject(AXPEntityDefinitionRegistryService);
1381
1372
  this.toastService = inject(AXToastService);
1382
1373
  this.translationService = inject(AXTranslationService);
1383
- this.eventService = inject(AXPBroadcastEventService);
1384
- this.platform = inject(AXPlatform);
1385
1374
  this.context = {};
1386
1375
  }
1387
1376
  async execute(input) {
@@ -1404,100 +1393,83 @@ class AXPCreateEntityCommand {
1404
1393
  };
1405
1394
  }
1406
1395
  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
- };
1396
+ let chain = this.entityForm.entity(`${moduleName}.${entityName}`).create(data);
1397
+ chain.actions((actions) => {
1398
+ actions.cancel('@general:actions.cancel.title');
1399
+ actions.submit('@general:actions.create.title');
1400
+ });
1401
+ if (excludeProperties && excludeProperties.length > 0) {
1402
+ chain = chain.exclude(...excludeProperties);
1403
+ }
1404
+ if (includeProperties && includeProperties.length > 0) {
1405
+ chain = chain.include(...includeProperties);
1406
+ }
1407
+ // Set dialog title: use decoration.header.title if available, otherwise use entityInfo.title
1408
+ if (headerTitle) {
1409
+ chain = chain.title(headerTitle);
1410
+ }
1411
+ else if (entityInfo?.title) {
1412
+ const createText = await this.translationService.translateAsync('@general:actions.create.title');
1413
+ const translatedTitle = await this.translationService.translateAsync(entityInfo.title);
1414
+ chain = chain.title(`${createText} ${translatedTitle}`);
1415
+ }
1416
+ // Set dialog size: prioritize layout.size, then dialogSize from input
1417
+ const finalSize = layoutSize || dialogSize;
1418
+ if (finalSize) {
1419
+ chain.size(finalSize);
1420
+ }
1421
+ const result = await chain
1422
+ .onAction(async (dialogRef) => {
1423
+ if (dialogRef.action() === 'cancel') {
1424
+ return { success: false };
1425
+ }
1426
+ const createFn = entityRef.commands?.create?.execute;
1427
+ if (!createFn) {
1428
+ const msg = await this.translationService.translateAsync('@general:messages.entity.create-command-unavailable');
1429
+ this.toastService.show({
1430
+ color: 'danger',
1431
+ title: await this.translationService.translateAsync('@general:messages.generic.error.title'),
1432
+ content: msg,
1433
+ });
1434
+ throw new Error(msg);
1446
1435
  }
1447
- else if (dialogRef.action() == 'submit') {
1448
- dialogRef.setLoading(true);
1436
+ dialogRef.setLoading(true);
1437
+ try {
1449
1438
  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
1439
  const result = await createFn(context);
1460
1440
  if (result) {
1461
- dialogRef.close();
1462
1441
  return {
1463
1442
  success: true,
1464
- data: result,
1443
+ data: result.data ?? result,
1465
1444
  message: {
1466
1445
  text: await this.translationService.translateAsync('@general:messages.generic.success.description'),
1467
1446
  },
1468
1447
  };
1469
1448
  }
1470
1449
  else {
1471
- return (result ?? {
1450
+ return {
1472
1451
  success: false,
1473
1452
  message: {
1474
- text: await this.translationService.translateAsync('@general:messages.entity.command-no-result'),
1453
+ text: await this.translationService.translateAsync('@general:messages.entity.create-failed'),
1475
1454
  },
1476
- });
1455
+ };
1477
1456
  }
1478
1457
  }
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) {
1458
+ catch (e) {
1459
+ const errorMsg = e.message ?? (await this.translationService.translateAsync('@general:messages.entity.create-failed'));
1460
+ this.toastService.show({
1461
+ color: 'danger',
1462
+ title: await this.translationService.translateAsync('@general:messages.generic.error.title'),
1463
+ content: errorMsg,
1464
+ });
1465
+ throw e;
1466
+ }
1467
+ finally {
1498
1468
  dialogRef.setLoading(false);
1499
1469
  }
1500
- }
1470
+ })
1471
+ .show();
1472
+ return result;
1501
1473
  }
1502
1474
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: AXPCreateEntityCommand, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
1503
1475
  static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: AXPCreateEntityCommand, providedIn: 'root' }); }
@@ -2071,7 +2043,7 @@ class AXPEntityDetailPopoverComponent {
2071
2043
  return importantProperties;
2072
2044
  }
2073
2045
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: AXPEntityDetailPopoverComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
2074
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.16", type: AXPEntityDetailPopoverComponent, isStandalone: true, selector: "axp-entity-detail-popover", inputs: { entity: { classPropertyName: "entity", publicName: "entity", isSignal: true, isRequired: true, transformFunction: null }, entityId: { classPropertyName: "entityId", publicName: "entityId", isSignal: true, isRequired: true, transformFunction: null }, textField: { classPropertyName: "textField", publicName: "textField", isSignal: true, isRequired: false, transformFunction: null }, valueField: { classPropertyName: "valueField", publicName: "valueField", isSignal: true, isRequired: false, transformFunction: null }, item: { classPropertyName: "item", publicName: "item", isSignal: true, isRequired: false, transformFunction: null }, breadcrumb: { classPropertyName: "breadcrumb", publicName: "breadcrumb", isSignal: true, isRequired: false, transformFunction: null } }, viewQueries: [{ propertyName: "detailPopover", first: true, predicate: ["detailPopover"], descendants: true, isSignal: true }], ngImport: i0, template: "<ax-popover [openOn]=\"'manual'\" #detailPopover (openChange)=\"onDetailPopoverOpenChange($event)\">\n <div class=\"ax-lightest-surface ax-border ax-rounded-lg ax-shadow-lg ax-p-4 ax-min-w-[400px]\">\n <div class=\"ax-mb-4 ax-border-b ax-pb-2\">\n <h3 class=\"ax-text-base ax-font-semibold ax-text-on-lightest-surface\">\n @if (entityDetails()?.entityData?.[textField()]) {\n {{ entityDetails()?.entityData[textField()] }}\n } @else {\n {{ item()?.[textField()] }}\n }\n </h3>\n @if (breadcrumb()) {\n <div class=\"ax-text-xs ax-text-neutral-500 ax-mt-1\">{{ breadcrumb() }}</div>\n }\n </div>\n @if (isLoadingDetails()) {\n <div class=\"ax-flex ax-items-center ax-justify-center ax-py-8\">\n <ax-loading>Loading details...</ax-loading>\n </div>\n } @else if (entityDetails()) {\n <div class=\"ax-space-y-3 ax-mb-4\">\n <!-- Important Entity Data -->\n @if (entityDetails()?.entityData) {\n <axp-widgets-container [context]=\"entityDetails()?.entityData\">\n <div class=\"ax-space-y-2\">\n @for (item of getEntityPropertiesWithWidgets(); track item.name) {\n <div class=\"ax-flex ax-justify-between ax-items-center\">\n <span class=\"ax-text-sm ax-font-medium\">{{ item.title | translate | async }}:</span>\n <div class=\"ax-flex-1 ax-ml-2 ax-max-w-48\">\n <ng-container axp-widget-renderer [node]=\"item.node\" [mode]=\"'view'\"></ng-container>\n </div>\n </div>\n }\n </div>\n </axp-widgets-container>\n }\n </div>\n <div class=\"ax-flex ax-gap-2 ax-justify-end ax-sm\">\n <ax-button [color]=\"'primary'\" [look]=\"'solid'\" text=\"Open Details\" (click)=\"navigateToDetails()\"> </ax-button>\n </div>\n }\n </div>\n</ax-popover>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: AXButtonModule }, { kind: "component", type: i3.AXButtonComponent, selector: "ax-button", inputs: ["disabled", "size", "tabIndex", "color", "look", "text", "toggleable", "selected", "iconOnly", "type", "loadingText"], outputs: ["onBlur", "onFocus", "onClick", "selectedChange", "toggleableChange", "lookChange", "colorChange", "disabledChange", "loadingTextChange"] }, { kind: "ngmodule", type: AXPopoverModule }, { kind: "component", type: i2.AXPopoverComponent, selector: "ax-popover", inputs: ["width", "disabled", "offsetX", "offsetY", "target", "placement", "content", "openOn", "closeOn", "hasBackdrop", "openAfter", "closeAfter", "repositionOnScroll", "backdropClass", "panelClass", "adaptivityEnabled"], outputs: ["onOpened", "onClosed"] }, { kind: "ngmodule", type: AXPWidgetCoreModule }, { kind: "component", type: i3$1.AXPWidgetContainerComponent, selector: "axp-widgets-container", inputs: ["context", "functions"], outputs: ["onContextChanged"] }, { kind: "directive", type: i3$1.AXPWidgetRendererDirective, selector: "[axp-widget-renderer]", inputs: ["parentNode", "index", "mode", "node"], outputs: ["onOptionsChanged", "onValueChanged", "onLoad"], exportAs: ["widgetRenderer"] }, { kind: "ngmodule", type: AXTranslationModule }, { kind: "ngmodule", type: AXLoadingModule }, { kind: "component", type: i4.AXLoadingComponent, selector: "ax-loading", inputs: ["visible", "type", "context"], outputs: ["visibleChange"] }, { kind: "pipe", type: i5.AsyncPipe, name: "async" }, { kind: "pipe", type: i6.AXTranslatorPipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
2046
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.16", type: AXPEntityDetailPopoverComponent, isStandalone: true, selector: "axp-entity-detail-popover", inputs: { entity: { classPropertyName: "entity", publicName: "entity", isSignal: true, isRequired: true, transformFunction: null }, entityId: { classPropertyName: "entityId", publicName: "entityId", isSignal: true, isRequired: true, transformFunction: null }, textField: { classPropertyName: "textField", publicName: "textField", isSignal: true, isRequired: false, transformFunction: null }, valueField: { classPropertyName: "valueField", publicName: "valueField", isSignal: true, isRequired: false, transformFunction: null }, item: { classPropertyName: "item", publicName: "item", isSignal: true, isRequired: false, transformFunction: null }, breadcrumb: { classPropertyName: "breadcrumb", publicName: "breadcrumb", isSignal: true, isRequired: false, transformFunction: null } }, viewQueries: [{ propertyName: "detailPopover", first: true, predicate: ["detailPopover"], descendants: true, isSignal: true }], ngImport: i0, template: "<ax-popover [openOn]=\"'manual'\" #detailPopover (openChange)=\"onDetailPopoverOpenChange($event)\">\n <div class=\"ax-lightest-surface ax-border ax-rounded-lg ax-shadow-lg ax-p-4 ax-min-w-[400px]\">\n <div class=\"ax-mb-4 ax-border-b ax-pb-2\">\n <h3 class=\"ax-text-base ax-font-semibold ax-text-on-lightest-surface\">\n @if (entityDetails()?.entityData?.[textField()]) {\n {{ entityDetails()?.entityData[textField()] }}\n } @else {\n {{ item()?.[textField()] }}\n }\n </h3>\n @if (breadcrumb()) {\n <div class=\"ax-text-xs ax-text-neutral-500 ax-mt-1\">{{ breadcrumb() }}</div>\n }\n </div>\n @if (isLoadingDetails()) {\n <div class=\"ax-flex ax-items-center ax-justify-center ax-py-8\">\n <ax-loading>Loading details...</ax-loading>\n </div>\n } @else if (entityDetails()) {\n <div class=\"ax-space-y-3 ax-mb-4\">\n <!-- Important Entity Data -->\n @if (entityDetails()?.entityData) {\n <axp-widgets-container [context]=\"entityDetails()?.entityData\">\n <div class=\"ax-space-y-2\">\n @for (item of getEntityPropertiesWithWidgets(); track item.name) {\n <div class=\"ax-flex ax-justify-between ax-items-center\">\n <span class=\"ax-text-sm ax-font-medium\">{{ item.title | translate | async }}:</span>\n <div class=\"ax-flex-1 ax-ml-2 ax-max-w-48\">\n <ng-container axp-widget-renderer [node]=\"item.node\" [mode]=\"'view'\"></ng-container>\n </div>\n </div>\n }\n </div>\n </axp-widgets-container>\n }\n </div>\n <div class=\"ax-flex ax-gap-2 ax-justify-end ax-sm\">\n <ax-button [color]=\"'primary'\" [look]=\"'solid'\" text=\"Open Details\" (click)=\"navigateToDetails()\"> </ax-button>\n </div>\n }\n </div>\n</ax-popover>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: AXButtonModule }, { kind: "component", type: i3.AXButtonComponent, selector: "ax-button", inputs: ["disabled", "size", "tabIndex", "color", "look", "text", "toggleable", "selected", "iconOnly", "type", "loadingText"], outputs: ["onBlur", "onFocus", "onClick", "selectedChange", "toggleableChange", "lookChange", "colorChange", "disabledChange", "loadingTextChange"] }, { kind: "ngmodule", type: AXPopoverModule }, { kind: "component", type: i2.AXPopoverComponent, selector: "ax-popover", inputs: ["width", "forceDisableActionSheetStyle", "disabled", "offsetX", "offsetY", "target", "placement", "content", "openOn", "closeOn", "hasBackdrop", "openAfter", "closeAfter", "repositionOnScroll", "backdropClass", "panelClass", "adaptivityEnabled"], outputs: ["onOpened", "onClosed"] }, { kind: "ngmodule", type: AXPWidgetCoreModule }, { kind: "component", type: i3$1.AXPWidgetContainerComponent, selector: "axp-widgets-container", inputs: ["context", "functions"], outputs: ["onContextChanged"] }, { kind: "directive", type: i3$1.AXPWidgetRendererDirective, selector: "[axp-widget-renderer]", inputs: ["parentNode", "index", "mode", "node"], outputs: ["onOptionsChanged", "onValueChanged", "onLoad"], exportAs: ["widgetRenderer"] }, { kind: "ngmodule", type: AXTranslationModule }, { kind: "ngmodule", type: AXLoadingModule }, { kind: "component", type: i4.AXLoadingComponent, selector: "ax-loading", inputs: ["visible", "type", "context"], outputs: ["visibleChange"] }, { kind: "pipe", type: i5.AsyncPipe, name: "async" }, { kind: "pipe", type: i6.AXTranslatorPipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
2075
2047
  }
2076
2048
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: AXPEntityDetailPopoverComponent, decorators: [{
2077
2049
  type: Component,
@@ -2919,6 +2891,12 @@ class AXPEntityMasterListViewModel {
2919
2891
  key: 'id',
2920
2892
  });
2921
2893
  this.selectedItems = signal([], ...(ngDevMode ? [{ debugName: "selectedItems" }] : []));
2894
+ /**
2895
+ * Selected category from the category sidebar (left panel).
2896
+ * Used to set default category when creating new entities via command.
2897
+ * Structure: { id: string; title: string } or null when "All Items" is selected.
2898
+ */
2899
+ this.selectedCategory = signal(null, ...(ngDevMode ? [{ debugName: "selectedCategory" }] : []));
2922
2900
  this.parentKey = computed(() => {
2923
2901
  return this.entityDef.parentKey;
2924
2902
  }, ...(ngDevMode ? [{ debugName: "parentKey" }] : []));
@@ -3189,6 +3167,13 @@ class AXPEntityMasterListViewModel {
3189
3167
  clearSelection() {
3190
3168
  this.selectedItems.set([]);
3191
3169
  }
3170
+ /**
3171
+ * Sets the selected category from the category sidebar.
3172
+ * Call with null to clear (e.g. when "All Items" is selected).
3173
+ */
3174
+ setSelectedCategory(category) {
3175
+ this.selectedCategory.set(category);
3176
+ }
3192
3177
  async getPrimaryActions() {
3193
3178
  const scope = {};
3194
3179
  const actions = await Promise.all(this.allActions()
@@ -3352,6 +3337,8 @@ class AXPEntityMasterListViewModel {
3352
3337
  });
3353
3338
  const command = commandName.split('&')[0];
3354
3339
  const options = await this.evaluateExpressions(action?.options, data);
3340
+ const baseData = action?.scope == AXPEntityCommandScope.Selected ? this.selectedItems() : data;
3341
+ const commandData = this.mergeDefaultCategoryForCreate(command, baseData);
3355
3342
  if (this.workflow.exists(command)) {
3356
3343
  await this.workflow.execute(command, {
3357
3344
  entity: getEntityInfo(this.entityDef).source,
@@ -3362,7 +3349,7 @@ class AXPEntityMasterListViewModel {
3362
3349
  parentKey: this.entityDef.parentKey,
3363
3350
  source: `${this.entityDef.module}.${this.entityDef.name}`,
3364
3351
  },
3365
- data: action?.scope == AXPEntityCommandScope.Selected ? this.selectedItems() : data,
3352
+ data: commandData,
3366
3353
  options: options,
3367
3354
  metadata: action?.metadata,
3368
3355
  });
@@ -3370,7 +3357,7 @@ class AXPEntityMasterListViewModel {
3370
3357
  else {
3371
3358
  const commandOptions = {
3372
3359
  __context__: {
3373
- data: action?.scope == AXPEntityCommandScope.Selected ? this.selectedItems() : data,
3360
+ data: commandData,
3374
3361
  entity: getEntityInfo(this.entityDef).source,
3375
3362
  entityInfo: {
3376
3363
  name: this.entityDef.name,
@@ -3398,6 +3385,41 @@ class AXPEntityMasterListViewModel {
3398
3385
  // });
3399
3386
  }
3400
3387
  }
3388
+ /**
3389
+ * Merges default category from selected category in sidebar when executing create command.
3390
+ * For Entity:Create and create-entity, if entity has category plugin and a category is selected,
3391
+ * pre-fills categoryIds and categories for the create form.
3392
+ */
3393
+ mergeDefaultCategoryForCreate(command, baseData) {
3394
+ const isCreateCommand = command === 'Entity:Create' || command === 'create-entity';
3395
+ if (!isCreateCommand) {
3396
+ return baseData;
3397
+ }
3398
+ const categoryExt = this.entityDef.extensions?.category;
3399
+ if (!categoryExt) {
3400
+ return baseData;
3401
+ }
3402
+ const selectedCat = this.selectedCategory();
3403
+ if (!selectedCat) {
3404
+ return baseData;
3405
+ }
3406
+ const propName = categoryExt.propertyName || 'categoryIds';
3407
+ const valueField = categoryExt.valueField ?? 'id';
3408
+ const textField = categoryExt.textField ?? 'title';
3409
+ const idVal = selectedCat[valueField] ?? selectedCat.id;
3410
+ const titleVal = selectedCat[textField] ?? selectedCat.title;
3411
+ const defaultCategoryData = {
3412
+ [propName]: [idVal],
3413
+ categories: [{ [valueField]: idVal, [textField]: titleVal }],
3414
+ };
3415
+ if (baseData == null || (Array.isArray(baseData) && baseData.length === 0)) {
3416
+ return defaultCategoryData;
3417
+ }
3418
+ if (typeof baseData === 'object' && !Array.isArray(baseData)) {
3419
+ return { ...baseData, ...defaultCategoryData };
3420
+ }
3421
+ return baseData;
3422
+ }
3401
3423
  async execute(command) {
3402
3424
  switch (command?.name) {
3403
3425
  case 'navigate':
@@ -5014,7 +5036,8 @@ class AXPPageListConverter extends AXPBaseRelatedEntityConverter {
5014
5036
  };
5015
5037
  // Don't evaluate actions here - keep expression strings for lazy evaluation at execution time
5016
5038
  // This ensures actions use the latest context data when executed
5017
- const actions = relatedEntity?.actions ?? [];
5039
+ // const actions = relatedEntity?.actions ?? [];
5040
+ const actions = await evaluateExpressions(relatedEntity?.actions ?? []);
5018
5041
  const filters = await Promise.all(relatedEntity.conditions?.map(async (c) => {
5019
5042
  const value = await evaluateExpressions(c.value);
5020
5043
  return {
@@ -5238,7 +5261,7 @@ class AXPTabListConverter extends AXPBaseRelatedEntityConverter {
5238
5261
  }) ?? []);
5239
5262
  // Don't evaluate actions here - keep expression strings for lazy evaluation at execution time
5240
5263
  // This ensures actions use the latest context data when executed
5241
- const actions = relatedEntity.actions;
5264
+ const actions = await evaluateExpressions(relatedEntity.actions);
5242
5265
  return {
5243
5266
  id: entityDef?.name ?? '',
5244
5267
  title: relatedEntity.title ?? entityDef?.title ?? '',
@@ -6171,12 +6194,12 @@ const columnWidthMiddlewareProvider = {
6171
6194
  * Default order for common sections
6172
6195
  */
6173
6196
  const DEFAULT_SECTION_ORDER = {
6174
- 'basic-info': 0,
6197
+ 'basic-info': -100,
6175
6198
  classification: 20,
6176
- appearance: 30,
6177
- settings: 100,
6199
+ appearance: 90,
6178
6200
  advanced: 110,
6179
6201
  metadata: 120,
6202
+ settings: 200,
6180
6203
  };
6181
6204
  /**
6182
6205
  * Default order for common properties
@@ -6640,11 +6663,22 @@ class AXPCategoryTreeService {
6640
6663
  convertToTreeNode(item, config) {
6641
6664
  const textField = config.textField ?? 'title';
6642
6665
  const valueField = config.valueField ?? 'id';
6666
+ // Determine childrenCount properly:
6667
+ // - Use explicit childrenCount from backend if it's a number (including 0)
6668
+ // - Or use children array length if available
6669
+ // - Default to undefined to indicate "unknown" - this allows lazy loading to work
6670
+ let childrenCount;
6671
+ if (typeof item['childrenCount'] === 'number') {
6672
+ childrenCount = item['childrenCount'];
6673
+ }
6674
+ else if (Array.isArray(item['children'])) {
6675
+ childrenCount = item['children'].length;
6676
+ }
6643
6677
  return {
6644
6678
  id: String(item[valueField] ?? ''),
6645
6679
  title: String(item[textField] ?? ''),
6646
6680
  icon: 'fa-solid fa-folder',
6647
- childrenCount: item['childrenCount'] || item['children']?.length || 0,
6681
+ childrenCount,
6648
6682
  expanded: false,
6649
6683
  data: item,
6650
6684
  };
@@ -6800,7 +6834,6 @@ class AXPEntityCategoryTreeSelectorComponent extends AXBasePageComponent {
6800
6834
  this.relevantNodeIds = new Set(); // For search filtering
6801
6835
  this.expandedNodesBeforeSearch = []; // Store expanded nodes before search to restore after
6802
6836
  this.nodesExpandedDuringSearch = []; // Track nodes we expanded during search
6803
- this.isInitializing = false; // Flag to track if we're in initialization phase
6804
6837
  /** Datasource callback for tree-view component. */
6805
6838
  this.datasource = async (id) => {
6806
6839
  if (!this.treeData || !this.treeConfig) {
@@ -6847,31 +6880,48 @@ class AXPEntityCategoryTreeSelectorComponent extends AXBasePageComponent {
6847
6880
  }
6848
6881
  });
6849
6882
  }
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
- }
6883
+ // Mark pre-selected nodes as selected in the child nodes (for visual display)
6884
+ // ONLY do this during initial load, NOT during user selection changes
6885
+ if (!this.isUpdatingSelection) {
6886
+ const selectedIds = this.selectedNodeIds();
6887
+ childNodes.forEach((node) => {
6888
+ const nodeId = String(node['id'] ?? '');
6889
+ if (nodeId && selectedIds.includes(nodeId)) {
6890
+ node['selected'] = true;
6891
+ }
6892
+ });
6893
+ // After children load, programmatically select pre-selected nodes in tree component
6894
+ // This ensures the tree's internal state matches our selectedNodeIds
6895
+ // Skip this if we're in the middle of a selection update (user is selecting/deselecting)
6896
+ const treeComponent = this.tree();
6897
+ if (treeComponent && selectedIds.length > 0) {
6898
+ // Use setTimeout to ensure nodes are in tree structure before selecting
6899
+ setTimeout(() => {
6900
+ // Double-check we're not in a selection update when timeout fires
6901
+ if (this.isUpdatingSelection) {
6902
+ return;
6872
6903
  }
6873
- });
6874
- }, 10);
6904
+ this.isUpdatingSelection = true;
6905
+ try {
6906
+ // Re-check selectedNodeIds at this point (might have changed)
6907
+ const currentSelectedIds = this.selectedNodeIds();
6908
+ childNodes.forEach((node) => {
6909
+ const nodeId = String(node['id'] ?? '');
6910
+ if (nodeId && currentSelectedIds.includes(nodeId) && nodeId !== 'all') {
6911
+ try {
6912
+ treeComponent.selectNode(nodeId);
6913
+ }
6914
+ catch {
6915
+ // Node might not be in tree yet
6916
+ }
6917
+ }
6918
+ });
6919
+ }
6920
+ finally {
6921
+ this.isUpdatingSelection = false;
6922
+ }
6923
+ }, 10);
6924
+ }
6875
6925
  }
6876
6926
  return childNodes;
6877
6927
  };
@@ -6890,7 +6940,6 @@ class AXPEntityCategoryTreeSelectorComponent extends AXBasePageComponent {
6890
6940
  return;
6891
6941
  }
6892
6942
  this.loading.set(true);
6893
- this.isInitializing = true; // Mark that we're in initialization phase
6894
6943
  try {
6895
6944
  this.treeConfig = {
6896
6945
  entityKey: this.entityKey(),
@@ -6900,7 +6949,6 @@ class AXPEntityCategoryTreeSelectorComponent extends AXBasePageComponent {
6900
6949
  this.treeData = await this.categoryTreeService.initializeCategoryTree(this.treeConfig);
6901
6950
  if (!this.treeData) {
6902
6951
  this.loading.set(false);
6903
- this.isInitializing = false;
6904
6952
  return;
6905
6953
  }
6906
6954
  // Get parentKey from entity definition
@@ -6946,7 +6994,6 @@ class AXPEntityCategoryTreeSelectorComponent extends AXBasePageComponent {
6946
6994
  console.error('Error syncing selection after tree render:', error);
6947
6995
  }
6948
6996
  }
6949
- this.isInitializing = false; // Mark initialization as complete
6950
6997
  }
6951
6998
  //#endregion
6952
6999
  //#region ---- Public Methods ----
@@ -7368,6 +7415,8 @@ class AXPEntityCategoryTreeSelectorComponent extends AXBasePageComponent {
7368
7415
  this.nodesExpandedDuringSearch = [];
7369
7416
  return;
7370
7417
  }
7418
+ // Store current selected IDs before reload
7419
+ const selectedIds = this.selectedNodeIds();
7371
7420
  // Reload tree to show all nodes (no filtering)
7372
7421
  await treeComponent.reloadData();
7373
7422
  // Collapse nodes that were expanded during search (in reverse order - leaves first)
@@ -7386,88 +7435,136 @@ class AXPEntityCategoryTreeSelectorComponent extends AXBasePageComponent {
7386
7435
  }
7387
7436
  // Clear the stored expanded nodes
7388
7437
  this.expandedNodesBeforeSearch = [];
7438
+ // Restore selection state after tree reload
7439
+ if (selectedIds.length > 0) {
7440
+ // Wait for tree to stabilize after reload
7441
+ await new Promise((resolve) => setTimeout(resolve, 100));
7442
+ // Re-sync selection with tree
7443
+ await this.restoreSelectionAfterReload(selectedIds);
7444
+ }
7389
7445
  }
7390
- async onNodeToggle(event) {
7391
- // Tree component handles lazy loading via datasource callback
7446
+ /**
7447
+ * Restores selection state after tree reload.
7448
+ * Expands ancestor nodes and selects the previously selected leaf nodes.
7449
+ */
7450
+ async restoreSelectionAfterReload(selectedIds) {
7451
+ const treeComponent = this.tree();
7452
+ if (!treeComponent || selectedIds.length === 0) {
7453
+ return;
7454
+ }
7455
+ this.isUpdatingSelection = true;
7456
+ try {
7457
+ // Build ancestor chains for selected nodes
7458
+ const ancestorChains = await this.buildAncestorChains(selectedIds);
7459
+ // Expand ancestor nodes to make selected nodes visible
7460
+ await this.expandAncestorNodesInOrder(ancestorChains);
7461
+ // Wait for tree to render expanded nodes
7462
+ await new Promise((resolve) => setTimeout(resolve, 50));
7463
+ // Select the nodes visually in the tree
7464
+ for (const id of selectedIds) {
7465
+ try {
7466
+ treeComponent.selectNode(id);
7467
+ }
7468
+ catch {
7469
+ // Node might not be in tree yet
7470
+ }
7471
+ }
7472
+ }
7473
+ finally {
7474
+ this.isUpdatingSelection = false;
7475
+ }
7392
7476
  }
7393
7477
  async onNodeSelect(event) {
7394
7478
  const node = event.node;
7395
7479
  const nodeId = String(node['id'] ?? '');
7396
- if (!node || nodeId === 'all') {
7480
+ if (!node) {
7397
7481
  return;
7398
7482
  }
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);
7483
+ // Only process USER interactions, not programmatic selections
7484
+ // This prevents infinite loops when datasource callback syncs selection
7485
+ if (!event.isUserInteraction) {
7486
+ return;
7403
7487
  }
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) {
7488
+ // Don't process if we're already updating selection
7489
+ if (this.isUpdatingSelection) {
7412
7490
  return;
7413
7491
  }
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'] ?? '');
7492
+ // Cache node data for getSelectedItems (except for 'all' node)
7493
+ if (nodeId !== 'all') {
7420
7494
  const nodeData = node['data'];
7421
- if (nodeData && nodeId && typeof nodeData === 'object') {
7495
+ if (nodeData && typeof nodeData === 'object' && nodeData !== null && !Array.isArray(nodeData)) {
7422
7496
  this.nodeDataCache.set(nodeId, nodeData);
7423
7497
  }
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
7498
+ }
7499
+ // Check if this node is being selected or deselected
7500
+ const isSelected = node['selected'] === true;
7501
+ if (this.allowMultiple()) {
7433
7502
  const children = node['children'];
7434
7503
  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
- // }
7504
+ // Determine if this node has children (is a parent node)
7505
+ // A node has children if: childrenCount > 0, or has non-empty children array
7506
+ // Note: childrenCount === undefined means "unknown" - we need to check via datasource
7507
+ const hasLoadedChildren = children && children.length > 0;
7508
+ const hasChildrenCount = childrenCount !== undefined && childrenCount > 0;
7509
+ const isDefinitelyLeaf = childrenCount === 0 && (!children || children.length === 0);
7510
+ if (isSelected) {
7511
+ // SELECTION: Only add LEAF nodes to selectedNodeIds
7512
+ this.isUpdatingSelection = true;
7513
+ try {
7514
+ if (nodeId === 'all') {
7515
+ // "All Items" selected - recursively select all leaf descendants
7516
+ await this.selectAllLeafDescendants(nodeId);
7517
+ }
7518
+ else if (isDefinitelyLeaf) {
7519
+ // This is definitely a leaf node - add it directly
7520
+ const currentSelected = new Set(this.selectedNodeIds());
7521
+ currentSelected.add(nodeId);
7522
+ this.selectedNodeIds.set(Array.from(currentSelected));
7523
+ }
7524
+ else if (hasLoadedChildren || hasChildrenCount) {
7525
+ // This node has children - only add its leaf descendants
7526
+ await this.selectAllLeafDescendants(nodeId);
7527
+ }
7528
+ else {
7529
+ // childrenCount is undefined - need to check if it has children
7530
+ const childNodes = await this.datasource(nodeId);
7531
+ if (!childNodes || childNodes.length === 0) {
7532
+ // No children - this is a leaf node, add it
7533
+ const currentSelected = new Set(this.selectedNodeIds());
7534
+ currentSelected.add(nodeId);
7535
+ this.selectedNodeIds.set(Array.from(currentSelected));
7536
+ }
7537
+ else {
7538
+ // Has children - only add leaf descendants
7539
+ await this.selectAllLeafDescendants(nodeId);
7540
+ }
7541
+ }
7542
+ }
7543
+ finally {
7544
+ this.isUpdatingSelection = false;
7545
+ }
7546
+ }
7547
+ else {
7548
+ // DESELECTION: Remove node (if leaf) and all leaf descendants from selectedNodeIds
7549
+ this.isUpdatingSelection = true;
7550
+ try {
7551
+ await this.deselectAllLeafDescendants(nodeId);
7552
+ }
7553
+ finally {
7554
+ this.isUpdatingSelection = false;
7555
+ }
7556
+ }
7557
+ }
7558
+ else {
7559
+ // Single selection mode: just update selectedNodeIds
7560
+ if (isSelected) {
7561
+ this.selectedNodeIds.set([nodeId]);
7562
+ }
7563
+ else {
7564
+ this.selectedNodeIds.set([]);
7565
+ }
7566
+ }
7567
+ }
7471
7568
  async onConfirm() {
7472
7569
  // Use internal selectedNodeIds state which is kept in sync via onNodeSelect events
7473
7570
  const selectedItems = await this.getSelectedItems();
@@ -7480,21 +7577,11 @@ class AXPEntityCategoryTreeSelectorComponent extends AXBasePageComponent {
7480
7577
  * Clears all selected items
7481
7578
  */
7482
7579
  onClearAll() {
7580
+ this.selectedNodeIds.set([]);
7483
7581
  const treeComponent = this.tree();
7484
7582
  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
- }
7583
+ treeComponent.deselectAll();
7495
7584
  }
7496
- // Clear the selection
7497
- this.selectedNodeIds.set([]);
7498
7585
  }
7499
7586
  /**
7500
7587
  * Checks if a node matches the current search term
@@ -7513,543 +7600,207 @@ class AXPEntityCategoryTreeSelectorComponent extends AXBasePageComponent {
7513
7600
  return this.selectedNodeIds().includes(id);
7514
7601
  }
7515
7602
  /**
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
7603
+ * Expands parent nodes, collects all LEAF descendants, and selects them visually.
7604
+ * If the parent itself is a leaf (no children), it will be added.
7519
7605
  */
7520
- async handleCheckboxChange(nodeId, checked) {
7521
- if (!nodeId || nodeId === 'all') {
7606
+ async selectAllLeafDescendants(parentId) {
7607
+ if (!this.treeData || !this.treeConfig) {
7522
7608
  return;
7523
7609
  }
7524
- const id = String(nodeId);
7525
7610
  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
7611
  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
- }
7612
+ const leafIds = new Set();
7613
+ // Expand and collect leaf nodes simultaneously
7614
+ await this.collectLeafNodes(parentId, leafIds, treeComponent);
7615
+ // Also check if the parent itself is a leaf (has no children)
7616
+ if (parentId !== 'all') {
7617
+ const isLeaf = await this.isLeafNodeCheck(parentId);
7618
+ if (isLeaf) {
7619
+ leafIds.add(parentId);
7570
7620
  }
7571
7621
  }
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();
7622
+ if (leafIds.size === 0) {
7623
+ return;
7624
+ }
7625
+ // Update our internal state
7626
+ const currentSelected = new Set(this.selectedNodeIds());
7627
+ for (const leafId of leafIds) {
7628
+ currentSelected.add(leafId);
7629
+ }
7630
+ this.selectedNodeIds.set(Array.from(currentSelected));
7631
+ // Select all leaf nodes visually in the tree
7596
7632
  if (treeComponent) {
7597
- for (const leafId of leafNodes) {
7633
+ for (const leafId of leafIds) {
7598
7634
  try {
7599
- const node = treeComponent.findNode(leafId);
7600
- if (node) {
7601
- treeComponent.deselectNode(leafId);
7602
- }
7635
+ treeComponent.selectNode(leafId);
7603
7636
  }
7604
7637
  catch {
7605
- // Node might not be in tree
7638
+ // Node might not be in tree yet
7606
7639
  }
7607
7640
  }
7608
7641
  }
7609
7642
  }
7610
- finally {
7611
- this.isUpdatingSelection = false;
7643
+ catch (error) {
7644
+ console.error(`Error selecting leaf descendants for node ${parentId}:`, error);
7612
7645
  }
7613
7646
  }
7614
7647
  /**
7615
- * Collects all LEAF descendant node IDs recursively
7616
- * A leaf node is one that has no children
7648
+ * Removes all LEAF descendants from selectedNodeIds.
7649
+ * For 'all' node: clears everything and uses tree's deselectAll().
7650
+ * For other nodes: tree handles visual state via user click.
7617
7651
  */
7618
- async collectLeafDescendants(parentId, collection) {
7619
- if (!this.treeData || !this.treeConfig || !parentId || parentId === 'all') {
7652
+ async deselectAllLeafDescendants(parentId) {
7653
+ if (!this.treeData || !this.treeConfig) {
7620
7654
  return;
7621
7655
  }
7622
7656
  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
7657
+ // Special case: deselecting 'all' clears everything
7658
+ if (parentId === 'all') {
7659
+ this.selectedNodeIds.set([]);
7660
+ // Use tree's deselectAll() to clear all visual selections
7661
+ const treeComponent = this.tree();
7662
+ if (treeComponent) {
7663
+ treeComponent.deselectAll();
7664
+ }
7627
7665
  return;
7628
7666
  }
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
- }
7667
+ const leafIds = new Set();
7668
+ await this.collectLeafNodes(parentId, leafIds);
7669
+ // Also check if the parent itself is a leaf
7670
+ const isLeaf = await this.isLeafNodeCheck(parentId);
7671
+ if (isLeaf) {
7672
+ leafIds.add(parentId);
7649
7673
  }
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
- }
7674
+ if (leafIds.size === 0) {
7675
+ return;
7688
7676
  }
7677
+ // Only update our internal state - tree handles visual state
7678
+ const currentSelected = this.selectedNodeIds();
7679
+ const newSelected = currentSelected.filter((id) => !leafIds.has(id));
7689
7680
  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
7681
  }
7705
- finally {
7706
- this.isUpdatingSelection = false;
7682
+ catch (error) {
7683
+ console.error(`Error deselecting leaf descendants for node ${parentId}:`, error);
7707
7684
  }
7708
7685
  }
7709
7686
  /**
7710
- * Collects all descendant node IDs recursively
7687
+ * Recursively expands parent nodes and collects LEAF node IDs.
7688
+ * When treeComponent is provided, expands each parent node before loading children.
7711
7689
  */
7712
- async collectAllDescendants(parentId, collection) {
7713
- if (!this.treeData || !this.treeConfig || !parentId || parentId === 'all') {
7690
+ async collectLeafNodes(parentId, collection, treeComponent) {
7691
+ if (!this.treeData || !this.treeConfig) {
7714
7692
  return;
7715
7693
  }
7716
7694
  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);
7695
+ let childNodes;
7696
+ if (parentId === 'all') {
7697
+ // Expand 'all' node if tree component provided
7698
+ if (treeComponent) {
7699
+ try {
7700
+ treeComponent.expandNode('all');
7729
7701
  }
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);
7702
+ catch {
7703
+ // Ignore expand errors
7739
7704
  }
7740
- // Recursively collect descendants
7741
- await this.collectAllDescendants(childId, collection);
7742
7705
  }
7706
+ // Get root node's children
7707
+ const rootNodes = await this.datasource();
7708
+ if (!rootNodes || rootNodes.length === 0) {
7709
+ return;
7710
+ }
7711
+ const rootNode = rootNodes[0];
7712
+ childNodes = rootNode['children'] || [];
7743
7713
  }
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));
7714
+ else {
7715
+ // Expand this parent node if tree component provided
7716
+ if (treeComponent) {
7717
+ try {
7718
+ treeComponent.expandNode(parentId);
7719
+ }
7720
+ catch {
7721
+ // Ignore expand errors
7775
7722
  }
7776
7723
  }
7777
- catch {
7778
- // Node might not be in tree
7779
- }
7724
+ // Load children via datasource
7725
+ childNodes = await this.datasource(parentId);
7780
7726
  }
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
7727
  if (!childNodes || childNodes.length === 0) {
7801
7728
  return;
7802
7729
  }
7803
- const childIdsToDeselect = [];
7804
- // Collect all child IDs
7730
+ // Process each child
7805
7731
  for (const childNode of childNodes) {
7806
7732
  const childId = String(childNode['id'] ?? '');
7807
- if (childId && childId !== 'all') {
7808
- childIdsToDeselect.push(childId);
7733
+ if (!childId || childId === 'all' || collection.has(childId)) {
7734
+ continue;
7809
7735
  }
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);
7736
+ // Cache node data
7737
+ this.cacheNodeFromTreeNode(childNode);
7738
+ // Check if this child has children
7739
+ const hasChildren = await this.nodeHasChildren(childId, childNode);
7740
+ if (!hasChildren) {
7741
+ // This is a LEAF node - add it
7742
+ collection.add(childId);
7819
7743
  }
7820
- catch {
7821
- // Node might not be in tree
7744
+ else {
7745
+ // Has children - expand and recurse (pass tree component to expand children too)
7746
+ await this.collectLeafNodes(childId, collection, treeComponent);
7822
7747
  }
7823
7748
  }
7824
- // Recursively deselect children of each child
7825
- await Promise.all(childIdsToDeselect.map((childId) => this.loadAndDeselectChildrenRecursively(childId)));
7826
7749
  }
7827
7750
  catch (error) {
7828
- console.error(`Error deselecting children for node ${parentId}:`, error);
7751
+ console.error(`Error collecting leaf nodes for ${parentId}:`, error);
7829
7752
  }
7830
7753
  }
7831
7754
  /**
7832
- * Updates parent states based on children selection (select/intermediate)
7833
- * Called after a node is selected/deselected to update parent checkbox states
7755
+ * Checks if a node has children
7834
7756
  */
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;
7757
+ async nodeHasChildren(nodeId, node) {
7758
+ // First check node properties if available
7759
+ if (node) {
7760
+ const children = node['children'];
7761
+ const childrenCount = node['childrenCount'];
7762
+ if (children && children.length > 0) {
7763
+ return true;
7860
7764
  }
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;
7765
+ if (childrenCount !== undefined && childrenCount > 0) {
7766
+ return true;
7935
7767
  }
7936
- catch (error) {
7937
- console.error(`Error updating parent state for ${parentIdStr}:`, error);
7938
- break;
7768
+ if (childrenCount === 0) {
7769
+ return false;
7939
7770
  }
7940
7771
  }
7772
+ // Query datasource to check
7773
+ const childNodes = await this.datasource(nodeId);
7774
+ return childNodes && childNodes.length > 0;
7941
7775
  }
7942
7776
  /**
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
7777
+ * Checks if a node is a leaf (has no children)
7945
7778
  */
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
- }
7779
+ async isLeafNodeCheck(nodeId) {
7780
+ const hasChildren = await this.nodeHasChildren(nodeId);
7781
+ return !hasChildren;
8027
7782
  }
8028
- //#endregion
8029
- async updateSelectedNodes(selectedIds) {
8030
- if (!selectedIds || selectedIds.length === 0) {
8031
- this.selectedNodeIds.set([]);
7783
+ /**
7784
+ * Caches node data from a tree node
7785
+ */
7786
+ cacheNodeFromTreeNode(node) {
7787
+ const nodeId = String(node['id'] ?? '');
7788
+ if (!nodeId || !this.treeConfig) {
8032
7789
  return;
8033
7790
  }
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;
7791
+ let nodeData = node['data'];
7792
+ if (!nodeData || typeof nodeData !== 'object') {
7793
+ const valueField = this.treeConfig.valueField || 'id';
7794
+ const textField = this.treeConfig.textField || 'title';
7795
+ nodeData = {
7796
+ [valueField]: nodeId,
7797
+ [textField]: node['title'] ?? '',
7798
+ ...node,
7799
+ };
8051
7800
  }
7801
+ this.nodeDataCache.set(nodeId, nodeData);
8052
7802
  }
7803
+ //#endregion
8053
7804
  /**
8054
7805
  * Builds complete ancestor chains for all selected node IDs.
8055
7806
  * Returns a Map where key is the selected node ID and value is array of ancestor IDs from root to parent.
@@ -8430,42 +8181,38 @@ class AXPEntityCategoryTreeSelectorComponent extends AXBasePageComponent {
8430
8181
  const treeComponent = this.tree();
8431
8182
  if (treeComponent) {
8432
8183
  setTimeout(() => {
8433
- const selectedIds = this.selectedNodeIds();
8434
- selectedIds.forEach((id) => {
8435
- if (id && id !== 'all') {
8436
- treeComponent.selectNode(id);
8437
- }
8438
- });
8184
+ this.isUpdatingSelection = true;
8185
+ try {
8186
+ const selectedIds = this.selectedNodeIds();
8187
+ selectedIds.forEach((id) => {
8188
+ if (id && id !== 'all') {
8189
+ try {
8190
+ treeComponent.selectNode(id);
8191
+ }
8192
+ catch {
8193
+ // Node might not be in tree yet
8194
+ }
8195
+ }
8196
+ });
8197
+ }
8198
+ finally {
8199
+ this.isUpdatingSelection = false;
8200
+ }
8439
8201
  }, 0);
8440
8202
  }
8441
8203
  }
8442
8204
  /**
8443
- * Processes child nodes: marks excluded as disabled, marks selected, and syncs selection
8205
+ * Processes child nodes: marks excluded as disabled
8206
+ * Selection marking is handled in datasource callback ONLY during initial load
8444
8207
  */
8445
8208
  processChildNodes(childNodes) {
8446
8209
  const excludedId = this.excludedNodeId();
8447
- const selectedIds = this.selectedNodeIds();
8448
8210
  childNodes.forEach((node) => {
8449
8211
  const nodeId = String(node['id'] ?? '');
8450
8212
  if (excludedId && nodeId === excludedId) {
8451
8213
  node['disabled'] = true;
8452
8214
  }
8453
- if (nodeId && selectedIds.includes(nodeId)) {
8454
- node['selected'] = true;
8455
- }
8456
8215
  });
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
8216
  }
8470
8217
  /**
8471
8218
  * Caches node data from items array
@@ -8609,67 +8356,6 @@ class AXPEntityCategoryTreeSelectorComponent extends AXBasePageComponent {
8609
8356
  const textField = this.treeConfig.textField || 'title';
8610
8357
  return String(nodeData[textField] ?? '');
8611
8358
  }
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
8359
  async getSelectedItems() {
8674
8360
  // selectedNodeIds now only contains LEAF nodes (already filtered)
8675
8361
  const selectedIds = this.selectedNodeIds();
@@ -8769,8 +8455,6 @@ class AXPEntityCategoryTreeSelectorComponent extends AXBasePageComponent {
8769
8455
  [titleField]="textField()"
8770
8456
  [idField]="valueField()"
8771
8457
  (onNodeSelect)="onNodeSelect($event)"
8772
- (onSelectionChange)="onSelectionChange($event)"
8773
- (onNodeToggle)="onNodeToggle($event)"
8774
8458
  [nodeTemplate]="itemTemplate"
8775
8459
  #tree
8776
8460
  >
@@ -8900,8 +8584,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
8900
8584
  [titleField]="textField()"
8901
8585
  [idField]="valueField()"
8902
8586
  (onNodeSelect)="onNodeSelect($event)"
8903
- (onSelectionChange)="onSelectionChange($event)"
8904
- (onNodeToggle)="onNodeToggle($event)"
8905
8587
  [nodeTemplate]="itemTemplate"
8906
8588
  #tree
8907
8589
  >
@@ -12876,7 +12558,7 @@ class AXPLookupWidgetColumnComponent extends AXPColumnWidgetComponent {
12876
12558
  return get(item, this.displayField()) ?? '';
12877
12559
  }
12878
12560
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: AXPLookupWidgetColumnComponent, deps: null, target: i0.ɵɵFactoryTarget.Component }); }
12879
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.16", type: AXPLookupWidgetColumnComponent, isStandalone: true, selector: "ng-component", inputs: { rawValue: "rawValue", rowData: "rowData" }, viewQueries: [{ propertyName: "moreButton", first: true, predicate: ["moreButton"], descendants: true, isSignal: true }, { propertyName: "morePopover", first: true, predicate: ["morePopover"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<div class=\"ax-flex ax-gap-1 ax-items-center\">\n @if (visibleItems().length > 0) {\n @for (item of visibleItems(); track $index) {\n <span class=\"ax-cursor-pointer hover:ax-text-primary hover:ax-underline\" (click)=\"handleItemClick($index)\">\n {{ getDisplayText(item) }}\n </span>\n @if ($index < visibleItems().length - 1) { <span class=\"ax-text-muted\">\u2022</span>\n }\n }\n } @else {\n <span class=\"ax-text-muted\">---</span>\n }\n\n @if (hasMoreItems()) {\n <span\n class=\"ax-absolute ax-flex ax-items-center ax-end-0 ax-px-1 ax-cursor-pointer ax-h-full hover:ax-primary-lighter\"\n (click)=\"showMoreItems()\" #moreButton>\n <i class=\"fa-light fa-ellipsis-vertical\"></i>\n </span>\n }\n</div>\n\n<!-- More Items Popover -->\n<ax-popover [openOn]=\"'manual'\" #morePopover (openChange)=\"onMorePopoverOpenChange($event)\">\n <div class=\"ax-lightest-surface ax-border ax-rounded-lg ax-shadow-lg ax-p-4 ax-min-w-[280px]\">\n <div class=\"ax-mb-4 ax-border-b ax-pb-2\">\n <h3 class=\"ax-text-base ax-font-semibold\">All {{ allItems().length }} Items</h3>\n </div>\n <div class=\"ax-max-h-64 ax-flex ax-flex-col ax-gap-3\">\n @for (item of allItems(); track $index) {\n <span class=\"ax-cursor-pointer hover:ax-text-primary hover:ax-underline\" (click)=\"showItemDetail(item, $index)\">\n {{ getDisplayText(item) }}\n </span>\n }\n </div>\n </div>\n</ax-popover>", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: AXBadgeModule }, { kind: "ngmodule", type: AXButtonModule }, { kind: "ngmodule", type: AXPopoverModule }, { kind: "component", type: i2.AXPopoverComponent, selector: "ax-popover", inputs: ["width", "disabled", "offsetX", "offsetY", "target", "placement", "content", "openOn", "closeOn", "hasBackdrop", "openAfter", "closeAfter", "repositionOnScroll", "backdropClass", "panelClass", "adaptivityEnabled"], outputs: ["onOpened", "onClosed"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
12561
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.16", type: AXPLookupWidgetColumnComponent, isStandalone: true, selector: "ng-component", inputs: { rawValue: "rawValue", rowData: "rowData" }, viewQueries: [{ propertyName: "moreButton", first: true, predicate: ["moreButton"], descendants: true, isSignal: true }, { propertyName: "morePopover", first: true, predicate: ["morePopover"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<div class=\"ax-flex ax-gap-1 ax-items-center\">\n @if (visibleItems().length > 0) {\n @for (item of visibleItems(); track $index) {\n <span class=\"ax-cursor-pointer hover:ax-text-primary hover:ax-underline\" (click)=\"handleItemClick($index)\">\n {{ getDisplayText(item) }}\n </span>\n @if ($index < visibleItems().length - 1) { <span class=\"ax-text-muted\">\u2022</span>\n }\n }\n } @else {\n <span class=\"ax-text-muted\">---</span>\n }\n\n @if (hasMoreItems()) {\n <span\n class=\"ax-absolute ax-flex ax-items-center ax-end-0 ax-px-1 ax-cursor-pointer ax-h-full hover:ax-primary-lighter\"\n (click)=\"showMoreItems()\" #moreButton>\n <i class=\"fa-light fa-ellipsis-vertical\"></i>\n </span>\n }\n</div>\n\n<!-- More Items Popover -->\n<ax-popover [openOn]=\"'manual'\" #morePopover (openChange)=\"onMorePopoverOpenChange($event)\">\n <div class=\"ax-lightest-surface ax-border ax-rounded-lg ax-shadow-lg ax-p-4 ax-min-w-[280px]\">\n <div class=\"ax-mb-4 ax-border-b ax-pb-2\">\n <h3 class=\"ax-text-base ax-font-semibold\">All {{ allItems().length }} Items</h3>\n </div>\n <div class=\"ax-max-h-64 ax-flex ax-flex-col ax-gap-3\">\n @for (item of allItems(); track $index) {\n <span class=\"ax-cursor-pointer hover:ax-text-primary hover:ax-underline\" (click)=\"showItemDetail(item, $index)\">\n {{ getDisplayText(item) }}\n </span>\n }\n </div>\n </div>\n</ax-popover>", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: AXBadgeModule }, { kind: "ngmodule", type: AXButtonModule }, { kind: "ngmodule", type: AXPopoverModule }, { kind: "component", type: i2.AXPopoverComponent, selector: "ax-popover", inputs: ["width", "forceDisableActionSheetStyle", "disabled", "offsetX", "offsetY", "target", "placement", "content", "openOn", "closeOn", "hasBackdrop", "openAfter", "closeAfter", "repositionOnScroll", "backdropClass", "panelClass", "adaptivityEnabled"], outputs: ["onOpened", "onClosed"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
12880
12562
  }
12881
12563
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: AXPLookupWidgetColumnComponent, decorators: [{
12882
12564
  type: Component,
@@ -14711,11 +14393,11 @@ class AXPEntityModule {
14711
14393
  useClass: AXPEntitiesListDataSourceDefinition,
14712
14394
  multi: true,
14713
14395
  },
14714
- // {
14715
- // provide: AXP_ENTITY_MODIFIER,
14716
- // useValue: layoutOrderingMiddlewareProvider,
14717
- // multi: true,
14718
- // },
14396
+ {
14397
+ provide: AXP_ENTITY_MODIFIER,
14398
+ useValue: layoutOrderingMiddlewareProvider,
14399
+ multi: true,
14400
+ },
14719
14401
  provideCommandSetups([
14720
14402
  {
14721
14403
  key: 'Entity:OpenDetails',
@@ -14884,11 +14566,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
14884
14566
  useClass: AXPEntitiesListDataSourceDefinition,
14885
14567
  multi: true,
14886
14568
  },
14887
- // {
14888
- // provide: AXP_ENTITY_MODIFIER,
14889
- // useValue: layoutOrderingMiddlewareProvider,
14890
- // multi: true,
14891
- // },
14569
+ {
14570
+ provide: AXP_ENTITY_MODIFIER,
14571
+ useValue: layoutOrderingMiddlewareProvider,
14572
+ multi: true,
14573
+ },
14892
14574
  provideCommandSetups([
14893
14575
  {
14894
14576
  key: 'Entity:OpenDetails',
@@ -15052,7 +14734,7 @@ function entityMasterCrudActions(options) {
15052
14734
  create: true,
15053
14735
  delete: true,
15054
14736
  view: true,
15055
- edit: true,
14737
+ edit: false,
15056
14738
  ...options,
15057
14739
  };
15058
14740
  const actions = [];