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