@acorex/platform 20.7.12 → 20.7.15
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 +10 -1
- package/core/index.d.ts +94 -4
- package/fesm2022/{acorex-platform-common-common-settings.provider-BwBLG0Hl.mjs → acorex-platform-common-common-settings.provider-gyb6ohAE.mjs} +15 -1
- package/fesm2022/acorex-platform-common-common-settings.provider-gyb6ohAE.mjs.map +1 -0
- package/fesm2022/acorex-platform-common.mjs +3 -2
- package/fesm2022/acorex-platform-common.mjs.map +1 -1
- package/fesm2022/acorex-platform-core.mjs +428 -157
- package/fesm2022/acorex-platform-core.mjs.map +1 -1
- package/fesm2022/acorex-platform-domain.mjs.map +1 -1
- package/fesm2022/acorex-platform-layout-builder.mjs +90 -74
- package/fesm2022/acorex-platform-layout-builder.mjs.map +1 -1
- package/fesm2022/acorex-platform-layout-components.mjs +954 -1223
- package/fesm2022/acorex-platform-layout-components.mjs.map +1 -1
- package/fesm2022/acorex-platform-layout-entity.mjs +601 -193
- package/fesm2022/acorex-platform-layout-entity.mjs.map +1 -1
- package/fesm2022/acorex-platform-layout-views.mjs +7 -7
- package/fesm2022/acorex-platform-layout-views.mjs.map +1 -1
- package/fesm2022/{acorex-platform-layout-widgets-file-list-popup.component-BfV3spe3.mjs → acorex-platform-layout-widgets-file-list-popup.component-B0omAUil.mjs} +18 -3
- package/fesm2022/acorex-platform-layout-widgets-file-list-popup.component-B0omAUil.mjs.map +1 -0
- package/fesm2022/{acorex-platform-layout-widgets-repeater-widget-column.component-DnhR00cH.mjs → acorex-platform-layout-widgets-repeater-widget-column.component-fcCirNxz.mjs} +2 -2
- package/fesm2022/acorex-platform-layout-widgets-repeater-widget-column.component-fcCirNxz.mjs.map +1 -0
- package/fesm2022/acorex-platform-layout-widgets.mjs +165 -64
- package/fesm2022/acorex-platform-layout-widgets.mjs.map +1 -1
- package/fesm2022/{acorex-platform-themes-default-entity-master-create-view.component-CJcbkSBF.mjs → acorex-platform-themes-default-entity-master-create-view.component-CCiYPMhz.mjs} +29 -26
- package/fesm2022/acorex-platform-themes-default-entity-master-create-view.component-CCiYPMhz.mjs.map +1 -0
- package/fesm2022/{acorex-platform-themes-default-entity-master-list-view.component-HBr-ZTSt.mjs → acorex-platform-themes-default-entity-master-list-view.component-BQODc73e.mjs} +2 -2
- package/fesm2022/{acorex-platform-themes-default-entity-master-list-view.component-HBr-ZTSt.mjs.map → acorex-platform-themes-default-entity-master-list-view.component-BQODc73e.mjs.map} +1 -1
- package/fesm2022/{acorex-platform-themes-default-entity-master-modify-view.component-DAFQ4UI9.mjs → acorex-platform-themes-default-entity-master-modify-view.component-CgLUnYRq.mjs} +3 -4
- package/fesm2022/acorex-platform-themes-default-entity-master-modify-view.component-CgLUnYRq.mjs.map +1 -0
- package/fesm2022/{acorex-platform-themes-default-entity-master-single-view.component-CwHHYmiK.mjs → acorex-platform-themes-default-entity-master-single-view.component-di5w_3K2.mjs} +4 -4
- package/fesm2022/acorex-platform-themes-default-entity-master-single-view.component-di5w_3K2.mjs.map +1 -0
- package/fesm2022/acorex-platform-themes-default.mjs +11 -11
- package/fesm2022/acorex-platform-themes-default.mjs.map +1 -1
- package/fesm2022/acorex-platform-themes-shared.mjs +244 -246
- package/fesm2022/acorex-platform-themes-shared.mjs.map +1 -1
- package/fesm2022/acorex-platform-workflow.mjs +0 -3
- package/fesm2022/acorex-platform-workflow.mjs.map +1 -1
- package/layout/components/index.d.ts +159 -248
- package/layout/entity/index.d.ts +42 -1
- package/layout/widgets/index.d.ts +43 -5
- package/package.json +5 -5
- package/themes/shared/index.d.ts +1 -1
- package/workflow/index.d.ts +33 -30
- package/fesm2022/acorex-platform-common-common-settings.provider-BwBLG0Hl.mjs.map +0 -1
- package/fesm2022/acorex-platform-layout-widgets-file-list-popup.component-BfV3spe3.mjs.map +0 -1
- package/fesm2022/acorex-platform-layout-widgets-repeater-widget-column.component-DnhR00cH.mjs.map +0 -1
- package/fesm2022/acorex-platform-themes-default-entity-master-create-view.component-CJcbkSBF.mjs.map +0 -1
- package/fesm2022/acorex-platform-themes-default-entity-master-modify-view.component-DAFQ4UI9.mjs.map +0 -1
- package/fesm2022/acorex-platform-themes-default-entity-master-single-view.component-CwHHYmiK.mjs.map +0 -1
|
@@ -2043,7 +2043,7 @@ class AXPEntityDetailPopoverComponent {
|
|
|
2043
2043
|
return importantProperties;
|
|
2044
2044
|
}
|
|
2045
2045
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: AXPEntityDetailPopoverComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
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\n [color]=\"'primary'\"\n [look]=\"'solid'\"\n [text]=\"'@general:open-details.title' | translate | async\"\n (click)=\"navigateToDetails()\"\n >\n </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", "
|
|
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\n [color]=\"'primary'\"\n [look]=\"'solid'\"\n [text]=\"'@general:actions.open-details.title' | translate | async\"\n (click)=\"navigateToDetails()\"\n >\n </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 }); }
|
|
2047
2047
|
}
|
|
2048
2048
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: AXPEntityDetailPopoverComponent, decorators: [{
|
|
2049
2049
|
type: Component,
|
|
@@ -2054,7 +2054,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
|
|
|
2054
2054
|
AXPWidgetCoreModule,
|
|
2055
2055
|
AXTranslationModule,
|
|
2056
2056
|
AXLoadingModule,
|
|
2057
|
-
], 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\n [color]=\"'primary'\"\n [look]=\"'solid'\"\n [text]=\"'@general:open-details.title' | translate | async\"\n (click)=\"navigateToDetails()\"\n >\n </ax-button>\n </div>\n }\n </div>\n</ax-popover>\n" }]
|
|
2057
|
+
], 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\n [color]=\"'primary'\"\n [look]=\"'solid'\"\n [text]=\"'@general:actions.open-details.title' | translate | async\"\n (click)=\"navigateToDetails()\"\n >\n </ax-button>\n </div>\n }\n </div>\n</ax-popover>\n" }]
|
|
2058
2058
|
}], propDecorators: { entity: [{ type: i0.Input, args: [{ isSignal: true, alias: "entity", required: true }] }], entityId: [{ type: i0.Input, args: [{ isSignal: true, alias: "entityId", required: true }] }], textField: [{ type: i0.Input, args: [{ isSignal: true, alias: "textField", required: false }] }], valueField: [{ type: i0.Input, args: [{ isSignal: true, alias: "valueField", required: false }] }], item: [{ type: i0.Input, args: [{ isSignal: true, alias: "item", required: false }] }], breadcrumb: [{ type: i0.Input, args: [{ isSignal: true, alias: "breadcrumb", required: false }] }], detailPopover: [{ type: i0.ViewChild, args: ['detailPopover', { isSignal: true }] }] } });
|
|
2059
2059
|
|
|
2060
2060
|
class AXPEntityDetailPopoverService {
|
|
@@ -3864,7 +3864,7 @@ class AXPEntityPerformDeleteAction extends AXPWorkflowAction {
|
|
|
3864
3864
|
for await (const entity of entities) {
|
|
3865
3865
|
const [moduleName, entityName] = entity.source.split('.');
|
|
3866
3866
|
const entityDefinition = await this.entityRegistery.resolve(moduleName, entityName);
|
|
3867
|
-
if (entityDefinition.parentKey && data[entityDefinition.parentKey]) {
|
|
3867
|
+
if (data != null && entityDefinition.parentKey && data[entityDefinition.parentKey]) {
|
|
3868
3868
|
context.setVariable('meta', { ...meta, refreshTargetId: data[entityDefinition.parentKey] });
|
|
3869
3869
|
}
|
|
3870
3870
|
entityDefinitions[entity.source] = entityDefinition;
|
|
@@ -6446,7 +6446,6 @@ const AXPCrudModifier = {
|
|
|
6446
6446
|
if (!queries?.list) {
|
|
6447
6447
|
queries.list = {
|
|
6448
6448
|
execute: async (e) => {
|
|
6449
|
-
console.log({ e });
|
|
6450
6449
|
return await dataService.query(e);
|
|
6451
6450
|
},
|
|
6452
6451
|
type: AXPEntityQueryType.List,
|
|
@@ -6531,12 +6530,28 @@ function isAXPMiddlewareAbortError(error) {
|
|
|
6531
6530
|
const AXP_ENTITY_STORAGE_BACKEND = new InjectionToken('AXP_ENTITY_STORAGE_BACKEND');
|
|
6532
6531
|
const AXP_ENTITY_STORAGE_MIDDLEWARE = new InjectionToken('AXP_ENTITY_STORAGE_MIDDLEWARE');
|
|
6533
6532
|
|
|
6533
|
+
/** In-flight request deduplication - parallel identical read requests share one execution */
|
|
6534
|
+
function getDedupKey(op, entityName, init) {
|
|
6535
|
+
if (op === 'getOne' && init?.id !== undefined) {
|
|
6536
|
+
return `getOne:${entityName}:${String(init.id)}`;
|
|
6537
|
+
}
|
|
6538
|
+
if (op === 'getAll') {
|
|
6539
|
+
return `getAll:${entityName}`;
|
|
6540
|
+
}
|
|
6541
|
+
if (op === 'query' && init?.request) {
|
|
6542
|
+
const r = init.request;
|
|
6543
|
+
return `query:${entityName}:${r.skip ?? 0}:${r.take ?? 0}:${JSON.stringify(r.filter ?? null)}:${JSON.stringify(r.sort ?? null)}:${JSON.stringify(r.params ?? null)}`;
|
|
6544
|
+
}
|
|
6545
|
+
return '';
|
|
6546
|
+
}
|
|
6534
6547
|
class AXPMiddlewareEntityStorageService extends AXPEntityStorageService {
|
|
6535
6548
|
constructor() {
|
|
6536
6549
|
super(...arguments);
|
|
6537
6550
|
this.backend = inject(AXP_ENTITY_STORAGE_BACKEND);
|
|
6538
6551
|
this.allMiddlewares = (inject(AXP_ENTITY_STORAGE_MIDDLEWARE, { optional: true }) || []).slice();
|
|
6539
6552
|
this.injector = inject(EnvironmentInjector);
|
|
6553
|
+
/** In-flight read requests - identical parallel requests share one execution */
|
|
6554
|
+
this.inFlight = new Map();
|
|
6540
6555
|
}
|
|
6541
6556
|
get dbName() {
|
|
6542
6557
|
return this.backend.dbName;
|
|
@@ -6595,6 +6610,19 @@ class AXPMiddlewareEntityStorageService extends AXPEntityStorageService {
|
|
|
6595
6610
|
return this.run(ctx, () => this.backend.initial(ctx.entityName, ctx.data, options));
|
|
6596
6611
|
}
|
|
6597
6612
|
async getOne(entityName, id) {
|
|
6613
|
+
const key = getDedupKey('getOne', entityName, { id });
|
|
6614
|
+
if (key) {
|
|
6615
|
+
const existing = this.inFlight.get(key);
|
|
6616
|
+
if (existing) {
|
|
6617
|
+
return existing;
|
|
6618
|
+
}
|
|
6619
|
+
const promise = this.runWithDedup(key, () => {
|
|
6620
|
+
const ctx = this.createCtx('getOne', entityName, { id });
|
|
6621
|
+
return this.run(ctx, () => this.backend.getOne(ctx.entityName, id));
|
|
6622
|
+
});
|
|
6623
|
+
this.inFlight.set(key, promise);
|
|
6624
|
+
return promise;
|
|
6625
|
+
}
|
|
6598
6626
|
const ctx = this.createCtx('getOne', entityName, { id });
|
|
6599
6627
|
return this.run(ctx, () => this.backend.getOne(ctx.entityName, id));
|
|
6600
6628
|
}
|
|
@@ -6611,13 +6639,47 @@ class AXPMiddlewareEntityStorageService extends AXPEntityStorageService {
|
|
|
6611
6639
|
return this.run(ctx, () => this.backend.insertOne(ctx.entityName, ctx.data));
|
|
6612
6640
|
}
|
|
6613
6641
|
async getAll(entityName) {
|
|
6642
|
+
const key = getDedupKey('getAll', entityName);
|
|
6643
|
+
if (key) {
|
|
6644
|
+
const existing = this.inFlight.get(key);
|
|
6645
|
+
if (existing) {
|
|
6646
|
+
return existing;
|
|
6647
|
+
}
|
|
6648
|
+
const promise = this.runWithDedup(key, () => {
|
|
6649
|
+
const ctx = this.createCtx('getAll', entityName);
|
|
6650
|
+
return this.run(ctx, () => this.backend.getAll(ctx.entityName));
|
|
6651
|
+
});
|
|
6652
|
+
this.inFlight.set(key, promise);
|
|
6653
|
+
return promise;
|
|
6654
|
+
}
|
|
6614
6655
|
const ctx = this.createCtx('getAll', entityName);
|
|
6615
6656
|
return this.run(ctx, () => this.backend.getAll(ctx.entityName));
|
|
6616
6657
|
}
|
|
6617
6658
|
async query(entityName, request) {
|
|
6659
|
+
const key = getDedupKey('query', entityName, { request });
|
|
6660
|
+
if (key) {
|
|
6661
|
+
const existing = this.inFlight.get(key);
|
|
6662
|
+
if (existing) {
|
|
6663
|
+
return existing;
|
|
6664
|
+
}
|
|
6665
|
+
const promise = this.runWithDedup(key, () => {
|
|
6666
|
+
const ctx = this.createCtx('query', entityName, { request });
|
|
6667
|
+
return this.run(ctx, () => this.backend.query(ctx.entityName, request));
|
|
6668
|
+
});
|
|
6669
|
+
this.inFlight.set(key, promise);
|
|
6670
|
+
return promise;
|
|
6671
|
+
}
|
|
6618
6672
|
const ctx = this.createCtx('query', entityName, { request });
|
|
6619
6673
|
return this.run(ctx, () => this.backend.query(ctx.entityName, request));
|
|
6620
6674
|
}
|
|
6675
|
+
async runWithDedup(key, fn) {
|
|
6676
|
+
try {
|
|
6677
|
+
return await fn();
|
|
6678
|
+
}
|
|
6679
|
+
finally {
|
|
6680
|
+
this.inFlight.delete(key);
|
|
6681
|
+
}
|
|
6682
|
+
}
|
|
6621
6683
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: AXPMiddlewareEntityStorageService, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
6622
6684
|
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: AXPMiddlewareEntityStorageService }); }
|
|
6623
6685
|
}
|
|
@@ -6728,6 +6790,42 @@ class AXPCategoryTreeService {
|
|
|
6728
6790
|
return null;
|
|
6729
6791
|
}
|
|
6730
6792
|
}
|
|
6793
|
+
/**
|
|
6794
|
+
* Fetches a single category item by ID. Uses byKey when available (full item with parent),
|
|
6795
|
+
* otherwise falls back to list query with filter so ancestor chains can be built.
|
|
6796
|
+
*/
|
|
6797
|
+
async fetchItemById(id, treeData, config) {
|
|
6798
|
+
const byKey = treeData.categoryEntityDef?.queries?.byKey?.execute;
|
|
6799
|
+
if (typeof byKey === 'function') {
|
|
6800
|
+
try {
|
|
6801
|
+
const item = await byKey(id);
|
|
6802
|
+
return item ?? null;
|
|
6803
|
+
}
|
|
6804
|
+
catch (error) {
|
|
6805
|
+
console.error('Error fetching category by key:', error);
|
|
6806
|
+
return null;
|
|
6807
|
+
}
|
|
6808
|
+
}
|
|
6809
|
+
if (!treeData.categoryEntityQueryFunc)
|
|
6810
|
+
return null;
|
|
6811
|
+
try {
|
|
6812
|
+
const valueField = config.valueField ?? 'id';
|
|
6813
|
+
const event = {
|
|
6814
|
+
...treeData.basicQueryEvent,
|
|
6815
|
+
filter: {
|
|
6816
|
+
field: valueField,
|
|
6817
|
+
value: id,
|
|
6818
|
+
operator: { type: 'equal' },
|
|
6819
|
+
},
|
|
6820
|
+
};
|
|
6821
|
+
const res = await treeData.categoryEntityQueryFunc(event);
|
|
6822
|
+
return res?.items?.[0] ?? null;
|
|
6823
|
+
}
|
|
6824
|
+
catch (error) {
|
|
6825
|
+
console.error('Error fetching category by id:', error);
|
|
6826
|
+
return null;
|
|
6827
|
+
}
|
|
6828
|
+
}
|
|
6731
6829
|
/**
|
|
6732
6830
|
* Load children for a given node
|
|
6733
6831
|
*/
|
|
@@ -6812,8 +6910,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
|
|
|
6812
6910
|
}] });
|
|
6813
6911
|
|
|
6814
6912
|
class AXPEntityCategoryTreeSelectorComponent extends AXBasePageComponent {
|
|
6913
|
+
//#endregion
|
|
6914
|
+
//#region ---- Lifecycle Methods ----
|
|
6815
6915
|
constructor() {
|
|
6816
|
-
super(
|
|
6916
|
+
super();
|
|
6817
6917
|
//#region ---- Services & Dependencies ----
|
|
6818
6918
|
this.categoryTreeService = inject(AXPCategoryTreeService);
|
|
6819
6919
|
this.translationService = inject(AXTranslationService);
|
|
@@ -6826,6 +6926,8 @@ class AXPEntityCategoryTreeSelectorComponent extends AXBasePageComponent {
|
|
|
6826
6926
|
this.valueField = signal('id', ...(ngDevMode ? [{ debugName: "valueField" }] : []));
|
|
6827
6927
|
this.allowMultiple = signal(false, ...(ngDevMode ? [{ debugName: "allowMultiple" }] : []));
|
|
6828
6928
|
this.selectedValues = signal([], ...(ngDevMode ? [{ debugName: "selectedValues" }] : []));
|
|
6929
|
+
/** Optional input for when popup passes selected values via inputs instead of data */
|
|
6930
|
+
this.selectedValuesInput = signal([], ...(ngDevMode ? [{ debugName: "selectedValuesInput" }] : []));
|
|
6829
6931
|
this.searchPlaceholder = signal('@general:terms.interface.category.search.placeholder', ...(ngDevMode ? [{ debugName: "searchPlaceholder" }] : []));
|
|
6830
6932
|
this.excludedNodeId = signal(undefined, ...(ngDevMode ? [{ debugName: "excludedNodeId" }] : [])); // Node ID to disable
|
|
6831
6933
|
this.searchWithChildren = signal(true, ...(ngDevMode ? [{ debugName: "searchWithChildren" }] : [])); // Include children of search results
|
|
@@ -6863,6 +6965,7 @@ class AXPEntityCategoryTreeSelectorComponent extends AXBasePageComponent {
|
|
|
6863
6965
|
this.relevantNodeIds = new Set(); // For search filtering
|
|
6864
6966
|
this.expandedNodesBeforeSearch = []; // Store expanded nodes before search to restore after
|
|
6865
6967
|
this.nodesExpandedDuringSearch = []; // Track nodes we expanded during search
|
|
6968
|
+
this.initialExpandSyncDone = false;
|
|
6866
6969
|
/** Datasource callback for tree-view component. */
|
|
6867
6970
|
this.datasource = async (id) => {
|
|
6868
6971
|
if (!this.treeData || !this.treeConfig) {
|
|
@@ -6954,9 +7057,22 @@ class AXPEntityCategoryTreeSelectorComponent extends AXBasePageComponent {
|
|
|
6954
7057
|
}
|
|
6955
7058
|
return childNodes;
|
|
6956
7059
|
};
|
|
7060
|
+
// When popup sets data after ngOnInit, run initial expand/sync when we get selected ids and tree is ready
|
|
7061
|
+
effect(() => {
|
|
7062
|
+
if (this.initialExpandSyncDone)
|
|
7063
|
+
return;
|
|
7064
|
+
const loading = this.loading();
|
|
7065
|
+
const fromData = this.selectedValues().filter((id) => id && id !== 'all');
|
|
7066
|
+
const fromInput = (this.selectedValuesInput() ?? []).filter((id) => id && id !== 'all');
|
|
7067
|
+
const ids = fromData.length > 0 ? fromData : fromInput;
|
|
7068
|
+
if (loading || ids.length === 0 || !this.treeData || !this.treeConfig)
|
|
7069
|
+
return;
|
|
7070
|
+
if (this.selectedNodeIds().length > 0)
|
|
7071
|
+
return; // already set in initializeTree
|
|
7072
|
+
this.initialExpandSyncDone = true;
|
|
7073
|
+
this.runInitialExpandAndSync(ids);
|
|
7074
|
+
});
|
|
6957
7075
|
}
|
|
6958
|
-
//#endregion
|
|
6959
|
-
//#region ---- Lifecycle Methods ----
|
|
6960
7076
|
ngOnInit() {
|
|
6961
7077
|
super.ngOnInit();
|
|
6962
7078
|
this.initializeTree();
|
|
@@ -6984,15 +7100,14 @@ class AXPEntityCategoryTreeSelectorComponent extends AXBasePageComponent {
|
|
|
6984
7100
|
if (this.treeData.categoryEntityDef?.parentKey) {
|
|
6985
7101
|
this.treeConfig.parentKey = this.treeData.categoryEntityDef.parentKey;
|
|
6986
7102
|
}
|
|
6987
|
-
//
|
|
6988
|
-
|
|
6989
|
-
|
|
6990
|
-
|
|
6991
|
-
const initialSelectedIds = this.selectedValues().filter((id) => id && id !== 'all');
|
|
7103
|
+
// Resolve initial selected IDs from data (popup) or input binding
|
|
7104
|
+
const fromData = this.selectedValues().filter((id) => id && id !== 'all');
|
|
7105
|
+
const fromInput = (this.selectedValuesInput() ?? []).filter((id) => id && id !== 'all');
|
|
7106
|
+
const initialSelectedIds = fromData.length > 0 ? fromData : fromInput;
|
|
6992
7107
|
if (initialSelectedIds.length > 0) {
|
|
6993
|
-
// Load missing node data into cache first (needed for path calculation and node display)
|
|
6994
7108
|
await this.loadMissingNodeData(initialSelectedIds);
|
|
6995
|
-
//
|
|
7109
|
+
// Re-fetch any node that has no parent info (batch query may return minimal fields)
|
|
7110
|
+
await this.ensureParentDataInCache(initialSelectedIds);
|
|
6996
7111
|
this.selectedNodeIds.set(initialSelectedIds);
|
|
6997
7112
|
}
|
|
6998
7113
|
}
|
|
@@ -7003,27 +7118,56 @@ class AXPEntityCategoryTreeSelectorComponent extends AXBasePageComponent {
|
|
|
7003
7118
|
// Now let tree render by setting loading to false
|
|
7004
7119
|
this.loading.set(false);
|
|
7005
7120
|
}
|
|
7006
|
-
// AFTER tree renders,
|
|
7007
|
-
// This must happen after loading.set(false) so the tree component exists in DOM.
|
|
7121
|
+
// AFTER tree renders: wait for tree to exist, then expand path and sync selection
|
|
7008
7122
|
const selectedIds = this.selectedNodeIds();
|
|
7009
7123
|
if (selectedIds.length > 0) {
|
|
7010
|
-
|
|
7011
|
-
|
|
7012
|
-
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
7013
|
-
// Build ancestor chains for path expansion (so selected nodes become visible)
|
|
7014
|
-
const ancestorChains = await this.buildAncestorChains(selectedIds);
|
|
7015
|
-
// Expand ancestor nodes to make selected nodes visible in tree
|
|
7016
|
-
await this.expandAncestorNodesInOrder(ancestorChains);
|
|
7017
|
-
// Wait for tree to process expansions
|
|
7018
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
7019
|
-
// Final sync to ensure all nodes are properly selected in tree component
|
|
7020
|
-
await this.syncSelectionWithTree(selectedIds);
|
|
7021
|
-
}
|
|
7022
|
-
catch (error) {
|
|
7023
|
-
console.error('Error syncing selection after tree render:', error);
|
|
7024
|
-
}
|
|
7124
|
+
this.initialExpandSyncDone = true;
|
|
7125
|
+
this.runExpandAndSyncWhenTreeReady(selectedIds);
|
|
7025
7126
|
}
|
|
7026
7127
|
}
|
|
7128
|
+
/**
|
|
7129
|
+
* Called when popup data arrives after ngOnInit: load data, set selection, expand and sync.
|
|
7130
|
+
*/
|
|
7131
|
+
runInitialExpandAndSync(ids) {
|
|
7132
|
+
this.selectedNodeIds.set(ids);
|
|
7133
|
+
Promise.resolve()
|
|
7134
|
+
.then(() => this.loadMissingNodeData(ids))
|
|
7135
|
+
.then(() => this.ensureParentDataInCache(ids))
|
|
7136
|
+
.then(() => {
|
|
7137
|
+
this.runExpandAndSyncWhenTreeReady(ids);
|
|
7138
|
+
})
|
|
7139
|
+
.catch((err) => console.error('Error in runInitialExpandAndSync:', err));
|
|
7140
|
+
}
|
|
7141
|
+
/**
|
|
7142
|
+
* Runs expand path + sync selection once the tree viewChild is available.
|
|
7143
|
+
* Uses retry loop so we don't run before the view has rendered the tree.
|
|
7144
|
+
*/
|
|
7145
|
+
runExpandAndSyncWhenTreeReady(selectedIds) {
|
|
7146
|
+
const maxWaitMs = 3500;
|
|
7147
|
+
const pollMs = 80;
|
|
7148
|
+
const start = Date.now();
|
|
7149
|
+
const run = async () => {
|
|
7150
|
+
while (Date.now() - start < maxWaitMs) {
|
|
7151
|
+
const treeComponent = this.tree();
|
|
7152
|
+
if (treeComponent) {
|
|
7153
|
+
try {
|
|
7154
|
+
await new Promise((resolve) => setTimeout(resolve, 320));
|
|
7155
|
+
const ancestorChains = await this.buildAncestorChains(selectedIds);
|
|
7156
|
+
await this.expandAncestorNodesInOrder(ancestorChains);
|
|
7157
|
+
await new Promise((resolve) => setTimeout(resolve, 120));
|
|
7158
|
+
await this.syncSelectionWithTree(selectedIds);
|
|
7159
|
+
}
|
|
7160
|
+
catch (error) {
|
|
7161
|
+
console.error('Error syncing selection after tree render:', error);
|
|
7162
|
+
}
|
|
7163
|
+
this.changeDetectorRef.markForCheck();
|
|
7164
|
+
return;
|
|
7165
|
+
}
|
|
7166
|
+
await new Promise((resolve) => setTimeout(resolve, pollMs));
|
|
7167
|
+
}
|
|
7168
|
+
};
|
|
7169
|
+
run();
|
|
7170
|
+
}
|
|
7027
7171
|
//#endregion
|
|
7028
7172
|
//#region ---- Public Methods ----
|
|
7029
7173
|
/**
|
|
@@ -7298,29 +7442,13 @@ class AXPEntityCategoryTreeSelectorComponent extends AXBasePageComponent {
|
|
|
7298
7442
|
}
|
|
7299
7443
|
}
|
|
7300
7444
|
/**
|
|
7301
|
-
* Fetches a single item by ID
|
|
7445
|
+
* Fetches a single item by ID. Uses category tree service so byKey is used when available
|
|
7446
|
+
* (returns full item with parent/parentId for building ancestor chains).
|
|
7302
7447
|
*/
|
|
7303
7448
|
async fetchItemById(id) {
|
|
7304
|
-
if (!this.treeData
|
|
7305
|
-
return null;
|
|
7306
|
-
}
|
|
7307
|
-
try {
|
|
7308
|
-
const valueField = this.treeConfig?.valueField || 'id';
|
|
7309
|
-
const event = {
|
|
7310
|
-
...this.treeData.basicQueryEvent,
|
|
7311
|
-
filter: {
|
|
7312
|
-
field: valueField,
|
|
7313
|
-
value: id,
|
|
7314
|
-
operator: { type: 'equal' },
|
|
7315
|
-
},
|
|
7316
|
-
};
|
|
7317
|
-
const res = await this.treeData.categoryEntityQueryFunc(event);
|
|
7318
|
-
return res?.items?.[0] ?? null;
|
|
7319
|
-
}
|
|
7320
|
-
catch (error) {
|
|
7321
|
-
console.error('Error fetching item by ID:', error);
|
|
7449
|
+
if (!this.treeData || !this.treeConfig)
|
|
7322
7450
|
return null;
|
|
7323
|
-
|
|
7451
|
+
return this.categoryTreeService.fetchItemById(id, this.treeData, this.treeConfig);
|
|
7324
7452
|
}
|
|
7325
7453
|
/**
|
|
7326
7454
|
* Sorts parent IDs by their depth (root first, leaves last)
|
|
@@ -7357,7 +7485,7 @@ class AXPEntityCategoryTreeSelectorComponent extends AXBasePageComponent {
|
|
|
7357
7485
|
break;
|
|
7358
7486
|
}
|
|
7359
7487
|
itemCache.set(currentId, item);
|
|
7360
|
-
currentId =
|
|
7488
|
+
currentId = this.getParentIdFromNodeData(item);
|
|
7361
7489
|
}
|
|
7362
7490
|
if (missingIds.size === 0 || !missingIds.has(parentId)) {
|
|
7363
7491
|
depthMap.set(parentId, depth);
|
|
@@ -7387,7 +7515,7 @@ class AXPEntityCategoryTreeSelectorComponent extends AXBasePageComponent {
|
|
|
7387
7515
|
depth++;
|
|
7388
7516
|
const item = this.nodeDataCache.get(currentId) || itemCache.get(currentId);
|
|
7389
7517
|
if (item) {
|
|
7390
|
-
currentId =
|
|
7518
|
+
currentId = this.getParentIdFromNodeData(item);
|
|
7391
7519
|
}
|
|
7392
7520
|
else {
|
|
7393
7521
|
break;
|
|
@@ -7836,35 +7964,33 @@ class AXPEntityCategoryTreeSelectorComponent extends AXBasePageComponent {
|
|
|
7836
7964
|
*/
|
|
7837
7965
|
async buildAncestorChains(selectedIds) {
|
|
7838
7966
|
const ancestorChains = new Map();
|
|
7839
|
-
if (!this.treeData || !this.treeConfig)
|
|
7967
|
+
if (!this.treeData || !this.treeConfig)
|
|
7840
7968
|
return ancestorChains;
|
|
7841
|
-
|
|
7842
|
-
const parentKey = this.treeData.categoryEntityDef?.parentKey;
|
|
7843
|
-
if (!parentKey) {
|
|
7844
|
-
// No parent key means flat structure
|
|
7969
|
+
if (!this.treeData.categoryEntityDef?.parentKey)
|
|
7845
7970
|
return ancestorChains;
|
|
7846
|
-
}
|
|
7847
7971
|
for (const nodeId of selectedIds) {
|
|
7848
7972
|
const chain = [];
|
|
7849
|
-
let currentData = this.nodeDataCache.get(nodeId);
|
|
7850
|
-
if (!currentData)
|
|
7973
|
+
let currentData = this.nodeDataCache.get(nodeId) ?? null;
|
|
7974
|
+
if (!currentData)
|
|
7851
7975
|
continue;
|
|
7976
|
+
if (!this.getParentIdFromNodeData(currentData)) {
|
|
7977
|
+
const refetched = await this.fetchItemById(nodeId);
|
|
7978
|
+
if (refetched) {
|
|
7979
|
+
currentData = refetched;
|
|
7980
|
+
this.nodeDataCache.set(nodeId, refetched);
|
|
7981
|
+
}
|
|
7852
7982
|
}
|
|
7853
|
-
// Traverse up to root, collecting ancestor IDs
|
|
7854
7983
|
const visited = new Set();
|
|
7855
7984
|
while (currentData) {
|
|
7856
|
-
const parentId = currentData
|
|
7857
|
-
if (!parentId ||
|
|
7985
|
+
const parentId = this.getParentIdFromNodeData(currentData);
|
|
7986
|
+
if (!parentId || visited.has(parentId))
|
|
7858
7987
|
break;
|
|
7859
|
-
|
|
7860
|
-
|
|
7861
|
-
|
|
7862
|
-
|
|
7863
|
-
// Load parent data if not cached
|
|
7864
|
-
if (!this.nodeDataCache.has(parentIdStr)) {
|
|
7865
|
-
const parentData = await this.fetchItemById(parentIdStr);
|
|
7988
|
+
visited.add(parentId);
|
|
7989
|
+
chain.unshift(parentId);
|
|
7990
|
+
if (!this.nodeDataCache.has(parentId)) {
|
|
7991
|
+
const parentData = await this.fetchItemById(parentId);
|
|
7866
7992
|
if (parentData) {
|
|
7867
|
-
this.nodeDataCache.set(
|
|
7993
|
+
this.nodeDataCache.set(parentId, parentData);
|
|
7868
7994
|
currentData = parentData;
|
|
7869
7995
|
}
|
|
7870
7996
|
else {
|
|
@@ -7872,7 +7998,7 @@ class AXPEntityCategoryTreeSelectorComponent extends AXBasePageComponent {
|
|
|
7872
7998
|
}
|
|
7873
7999
|
}
|
|
7874
8000
|
else {
|
|
7875
|
-
currentData = this.nodeDataCache.get(
|
|
8001
|
+
currentData = this.nodeDataCache.get(parentId) ?? null;
|
|
7876
8002
|
}
|
|
7877
8003
|
}
|
|
7878
8004
|
ancestorChains.set(nodeId, chain);
|
|
@@ -7905,30 +8031,41 @@ class AXPEntityCategoryTreeSelectorComponent extends AXBasePageComponent {
|
|
|
7905
8031
|
const sortedAncestors = Array.from(allAncestors).sort((a, b) => {
|
|
7906
8032
|
return (ancestorDepths.get(a) ?? 0) - (ancestorDepths.get(b) ?? 0);
|
|
7907
8033
|
});
|
|
7908
|
-
// First expand root node if not expanded
|
|
7909
8034
|
try {
|
|
7910
8035
|
if (!treeComponent.isNodeExpanded('all')) {
|
|
7911
8036
|
await treeComponent.expandNode('all');
|
|
7912
|
-
await
|
|
8037
|
+
await this.waitForNodeExpanded(treeComponent, 'all');
|
|
7913
8038
|
}
|
|
7914
8039
|
}
|
|
7915
8040
|
catch {
|
|
7916
8041
|
// Root might not exist
|
|
7917
8042
|
}
|
|
7918
|
-
// Expand ancestors in order - this loads them into the tree via datasource callback
|
|
7919
8043
|
for (const ancestorId of sortedAncestors) {
|
|
7920
8044
|
try {
|
|
7921
8045
|
if (!treeComponent.isNodeExpanded(ancestorId)) {
|
|
7922
8046
|
await treeComponent.expandNode(ancestorId);
|
|
7923
|
-
|
|
7924
|
-
await new Promise((resolve) => setTimeout(resolve, 30));
|
|
8047
|
+
await this.waitForNodeExpanded(treeComponent, ancestorId);
|
|
7925
8048
|
}
|
|
7926
8049
|
}
|
|
7927
8050
|
catch {
|
|
7928
|
-
// Ancestor might not be in tree yet
|
|
8051
|
+
// Ancestor might not be in tree yet
|
|
7929
8052
|
}
|
|
7930
8053
|
}
|
|
7931
8054
|
}
|
|
8055
|
+
/** Waits until the node is expanded (tree has loaded children). Polls so deep paths load. */
|
|
8056
|
+
async waitForNodeExpanded(treeComponent, nodeId, maxWaitMs = 2800, pollMs = 60) {
|
|
8057
|
+
const deadline = Date.now() + maxWaitMs;
|
|
8058
|
+
while (Date.now() < deadline) {
|
|
8059
|
+
try {
|
|
8060
|
+
if (treeComponent.isNodeExpanded(nodeId))
|
|
8061
|
+
return;
|
|
8062
|
+
}
|
|
8063
|
+
catch {
|
|
8064
|
+
// Node may not exist yet
|
|
8065
|
+
}
|
|
8066
|
+
await new Promise((r) => setTimeout(r, pollMs));
|
|
8067
|
+
}
|
|
8068
|
+
}
|
|
7932
8069
|
/**
|
|
7933
8070
|
* Syncs selection state with the tree component.
|
|
7934
8071
|
* Selects leaf nodes and manually updates parent states (indeterminate/selected).
|
|
@@ -8014,15 +8151,13 @@ class AXPEntityCategoryTreeSelectorComponent extends AXBasePageComponent {
|
|
|
8014
8151
|
if (!nodeData) {
|
|
8015
8152
|
continue;
|
|
8016
8153
|
}
|
|
8017
|
-
// Traverse up the parent chain
|
|
8018
8154
|
let currentData = nodeData;
|
|
8019
8155
|
while (currentData) {
|
|
8020
|
-
const parentId = currentData
|
|
8021
|
-
if (!parentId
|
|
8156
|
+
const parentId = this.getParentIdFromNodeData(currentData);
|
|
8157
|
+
if (!parentId)
|
|
8022
8158
|
break;
|
|
8023
|
-
|
|
8024
|
-
|
|
8025
|
-
currentData = this.nodeDataCache.get(String(parentId));
|
|
8159
|
+
parentsToUpdate.add(parentId);
|
|
8160
|
+
currentData = this.nodeDataCache.get(parentId);
|
|
8026
8161
|
}
|
|
8027
8162
|
}
|
|
8028
8163
|
// Sort parents by depth (deepest first so we update bottom-up)
|
|
@@ -8114,44 +8249,57 @@ class AXPEntityCategoryTreeSelectorComponent extends AXBasePageComponent {
|
|
|
8114
8249
|
if (!this.treeData || !this.treeConfig || selectedIds.length === 0) {
|
|
8115
8250
|
return;
|
|
8116
8251
|
}
|
|
8117
|
-
// Find IDs that are selected but not in cache
|
|
8118
8252
|
const missingIds = selectedIds.filter((id) => !this.nodeDataCache.has(id));
|
|
8119
|
-
if (missingIds.length === 0)
|
|
8253
|
+
if (missingIds.length === 0)
|
|
8120
8254
|
return;
|
|
8121
|
-
}
|
|
8122
8255
|
try {
|
|
8256
|
+
// Prefer fetching each id via service (uses byKey when available = full item with parent)
|
|
8123
8257
|
const valueField = this.treeConfig.valueField || 'id';
|
|
8124
|
-
const
|
|
8125
|
-
|
|
8126
|
-
|
|
8127
|
-
|
|
8128
|
-
// Query for missing nodes by their IDs
|
|
8129
|
-
const event = {
|
|
8130
|
-
...this.treeData.basicQueryEvent,
|
|
8131
|
-
filter: {
|
|
8132
|
-
filters: missingIds.map((id) => ({
|
|
8133
|
-
field: valueField,
|
|
8134
|
-
value: id,
|
|
8135
|
-
operator: { type: 'equal' },
|
|
8136
|
-
})),
|
|
8137
|
-
logic: 'or',
|
|
8138
|
-
},
|
|
8139
|
-
};
|
|
8140
|
-
const res = await categoryEntityQueryFunc(event);
|
|
8141
|
-
if (res?.items) {
|
|
8142
|
-
// Cache the fetched node data
|
|
8143
|
-
res.items.forEach((item) => {
|
|
8144
|
-
const itemId = String(item[valueField] ?? '');
|
|
8145
|
-
if (itemId) {
|
|
8146
|
-
this.nodeDataCache.set(itemId, item);
|
|
8147
|
-
}
|
|
8148
|
-
});
|
|
8258
|
+
for (const id of missingIds) {
|
|
8259
|
+
const item = await this.fetchItemById(id);
|
|
8260
|
+
if (item)
|
|
8261
|
+
this.nodeDataCache.set(String(item[valueField] ?? id), item);
|
|
8149
8262
|
}
|
|
8150
8263
|
}
|
|
8151
8264
|
catch (error) {
|
|
8152
8265
|
console.error('Error loading missing node data:', error);
|
|
8153
8266
|
}
|
|
8154
8267
|
}
|
|
8268
|
+
/**
|
|
8269
|
+
* For each selected id, if cached item has no parent (e.g. batch query returned minimal fields),
|
|
8270
|
+
* re-fetch by id so we have parent/parentId for building ancestor chains.
|
|
8271
|
+
*/
|
|
8272
|
+
async ensureParentDataInCache(selectedIds) {
|
|
8273
|
+
if (!this.treeConfig)
|
|
8274
|
+
return;
|
|
8275
|
+
for (const id of selectedIds) {
|
|
8276
|
+
const cached = this.nodeDataCache.get(id);
|
|
8277
|
+
if (cached && this.getParentIdFromNodeData(cached) === null) {
|
|
8278
|
+
const full = await this.fetchItemById(id);
|
|
8279
|
+
if (full)
|
|
8280
|
+
this.nodeDataCache.set(id, full);
|
|
8281
|
+
}
|
|
8282
|
+
}
|
|
8283
|
+
}
|
|
8284
|
+
/**
|
|
8285
|
+
* Resolves parent ID from node data: supports nested `parent` object or flat parentId/parentKey.
|
|
8286
|
+
*/
|
|
8287
|
+
getParentIdFromNodeData(item) {
|
|
8288
|
+
if (!item || !this.treeConfig)
|
|
8289
|
+
return null;
|
|
8290
|
+
const valueField = this.treeConfig.valueField || 'id';
|
|
8291
|
+
const parentKey = this.treeData?.categoryEntityDef?.parentKey || 'parentId';
|
|
8292
|
+
const parent = item['parent'];
|
|
8293
|
+
if (parent && typeof parent === 'object') {
|
|
8294
|
+
const id = String(parent[valueField] ?? parent['id'] ?? '');
|
|
8295
|
+
return id && id !== 'all' ? id : null;
|
|
8296
|
+
}
|
|
8297
|
+
const parentIdValue = item[parentKey] ?? item['parentId'];
|
|
8298
|
+
if (parentIdValue == null || parentIdValue === '')
|
|
8299
|
+
return null;
|
|
8300
|
+
const id = String(parentIdValue);
|
|
8301
|
+
return id && id !== 'all' ? id : null;
|
|
8302
|
+
}
|
|
8155
8303
|
/**
|
|
8156
8304
|
* Marks nodes as selected in the tree structure based on selectedNodeIds.
|
|
8157
8305
|
* This ensures pre-selected nodes appear selected when the tree is rendered.
|
|
@@ -8337,29 +8485,18 @@ class AXPEntityCategoryTreeSelectorComponent extends AXBasePageComponent {
|
|
|
8337
8485
|
while (currentId && currentData && !visitedIds.has(currentId)) {
|
|
8338
8486
|
visitedIds.add(currentId);
|
|
8339
8487
|
const title = this.getNodeTitle(currentData);
|
|
8340
|
-
if (title)
|
|
8488
|
+
if (title)
|
|
8341
8489
|
path.unshift(title);
|
|
8342
|
-
|
|
8343
|
-
|
|
8344
|
-
if (!parentId || parentId === 'all' || parentId === currentId) {
|
|
8490
|
+
const parentId = this.getParentIdFromNodeData(currentData);
|
|
8491
|
+
if (!parentId || parentId === currentId)
|
|
8345
8492
|
break;
|
|
8346
|
-
|
|
8347
|
-
let parentData = this.nodeDataCache.get(String(parentId));
|
|
8493
|
+
let parentData = this.nodeDataCache.get(parentId);
|
|
8348
8494
|
if (!parentData && this.treeData.categoryEntityQueryFunc) {
|
|
8349
8495
|
try {
|
|
8350
|
-
const
|
|
8351
|
-
|
|
8352
|
-
|
|
8353
|
-
|
|
8354
|
-
field: valueField,
|
|
8355
|
-
value: parentId,
|
|
8356
|
-
operator: { type: 'equal' },
|
|
8357
|
-
},
|
|
8358
|
-
};
|
|
8359
|
-
const res = await this.treeData.categoryEntityQueryFunc(event);
|
|
8360
|
-
if (res?.items && res.items.length > 0) {
|
|
8361
|
-
parentData = res.items[0];
|
|
8362
|
-
this.nodeDataCache.set(String(parentId), parentData);
|
|
8496
|
+
const fetched = await this.fetchItemById(parentId);
|
|
8497
|
+
if (fetched) {
|
|
8498
|
+
parentData = fetched;
|
|
8499
|
+
this.nodeDataCache.set(parentId, fetched);
|
|
8363
8500
|
}
|
|
8364
8501
|
}
|
|
8365
8502
|
catch (error) {
|
|
@@ -8367,10 +8504,9 @@ class AXPEntityCategoryTreeSelectorComponent extends AXBasePageComponent {
|
|
|
8367
8504
|
break;
|
|
8368
8505
|
}
|
|
8369
8506
|
}
|
|
8370
|
-
if (!parentData)
|
|
8507
|
+
if (!parentData)
|
|
8371
8508
|
break;
|
|
8372
|
-
|
|
8373
|
-
currentId = String(parentId);
|
|
8509
|
+
currentId = parentId;
|
|
8374
8510
|
currentData = parentData;
|
|
8375
8511
|
}
|
|
8376
8512
|
return path.length > 0 ? path : this.getNodeTitle(nodeData) ? [this.getNodeTitle(nodeData)] : [];
|
|
@@ -8434,7 +8570,7 @@ class AXPEntityCategoryTreeSelectorComponent extends AXBasePageComponent {
|
|
|
8434
8570
|
}));
|
|
8435
8571
|
return items.filter((item) => item != null);
|
|
8436
8572
|
}
|
|
8437
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: AXPEntityCategoryTreeSelectorComponent, deps:
|
|
8573
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: AXPEntityCategoryTreeSelectorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
8438
8574
|
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.16", type: AXPEntityCategoryTreeSelectorComponent, isStandalone: true, selector: "axp-entity-category-tree-selector", viewQueries: [{ propertyName: "tree", first: true, predicate: ["tree"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: `
|
|
8439
8575
|
<div class="ax-flex ax-flex-col ax-h-full">
|
|
8440
8576
|
@if (loading()) {
|
|
@@ -8703,7 +8839,211 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
|
|
|
8703
8839
|
FormsModule,
|
|
8704
8840
|
],
|
|
8705
8841
|
}]
|
|
8706
|
-
}], propDecorators: { tree: [{ type: i0.ViewChild, args: ['tree', { isSignal: true }] }] } });
|
|
8842
|
+
}], ctorParameters: () => [], propDecorators: { tree: [{ type: i0.ViewChild, args: ['tree', { isSignal: true }] }] } });
|
|
8843
|
+
|
|
8844
|
+
/**
|
|
8845
|
+
* Batch-resolves category data for entity-category-widget-column.
|
|
8846
|
+
* Only fetches when the column is visible. Returns full path for hierarchical display (e.g. "… > Sales").
|
|
8847
|
+
*/
|
|
8848
|
+
class AXPEntityCategoryBatchResolverService {
|
|
8849
|
+
constructor() {
|
|
8850
|
+
this.entityResolver = inject(AXPEntityDefinitionRegistryService);
|
|
8851
|
+
this.entityStorage = inject(AXPEntityStorageService);
|
|
8852
|
+
this.cache = new Map();
|
|
8853
|
+
this.pendingByEntity = new Map();
|
|
8854
|
+
this.flushScheduled = false;
|
|
8855
|
+
}
|
|
8856
|
+
async resolve(entityKey, ids, valueField, textField, parentKey) {
|
|
8857
|
+
const uniqueIds = Array.from(new Set(ids.filter((id) => id != null && id !== '')));
|
|
8858
|
+
if (uniqueIds.length === 0)
|
|
8859
|
+
return new Map();
|
|
8860
|
+
const cached = this.getFromCache(entityKey, uniqueIds, valueField);
|
|
8861
|
+
if (cached.size === uniqueIds.length)
|
|
8862
|
+
return cached;
|
|
8863
|
+
return new Promise((resolve, reject) => {
|
|
8864
|
+
const list = this.pendingByEntity.get(entityKey) ?? [];
|
|
8865
|
+
list.push({
|
|
8866
|
+
ids: uniqueIds,
|
|
8867
|
+
valueField,
|
|
8868
|
+
textField,
|
|
8869
|
+
parentKey,
|
|
8870
|
+
resolve: (m) => {
|
|
8871
|
+
const subset = new Map();
|
|
8872
|
+
for (const id of uniqueIds) {
|
|
8873
|
+
const r = m.get(id);
|
|
8874
|
+
if (r)
|
|
8875
|
+
subset.set(id, r);
|
|
8876
|
+
}
|
|
8877
|
+
resolve(subset);
|
|
8878
|
+
},
|
|
8879
|
+
reject,
|
|
8880
|
+
});
|
|
8881
|
+
this.pendingByEntity.set(entityKey, list);
|
|
8882
|
+
this.scheduleFlush();
|
|
8883
|
+
});
|
|
8884
|
+
}
|
|
8885
|
+
getFromCache(entityKey, ids, valueField) {
|
|
8886
|
+
const entityCache = this.cache.get(entityKey);
|
|
8887
|
+
if (!entityCache)
|
|
8888
|
+
return new Map();
|
|
8889
|
+
const result = new Map();
|
|
8890
|
+
for (const id of ids) {
|
|
8891
|
+
const cached = entityCache.get(id);
|
|
8892
|
+
if (cached)
|
|
8893
|
+
result.set(id, cached);
|
|
8894
|
+
}
|
|
8895
|
+
return result;
|
|
8896
|
+
}
|
|
8897
|
+
setCache(entityKey, items, valueField) {
|
|
8898
|
+
let entityCache = this.cache.get(entityKey);
|
|
8899
|
+
if (!entityCache) {
|
|
8900
|
+
entityCache = new Map();
|
|
8901
|
+
this.cache.set(entityKey, entityCache);
|
|
8902
|
+
}
|
|
8903
|
+
for (const item of items) {
|
|
8904
|
+
entityCache.set(item.id, item);
|
|
8905
|
+
}
|
|
8906
|
+
}
|
|
8907
|
+
scheduleFlush() {
|
|
8908
|
+
if (this.flushScheduled)
|
|
8909
|
+
return;
|
|
8910
|
+
this.flushScheduled = true;
|
|
8911
|
+
queueMicrotask(() => {
|
|
8912
|
+
this.flushScheduled = false;
|
|
8913
|
+
this.flushAll();
|
|
8914
|
+
});
|
|
8915
|
+
}
|
|
8916
|
+
async flushAll() {
|
|
8917
|
+
const entities = Array.from(this.pendingByEntity.keys());
|
|
8918
|
+
for (const entityKey of entities) {
|
|
8919
|
+
await this.flush(entityKey);
|
|
8920
|
+
}
|
|
8921
|
+
}
|
|
8922
|
+
async flush(entityKey) {
|
|
8923
|
+
const list = this.pendingByEntity.get(entityKey);
|
|
8924
|
+
if (!list?.length)
|
|
8925
|
+
return;
|
|
8926
|
+
this.pendingByEntity.delete(entityKey);
|
|
8927
|
+
const first = list[0];
|
|
8928
|
+
const { valueField, textField, parentKey } = first;
|
|
8929
|
+
const allIds = Array.from(new Set(list.flatMap((r) => r.ids)));
|
|
8930
|
+
try {
|
|
8931
|
+
const result = await this.fetchAndResolve(entityKey, allIds, valueField, textField, parentKey);
|
|
8932
|
+
for (const req of list) {
|
|
8933
|
+
const subset = new Map();
|
|
8934
|
+
for (const id of req.ids) {
|
|
8935
|
+
const r = result.get(id);
|
|
8936
|
+
if (r)
|
|
8937
|
+
subset.set(id, r);
|
|
8938
|
+
}
|
|
8939
|
+
req.resolve(subset);
|
|
8940
|
+
}
|
|
8941
|
+
}
|
|
8942
|
+
catch (err) {
|
|
8943
|
+
for (const req of list)
|
|
8944
|
+
req.reject(err);
|
|
8945
|
+
}
|
|
8946
|
+
}
|
|
8947
|
+
async fetchAndResolve(entityKey, ids, valueField, textField, parentKey) {
|
|
8948
|
+
const [moduleName, entityName] = entityKey.split('.');
|
|
8949
|
+
if (!moduleName || !entityName)
|
|
8950
|
+
return new Map();
|
|
8951
|
+
try {
|
|
8952
|
+
let items;
|
|
8953
|
+
let allItems;
|
|
8954
|
+
if (parentKey) {
|
|
8955
|
+
// Use storage directly to fetch ALL categories. The entity's list execute adds
|
|
8956
|
+
// a default filter (parentId isEmpty) that returns only roots - we need the full tree.
|
|
8957
|
+
const allResult = await this.entityStorage.query(entityKey, {
|
|
8958
|
+
skip: 0,
|
|
8959
|
+
take: 10000,
|
|
8960
|
+
});
|
|
8961
|
+
allItems = allResult?.items ?? [];
|
|
8962
|
+
items = allItems.filter((c) => ids.includes(String(get(c, valueField))));
|
|
8963
|
+
}
|
|
8964
|
+
else {
|
|
8965
|
+
const entityDef = await this.entityResolver.resolve(moduleName, entityName);
|
|
8966
|
+
const listExecute = entityDef?.queries?.list?.execute;
|
|
8967
|
+
if (!listExecute || typeof listExecute !== 'function')
|
|
8968
|
+
return new Map();
|
|
8969
|
+
const result = await listExecute({
|
|
8970
|
+
skip: 0,
|
|
8971
|
+
take: ids.length + 100,
|
|
8972
|
+
filter: { field: valueField, operator: { type: 'in' }, value: ids },
|
|
8973
|
+
});
|
|
8974
|
+
items = result?.items ?? [];
|
|
8975
|
+
allItems = items;
|
|
8976
|
+
}
|
|
8977
|
+
const pathMap = parentKey
|
|
8978
|
+
? this.buildPathMap(allItems, valueField, textField, parentKey)
|
|
8979
|
+
: null;
|
|
8980
|
+
const result = new Map();
|
|
8981
|
+
for (const item of items) {
|
|
8982
|
+
const id = String(get(item, valueField));
|
|
8983
|
+
const title = get(item, textField);
|
|
8984
|
+
const path = pathMap
|
|
8985
|
+
? pathMap.get(id) ?? (title != null ? [String(title)] : [])
|
|
8986
|
+
: title != null && title !== ''
|
|
8987
|
+
? [String(title)]
|
|
8988
|
+
: [];
|
|
8989
|
+
result.set(id, {
|
|
8990
|
+
id,
|
|
8991
|
+
title: title != null ? String(title) : '',
|
|
8992
|
+
path,
|
|
8993
|
+
});
|
|
8994
|
+
}
|
|
8995
|
+
this.setCache(entityKey, Array.from(result.values()), valueField);
|
|
8996
|
+
return result;
|
|
8997
|
+
}
|
|
8998
|
+
catch (error) {
|
|
8999
|
+
console.error('[EntityCategoryBatchResolver] Failed:', error);
|
|
9000
|
+
return new Map();
|
|
9001
|
+
}
|
|
9002
|
+
}
|
|
9003
|
+
buildPathMap(items, valueField, textField, parentKey) {
|
|
9004
|
+
const map = new Map();
|
|
9005
|
+
for (const item of items) {
|
|
9006
|
+
map.set(String(get(item, valueField)), item);
|
|
9007
|
+
}
|
|
9008
|
+
const pathMap = new Map();
|
|
9009
|
+
const visited = new Set();
|
|
9010
|
+
const buildPath = (id) => {
|
|
9011
|
+
if (visited.has(id))
|
|
9012
|
+
return pathMap.get(id) ?? [];
|
|
9013
|
+
const item = map.get(id);
|
|
9014
|
+
if (!item)
|
|
9015
|
+
return [];
|
|
9016
|
+
const parentId = get(item, parentKey);
|
|
9017
|
+
const title = String(get(item, textField) ?? '');
|
|
9018
|
+
if (parentId == null ||
|
|
9019
|
+
parentId === '' ||
|
|
9020
|
+
parentId === 'all' ||
|
|
9021
|
+
String(parentId) === id) {
|
|
9022
|
+
const path = title ? [title] : [];
|
|
9023
|
+
pathMap.set(id, path);
|
|
9024
|
+
visited.add(id);
|
|
9025
|
+
return path;
|
|
9026
|
+
}
|
|
9027
|
+
const parentPath = buildPath(String(parentId));
|
|
9028
|
+
const path = title ? [...parentPath, title] : parentPath;
|
|
9029
|
+
pathMap.set(id, path);
|
|
9030
|
+
visited.add(id);
|
|
9031
|
+
return path;
|
|
9032
|
+
};
|
|
9033
|
+
for (const item of items) {
|
|
9034
|
+
const id = String(get(item, valueField));
|
|
9035
|
+
if (!pathMap.has(id))
|
|
9036
|
+
buildPath(id);
|
|
9037
|
+
}
|
|
9038
|
+
return pathMap;
|
|
9039
|
+
}
|
|
9040
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: AXPEntityCategoryBatchResolverService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
9041
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: AXPEntityCategoryBatchResolverService, providedIn: 'root' }); }
|
|
9042
|
+
}
|
|
9043
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: AXPEntityCategoryBatchResolverService, decorators: [{
|
|
9044
|
+
type: Injectable,
|
|
9045
|
+
args: [{ providedIn: 'root' }]
|
|
9046
|
+
}] });
|
|
8707
9047
|
|
|
8708
9048
|
class AXPEntityCategoryWidgetColumnComponent extends AXPColumnWidgetComponent {
|
|
8709
9049
|
constructor() {
|
|
@@ -8712,6 +9052,7 @@ class AXPEntityCategoryWidgetColumnComponent extends AXPColumnWidgetComponent {
|
|
|
8712
9052
|
this.entityDetailPopoverService = inject(AXPEntityDetailPopoverService);
|
|
8713
9053
|
this.formatService = inject(AXFormatService);
|
|
8714
9054
|
this.entityResolver = inject(AXPEntityDefinitionRegistryService);
|
|
9055
|
+
this.categoryBatchResolver = inject(AXPEntityCategoryBatchResolverService);
|
|
8715
9056
|
//#endregion
|
|
8716
9057
|
//#region ---- Inputs ----
|
|
8717
9058
|
this.rawValueSignal = signal(null, ...(ngDevMode ? [{ debugName: "rawValueSignal" }] : []));
|
|
@@ -8763,7 +9104,7 @@ class AXPEntityCategoryWidgetColumnComponent extends AXPColumnWidgetComponent {
|
|
|
8763
9104
|
}
|
|
8764
9105
|
else {
|
|
8765
9106
|
const items = castArray(value);
|
|
8766
|
-
const itemsWithPaths = await
|
|
9107
|
+
const itemsWithPaths = await this.resolveItemsWithPaths(items);
|
|
8767
9108
|
this.displayItems.set(itemsWithPaths.filter((c) => c != null));
|
|
8768
9109
|
}
|
|
8769
9110
|
}, ...(ngDevMode ? [{ debugName: "efDisplay" }] : []));
|
|
@@ -8852,55 +9193,109 @@ class AXPEntityCategoryWidgetColumnComponent extends AXPColumnWidgetComponent {
|
|
|
8852
9193
|
}
|
|
8853
9194
|
//#endregion
|
|
8854
9195
|
//#region ---- Private Methods ----
|
|
8855
|
-
async
|
|
8856
|
-
|
|
8857
|
-
|
|
9196
|
+
async resolveItemsWithPaths(items) {
|
|
9197
|
+
const entityKey = this.entity();
|
|
9198
|
+
const valueField = this.valueField();
|
|
9199
|
+
const textField = this.displayField();
|
|
9200
|
+
const parentKey = this.entityDef()?.parentKey ?? (entityKey?.endsWith('Category') ? 'parentId' : undefined);
|
|
9201
|
+
// When record has categories: [{id, parentId, title}] from middleware, build path from chain
|
|
9202
|
+
const hasFullChain = items.some((item) => item &&
|
|
9203
|
+
typeof item === 'object' &&
|
|
9204
|
+
get(item, 'parentId') != null &&
|
|
9205
|
+
get(item, valueField) != null);
|
|
9206
|
+
if (hasFullChain && items.length > 0) {
|
|
9207
|
+
return items
|
|
9208
|
+
.filter((item) => item != null)
|
|
9209
|
+
.map((item) => this.buildItemWithPathFromChain(item, items, valueField, textField, parentKey ?? 'parentId'));
|
|
9210
|
+
}
|
|
9211
|
+
const needsResolve = items.filter((item) => {
|
|
9212
|
+
if (isNil(item))
|
|
9213
|
+
return false;
|
|
9214
|
+
if (typeof item !== 'object')
|
|
9215
|
+
return true;
|
|
9216
|
+
const idVal = get(item, valueField);
|
|
9217
|
+
const hasPath = item.path && Array.isArray(item.path) && item.path.length > 0;
|
|
9218
|
+
return !hasPath && idVal != null && idVal !== '';
|
|
9219
|
+
});
|
|
9220
|
+
const idsToResolve = needsResolve.map((item) => typeof item === 'object' ? String(get(item, valueField)) : String(item));
|
|
9221
|
+
let resolvedMap = new Map();
|
|
9222
|
+
if (idsToResolve.length > 0 && entityKey) {
|
|
9223
|
+
resolvedMap = await this.categoryBatchResolver.resolve(entityKey, idsToResolve, valueField, textField, parentKey);
|
|
9224
|
+
}
|
|
9225
|
+
return Promise.all(items.map((item) => this.extractItemWithPath(item, resolvedMap)));
|
|
9226
|
+
}
|
|
9227
|
+
/**
|
|
9228
|
+
* Build path from categories array when record has full chain (id, parentId, title).
|
|
9229
|
+
* Used when middleware enriches categories on create/update.
|
|
9230
|
+
*/
|
|
9231
|
+
buildItemWithPathFromChain(item, allItems, valueField, textField, parentKey) {
|
|
9232
|
+
const map = new Map();
|
|
9233
|
+
for (const c of allItems) {
|
|
9234
|
+
const id = get(c, valueField);
|
|
9235
|
+
if (id != null)
|
|
9236
|
+
map.set(String(id), c);
|
|
9237
|
+
}
|
|
9238
|
+
const path = [];
|
|
9239
|
+
let current = item;
|
|
9240
|
+
const visited = new Set();
|
|
9241
|
+
while (current) {
|
|
9242
|
+
const id = String(get(current, valueField) ?? '');
|
|
9243
|
+
if (!id || visited.has(id))
|
|
9244
|
+
break;
|
|
9245
|
+
visited.add(id);
|
|
9246
|
+
const title = String(get(current, textField) ?? '');
|
|
9247
|
+
if (title)
|
|
9248
|
+
path.unshift(title);
|
|
9249
|
+
const parentId = get(current, parentKey);
|
|
9250
|
+
if (parentId == null || parentId === '' || String(parentId) === id)
|
|
9251
|
+
break;
|
|
9252
|
+
current = map.get(String(parentId));
|
|
8858
9253
|
}
|
|
9254
|
+
const pathArray = path.length > 0 ? path : get(item, textField) ? [String(get(item, textField))] : [];
|
|
9255
|
+
return {
|
|
9256
|
+
[valueField]: get(item, valueField),
|
|
9257
|
+
[textField]: get(item, textField),
|
|
9258
|
+
path: pathArray,
|
|
9259
|
+
};
|
|
9260
|
+
}
|
|
9261
|
+
async extractItemWithPath(item, resolvedMap) {
|
|
9262
|
+
if (isNil(item))
|
|
9263
|
+
return null;
|
|
9264
|
+
const valueField = this.valueField();
|
|
9265
|
+
const textField = this.displayField();
|
|
8859
9266
|
let itemObj;
|
|
8860
9267
|
if (typeof item === 'object') {
|
|
8861
9268
|
itemObj = item;
|
|
8862
|
-
|
|
8863
|
-
|
|
8864
|
-
if (!
|
|
8865
|
-
const
|
|
8866
|
-
|
|
8867
|
-
|
|
8868
|
-
|
|
8869
|
-
|
|
8870
|
-
|
|
8871
|
-
|
|
8872
|
-
}
|
|
8873
|
-
}
|
|
8874
|
-
catch (error) {
|
|
8875
|
-
console.error('Error fetching full item for path calculation:', error);
|
|
8876
|
-
}
|
|
9269
|
+
const idVal = get(itemObj, valueField);
|
|
9270
|
+
const hasPath = itemObj.path && Array.isArray(itemObj.path) && itemObj.path.length > 0;
|
|
9271
|
+
if (!hasPath && resolvedMap && idVal != null && idVal !== '') {
|
|
9272
|
+
const r = resolvedMap.get(String(idVal));
|
|
9273
|
+
if (r) {
|
|
9274
|
+
itemObj = {
|
|
9275
|
+
[valueField]: r.id,
|
|
9276
|
+
[textField]: r.title,
|
|
9277
|
+
path: r.path,
|
|
9278
|
+
};
|
|
8877
9279
|
}
|
|
8878
9280
|
}
|
|
8879
9281
|
}
|
|
8880
9282
|
else {
|
|
8881
|
-
|
|
8882
|
-
|
|
8883
|
-
|
|
8884
|
-
|
|
8885
|
-
itemObj = await byKey(item);
|
|
9283
|
+
if (resolvedMap) {
|
|
9284
|
+
const r = resolvedMap.get(String(item));
|
|
9285
|
+
if (r) {
|
|
9286
|
+
itemObj = { [valueField]: r.id, [textField]: r.title, path: r.path };
|
|
8886
9287
|
}
|
|
8887
|
-
|
|
8888
|
-
|
|
8889
|
-
return null;
|
|
9288
|
+
else {
|
|
9289
|
+
itemObj = { [valueField]: item, [textField]: item, path: [String(item)] };
|
|
8890
9290
|
}
|
|
8891
9291
|
}
|
|
8892
9292
|
else {
|
|
8893
|
-
itemObj = {
|
|
8894
|
-
[this.valueField()]: item,
|
|
8895
|
-
[this.textField()]: item,
|
|
8896
|
-
};
|
|
9293
|
+
itemObj = { [valueField]: item, [textField]: item, path: [String(item)] };
|
|
8897
9294
|
}
|
|
8898
9295
|
}
|
|
8899
|
-
// If item already has a path array, return it as is
|
|
8900
9296
|
if (itemObj.path && Array.isArray(itemObj.path) && itemObj.path.length > 0) {
|
|
8901
9297
|
return itemObj;
|
|
8902
9298
|
}
|
|
8903
|
-
// Calculate path for the item
|
|
8904
9299
|
return await this.calculateItemPath(itemObj);
|
|
8905
9300
|
}
|
|
8906
9301
|
/**
|
|
@@ -9460,6 +9855,7 @@ class AXPEntityCategoryWidgetEditComponent extends AXPValueWidgetComponent {
|
|
|
9460
9855
|
selectedValues: signal(selectedIds),
|
|
9461
9856
|
searchPlaceholder: signal(this.searchPlaceholderText()),
|
|
9462
9857
|
excludedNodeId: signal(excludedNodeId),
|
|
9858
|
+
selectedValuesInput: signal(selectedIds),
|
|
9463
9859
|
},
|
|
9464
9860
|
});
|
|
9465
9861
|
if (result?.data?.selected && Array.isArray(result.data.selected)) {
|
|
@@ -10208,6 +10604,11 @@ class AXPEntityListTableService {
|
|
|
10208
10604
|
parentField: entity.parentKey,
|
|
10209
10605
|
fetchDataMode: 'manual',
|
|
10210
10606
|
minHeight: 250,
|
|
10607
|
+
// ⏳ Loading Configuration
|
|
10608
|
+
loading: {
|
|
10609
|
+
enabled: true,
|
|
10610
|
+
animation: true,
|
|
10611
|
+
},
|
|
10211
10612
|
// 🎪 Events
|
|
10212
10613
|
...this.createDefaultEvents(entity, allActions),
|
|
10213
10614
|
};
|
|
@@ -10744,25 +11145,27 @@ class AXPEntityListWidgetViewComponent extends AXPValueWidgetComponent {
|
|
|
10744
11145
|
};
|
|
10745
11146
|
const listOptions = await this.entityListTableService.convertEntityToListOptions(resolvedEntity, options, this.allActions());
|
|
10746
11147
|
const toolbarOptions = await this.entityListToolbarService.convertEntityToolbarOptions(resolvedEntity, options);
|
|
10747
|
-
|
|
10748
|
-
|
|
10749
|
-
|
|
10750
|
-
|
|
10751
|
-
|
|
10752
|
-
|
|
10753
|
-
|
|
10754
|
-
|
|
10755
|
-
|
|
10756
|
-
|
|
10757
|
-
|
|
10758
|
-
|
|
10759
|
-
|
|
10760
|
-
|
|
10761
|
-
|
|
10762
|
-
|
|
10763
|
-
|
|
10764
|
-
|
|
10765
|
-
|
|
11148
|
+
setTimeout(() => {
|
|
11149
|
+
this.listNode.set({
|
|
11150
|
+
type: AXPWidgetsCatalog.dataList,
|
|
11151
|
+
options: listOptions,
|
|
11152
|
+
path: `table`,
|
|
11153
|
+
name: 'table',
|
|
11154
|
+
mode: 'view',
|
|
11155
|
+
defaultValue: this.getValue()?.table,
|
|
11156
|
+
});
|
|
11157
|
+
this.toolbarNode.set({
|
|
11158
|
+
type: AXPWidgetsCatalog.listToolbar,
|
|
11159
|
+
path: `toolbar`,
|
|
11160
|
+
options: toolbarOptions,
|
|
11161
|
+
mode: 'view',
|
|
11162
|
+
defaultValue: {
|
|
11163
|
+
filters: this.getValue()?.toolbar?.filters,
|
|
11164
|
+
sorts: this.getValue()?.toolbar?.sorts,
|
|
11165
|
+
columns: this.getValue()?.toolbar?.columns,
|
|
11166
|
+
},
|
|
11167
|
+
});
|
|
11168
|
+
}, 100);
|
|
10766
11169
|
}
|
|
10767
11170
|
async ngAfterViewInit() {
|
|
10768
11171
|
this.workflow.events$
|
|
@@ -12565,7 +12968,7 @@ class AXPLookupWidgetColumnComponent extends AXPColumnWidgetComponent {
|
|
|
12565
12968
|
return get(item, this.displayField()) ?? '';
|
|
12566
12969
|
}
|
|
12567
12970
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: AXPLookupWidgetColumnComponent, deps: null, target: i0.ɵɵFactoryTarget.Component }); }
|
|
12568
|
-
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", "
|
|
12971
|
+
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 }); }
|
|
12569
12972
|
}
|
|
12570
12973
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: AXPLookupWidgetColumnComponent, decorators: [{
|
|
12571
12974
|
type: Component,
|
|
@@ -14193,6 +14596,8 @@ class AXPShowFileUploaderPopupAction extends AXPWorkflowAction {
|
|
|
14193
14596
|
const maxFileSize = context.getVariable('options.maxFileSize');
|
|
14194
14597
|
const files = context.getVariable('options.files');
|
|
14195
14598
|
const fileEditable = context.getVariable('options.fileEditable');
|
|
14599
|
+
const enableTitleDescription = context.getVariable('options.enableTitleDescription');
|
|
14600
|
+
const showEditDialogAfterSelect = context.getVariable('options.showEditDialogAfterSelect');
|
|
14196
14601
|
//check is correct file
|
|
14197
14602
|
const correctFiles = files.filter((item) => item != undefined);
|
|
14198
14603
|
// *********** show file list ***********
|
|
@@ -14202,9 +14607,10 @@ class AXPShowFileUploaderPopupAction extends AXPWorkflowAction {
|
|
|
14202
14607
|
multiple: multiple,
|
|
14203
14608
|
accept: accept,
|
|
14204
14609
|
fileEditable: fileEditable,
|
|
14610
|
+
enableTitleDescription: enableTitleDescription ?? false,
|
|
14611
|
+
showEditDialogAfterSelect: showEditDialogAfterSelect ?? false,
|
|
14205
14612
|
// maxFileSize: maxFileSize,
|
|
14206
14613
|
});
|
|
14207
|
-
debugger;
|
|
14208
14614
|
// Handle case when result is undefined or empty array
|
|
14209
14615
|
if (!res) {
|
|
14210
14616
|
context.setOutput('result', false);
|
|
@@ -14213,7 +14619,6 @@ class AXPShowFileUploaderPopupAction extends AXPWorkflowAction {
|
|
|
14213
14619
|
// *********** save files to entity ***********
|
|
14214
14620
|
const [module, entity] = context.getVariable('entity').split('.');
|
|
14215
14621
|
const entityDefinition = await this.entityRegistryService.resolve(module, entity);
|
|
14216
|
-
debugger;
|
|
14217
14622
|
if (entityDefinition) {
|
|
14218
14623
|
const updateExec = entityDefinition.commands?.update?.execute;
|
|
14219
14624
|
const entityData = await updateExec({
|
|
@@ -14221,7 +14626,10 @@ class AXPShowFileUploaderPopupAction extends AXPWorkflowAction {
|
|
|
14221
14626
|
[key]: res,
|
|
14222
14627
|
});
|
|
14223
14628
|
context.setOutput('result', true);
|
|
14224
|
-
|
|
14629
|
+
// Merge full file list (name, title, description) from popup into returned data so
|
|
14630
|
+
// workflow output includes title/description when enableTitleDescription is used
|
|
14631
|
+
const dataWithFiles = { ...entityData, [key]: res };
|
|
14632
|
+
context.setVariable('data', cloneDeep(dataWithFiles));
|
|
14225
14633
|
}
|
|
14226
14634
|
}
|
|
14227
14635
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: AXPShowFileUploaderPopupAction, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); }
|