@acorex/platform 21.0.0-next.88 → 21.0.0-next.90
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/fesm2022/acorex-platform-auth.mjs +8 -2
- package/fesm2022/acorex-platform-auth.mjs.map +1 -1
- package/fesm2022/{acorex-platform-common-common-settings.provider-VfmZct9H.mjs → acorex-platform-common-common-settings.provider-DilV4dql.mjs} +15 -42
- package/fesm2022/acorex-platform-common-common-settings.provider-DilV4dql.mjs.map +1 -0
- package/fesm2022/acorex-platform-common.mjs +3299 -3200
- package/fesm2022/acorex-platform-common.mjs.map +1 -1
- package/fesm2022/acorex-platform-contracts.mjs +1 -0
- package/fesm2022/acorex-platform-contracts.mjs.map +1 -1
- package/fesm2022/acorex-platform-core.mjs +643 -30
- package/fesm2022/acorex-platform-core.mjs.map +1 -1
- package/fesm2022/acorex-platform-layout-builder.mjs +98 -189
- package/fesm2022/acorex-platform-layout-builder.mjs.map +1 -1
- package/fesm2022/{acorex-platform-layout-components-binding-expression-editor-popup.component-CXEdvDTf.mjs → acorex-platform-layout-components-binding-expression-editor-popup.component-RttFwaw9.mjs} +17 -4
- package/fesm2022/acorex-platform-layout-components-binding-expression-editor-popup.component-RttFwaw9.mjs.map +1 -0
- package/fesm2022/acorex-platform-layout-components.mjs +125 -26
- package/fesm2022/acorex-platform-layout-components.mjs.map +1 -1
- package/fesm2022/acorex-platform-layout-designer.mjs +4 -4
- package/fesm2022/acorex-platform-layout-designer.mjs.map +1 -1
- package/fesm2022/acorex-platform-layout-entity-contracts.mjs +106 -12
- package/fesm2022/acorex-platform-layout-entity-contracts.mjs.map +1 -1
- package/fesm2022/acorex-platform-layout-entity.mjs +512 -155
- package/fesm2022/acorex-platform-layout-entity.mjs.map +1 -1
- package/fesm2022/acorex-platform-layout-views.mjs +921 -173
- package/fesm2022/acorex-platform-layout-views.mjs.map +1 -1
- package/fesm2022/acorex-platform-layout-widget-core.mjs +380 -103
- package/fesm2022/acorex-platform-layout-widget-core.mjs.map +1 -1
- package/fesm2022/{acorex-platform-layout-widgets-tabular-data-edit-popup.component-D4MOHEpI.mjs → acorex-platform-layout-widgets-tabular-data-edit-popup.component-BvFykMTH.mjs} +2 -2
- package/fesm2022/{acorex-platform-layout-widgets-tabular-data-edit-popup.component-D4MOHEpI.mjs.map → acorex-platform-layout-widgets-tabular-data-edit-popup.component-BvFykMTH.mjs.map} +1 -1
- package/fesm2022/{acorex-platform-layout-widgets-tabular-data-view-popup.component-gX-3Kx9I.mjs → acorex-platform-layout-widgets-tabular-data-view-popup.component-CntopmDS.mjs} +2 -2
- package/fesm2022/{acorex-platform-layout-widgets-tabular-data-view-popup.component-gX-3Kx9I.mjs.map → acorex-platform-layout-widgets-tabular-data-view-popup.component-CntopmDS.mjs.map} +1 -1
- package/fesm2022/acorex-platform-layout-widgets.mjs +575 -239
- package/fesm2022/acorex-platform-layout-widgets.mjs.map +1 -1
- package/fesm2022/acorex-platform-themes-default.mjs +232 -25
- package/fesm2022/acorex-platform-themes-default.mjs.map +1 -1
- package/fesm2022/{acorex-platform-themes-shared-settings.provider-BUwPqwUS.mjs → acorex-platform-themes-shared-settings.provider-UyKdkeyk.mjs} +48 -1
- package/fesm2022/acorex-platform-themes-shared-settings.provider-UyKdkeyk.mjs.map +1 -0
- package/fesm2022/acorex-platform-themes-shared.mjs +103 -24
- package/fesm2022/acorex-platform-themes-shared.mjs.map +1 -1
- package/package.json +1 -1
- package/types/acorex-platform-auth.d.ts +1 -0
- package/types/acorex-platform-common.d.ts +202 -151
- package/types/acorex-platform-contracts.d.ts +1 -0
- package/types/acorex-platform-core.d.ts +162 -7
- package/types/acorex-platform-layout-builder.d.ts +10 -20
- package/types/acorex-platform-layout-components.d.ts +14 -1
- package/types/acorex-platform-layout-entity-contracts.d.ts +31 -2
- package/types/acorex-platform-layout-entity.d.ts +91 -16
- package/types/acorex-platform-layout-views.d.ts +107 -20
- package/types/acorex-platform-layout-widget-core.d.ts +71 -9
- package/types/acorex-platform-layout-widgets.d.ts +48 -26
- package/types/acorex-platform-themes-default.d.ts +60 -6
- package/types/acorex-platform-themes-shared.d.ts +4 -0
- package/fesm2022/acorex-platform-common-common-settings.provider-VfmZct9H.mjs.map +0 -1
- package/fesm2022/acorex-platform-layout-components-binding-expression-editor-popup.component-CXEdvDTf.mjs.map +0 -1
- package/fesm2022/acorex-platform-themes-shared-settings.provider-BUwPqwUS.mjs.map +0 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { signal, computed, Injectable, InjectionToken, inject, ElementRef,
|
|
2
|
+
import { signal, computed, effect, untracked, Injectable, InjectionToken, inject, ElementRef, DestroyRef, EventEmitter, Injector, ChangeDetectorRef, ViewChild, Input, Output, ChangeDetectionStrategy, Component, input, output, ViewContainerRef, Directive, NgModule } from '@angular/core';
|
|
3
3
|
import { convertArrayToDataSource, AXDataSource, AX_STYLE_COLOR_TYPES, AX_STYLE_LOOK_TYPES } from '@acorex/cdk/common';
|
|
4
|
-
import { AXPContextStore, isSelectionValueEqual, isFormValueEqual, AXPDataSourceDefinitionProviderService, extractValue, AXPExpressionEvaluatorService, getSmart } from '@acorex/platform/core';
|
|
4
|
+
import { AXPContextStore, isSelectionValueEqual, isFormValueEqual, AXPDebugService, FORM_DIRTY_TRACE_NS, AXPDataSourceDefinitionProviderService, extractValue, AXPExpressionEvaluatorService, getSmart, summarizeFormContextDirtyDiff, OVERLAY_WIDGET_SETTLE_TRACE_NS } from '@acorex/platform/core';
|
|
5
5
|
import { set, merge, cloneDeep, isNil, get, isEqual, isUndefined, isObjectLike, sum, isEmpty, isString } from 'lodash-es';
|
|
6
6
|
import { Subject, BehaviorSubject, filter } from 'rxjs';
|
|
7
7
|
import { AXPWidgetsCatalog } from '@acorex/platform/contracts';
|
|
@@ -38,8 +38,6 @@ var AXPWidgetStatus;
|
|
|
38
38
|
// Rendering statuses
|
|
39
39
|
AXPWidgetStatus["Rendering"] = "rendering";
|
|
40
40
|
AXPWidgetStatus["Rendered"] = "rendered";
|
|
41
|
-
// Processing statuses
|
|
42
|
-
AXPWidgetStatus["Processing"] = "processing";
|
|
43
41
|
// Error handling
|
|
44
42
|
AXPWidgetStatus["Error"] = "error";
|
|
45
43
|
})(AXPWidgetStatus || (AXPWidgetStatus = {}));
|
|
@@ -59,14 +57,39 @@ class AXPWidgetCoreService {
|
|
|
59
57
|
this.onRefresh = new Subject();
|
|
60
58
|
this.widgets = new Map();
|
|
61
59
|
this.onWidgetRegistered = new Subject();
|
|
62
|
-
|
|
60
|
+
/** Fires whenever a widget registers, including re-registration after remount (same id, new instance). */
|
|
61
|
+
this.widgetRegistered$ = this.onWidgetRegistered.asObservable();
|
|
62
|
+
/** Bumps when widget render/hydration state changes so aggregate computeds re-evaluate. */
|
|
63
|
+
this.widgetsRevision = signal(0, ...(ngDevMode ? [{ debugName: "widgetsRevision" }] : /* istanbul ignore next */ []));
|
|
64
|
+
this.status$ = signal(AXPPageStatus.Rendered, ...(ngDevMode ? [{ debugName: "status$" }] : /* istanbul ignore next */ []));
|
|
63
65
|
this.status = this.status$.asReadonly();
|
|
64
|
-
this.isBusy = computed(() => {
|
|
65
|
-
return [AXPPageStatus.Processing, AXPPageStatus.Submitting, AXPPageStatus.Rendering].includes(this.status());
|
|
66
|
-
}, ...(ngDevMode ? [{ debugName: "isBusy" }] : /* istanbul ignore next */ []));
|
|
67
66
|
this.registeredWidgetsCount = signal(0, ...(ngDevMode ? [{ debugName: "registeredWidgetsCount" }] : /* istanbul ignore next */ []));
|
|
67
|
+
this.isRendering = computed(() => {
|
|
68
|
+
this.widgetsRevision();
|
|
69
|
+
return Array.from(this.widgets.values()).some((widget) => widget.status() === AXPWidgetStatus.Rendering);
|
|
70
|
+
}, ...(ngDevMode ? [{ debugName: "isRendering" }] : /* istanbul ignore next */ []));
|
|
71
|
+
this.isHydrating = computed(() => {
|
|
72
|
+
this.widgetsRevision();
|
|
73
|
+
return Array.from(this.widgets.values()).some((widget) => widget.isHydrating());
|
|
74
|
+
}, ...(ngDevMode ? [{ debugName: "isHydrating" }] : /* istanbul ignore next */ []));
|
|
75
|
+
this.isBusy = computed(() => this.isRendering() || this.isHydrating(), ...(ngDevMode ? [{ debugName: "isBusy" }] : /* istanbul ignore next */ []));
|
|
76
|
+
this.isStructureReady = computed(() => {
|
|
77
|
+
this.widgetsRevision();
|
|
78
|
+
const widgets = Array.from(this.widgets.values());
|
|
79
|
+
if (!widgets.length) {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
return widgets.every((widget) => widget.status() === AXPWidgetStatus.Rendered);
|
|
83
|
+
}, ...(ngDevMode ? [{ debugName: "isStructureReady" }] : /* istanbul ignore next */ []));
|
|
68
84
|
this.dirtyWidgetsRevision = signal(0, ...(ngDevMode ? [{ debugName: "dirtyWidgetsRevision" }] : /* istanbul ignore next */ []));
|
|
69
85
|
this.dirtyWidgetsRevisionSignal = this.dirtyWidgetsRevision.asReadonly();
|
|
86
|
+
effect(() => {
|
|
87
|
+
this.widgetsRevision();
|
|
88
|
+
this.registeredWidgetsCount();
|
|
89
|
+
untracked(() => {
|
|
90
|
+
this.status$.set(this.detectAggregateStatus());
|
|
91
|
+
});
|
|
92
|
+
});
|
|
70
93
|
}
|
|
71
94
|
get variables() {
|
|
72
95
|
return this.variables$();
|
|
@@ -74,24 +97,49 @@ class AXPWidgetCoreService {
|
|
|
74
97
|
get functions() {
|
|
75
98
|
return this.functions$();
|
|
76
99
|
}
|
|
77
|
-
|
|
78
|
-
|
|
100
|
+
/** Recomputes aggregate container state after a widget registers or changes status/hydration. */
|
|
101
|
+
notifyWidgetStateChanged() {
|
|
79
102
|
this.registeredWidgetsCount.set(this.widgets.size);
|
|
103
|
+
this.widgetsRevision.update((value) => value + 1);
|
|
80
104
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
105
|
+
/** @deprecated Prefer {@link notifyWidgetStateChanged}. */
|
|
106
|
+
updateStatus() {
|
|
107
|
+
this.notifyWidgetStateChanged();
|
|
108
|
+
}
|
|
109
|
+
/** Debug snapshot of registered widget render/hydration state. */
|
|
110
|
+
getWidgetStateSnapshot() {
|
|
111
|
+
const widgets = Array.from(this.widgets.entries()).map(([id, widget]) => ({
|
|
112
|
+
id,
|
|
113
|
+
status: widget.status(),
|
|
114
|
+
isHydrating: widget.isHydrating(),
|
|
115
|
+
}));
|
|
116
|
+
return {
|
|
117
|
+
count: widgets.length,
|
|
118
|
+
isRendering: this.isRendering(),
|
|
119
|
+
isHydrating: this.isHydrating(),
|
|
120
|
+
isStructureReady: this.isStructureReady(),
|
|
121
|
+
isBusy: this.isBusy(),
|
|
122
|
+
widgets,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
detectAggregateStatus() {
|
|
126
|
+
const widgets = Array.from(this.widgets.values());
|
|
127
|
+
if (!widgets.length) {
|
|
87
128
|
return AXPPageStatus.Rendered;
|
|
88
129
|
}
|
|
89
|
-
|
|
90
|
-
|
|
130
|
+
const statuses = widgets.map((widget) => widget.status());
|
|
131
|
+
if (statuses.some((status) => status === AXPWidgetStatus.Rendering)) {
|
|
132
|
+
return AXPPageStatus.Rendering;
|
|
91
133
|
}
|
|
92
134
|
if (statuses.some((status) => status === AXPWidgetStatus.Error)) {
|
|
93
135
|
return AXPPageStatus.Error;
|
|
94
136
|
}
|
|
137
|
+
if (widgets.some((widget) => widget.isHydrating())) {
|
|
138
|
+
return AXPPageStatus.Rendering;
|
|
139
|
+
}
|
|
140
|
+
if (statuses.every((status) => status === AXPWidgetStatus.Rendered)) {
|
|
141
|
+
return AXPPageStatus.Rendered;
|
|
142
|
+
}
|
|
95
143
|
return AXPPageStatus.Rendered;
|
|
96
144
|
}
|
|
97
145
|
refresh() {
|
|
@@ -123,6 +171,19 @@ class AXPWidgetCoreService {
|
|
|
123
171
|
registerWidget(id, widget) {
|
|
124
172
|
this.widgets.set(id, widget);
|
|
125
173
|
this.onWidgetRegistered.next({ id, widget });
|
|
174
|
+
this.notifyWidgetStateChanged();
|
|
175
|
+
}
|
|
176
|
+
unregisterWidget(id) {
|
|
177
|
+
if (this.widgets.delete(id)) {
|
|
178
|
+
this.notifyWidgetStateChanged();
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
clearWidgets() {
|
|
182
|
+
if (this.widgets.size === 0) {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
this.widgets.clear();
|
|
186
|
+
this.notifyWidgetStateChanged();
|
|
126
187
|
}
|
|
127
188
|
getWidget(id) {
|
|
128
189
|
return this.widgets.get(id);
|
|
@@ -168,7 +229,7 @@ class AXPWidgetCoreService {
|
|
|
168
229
|
}
|
|
169
230
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPWidgetCoreService, decorators: [{
|
|
170
231
|
type: Injectable
|
|
171
|
-
}] });
|
|
232
|
+
}], ctorParameters: () => [] });
|
|
172
233
|
|
|
173
234
|
function cloneProperty(property, values) {
|
|
174
235
|
return merge(cloneDeep(property), values);
|
|
@@ -262,6 +323,18 @@ class AXPBaseWidgetComponent extends AXPWidgetCoreElement {
|
|
|
262
323
|
this.host = inject(ElementRef).nativeElement;
|
|
263
324
|
this.layoutService = inject(AXPWidgetCoreService);
|
|
264
325
|
this.contextService = inject(AXPContextStore);
|
|
326
|
+
/** Unregisters this widget from the container registry when the instance is destroyed. */
|
|
327
|
+
this.#unregisterFromLayoutOnDestroy = inject(DestroyRef).onDestroy(() => {
|
|
328
|
+
const registrationId = this._id ?? this.name;
|
|
329
|
+
if (registrationId) {
|
|
330
|
+
this.layoutService.unregisterWidget(registrationId);
|
|
331
|
+
}
|
|
332
|
+
if (this.hydrationDepth > 0) {
|
|
333
|
+
this.hydrationDepth = 0;
|
|
334
|
+
this._isHydrating.set(false);
|
|
335
|
+
this.layoutService.notifyWidgetStateChanged();
|
|
336
|
+
}
|
|
337
|
+
});
|
|
265
338
|
this.config = this.token.config;
|
|
266
339
|
this.node = this.token.node;
|
|
267
340
|
this.name = this.token.node.name;
|
|
@@ -272,13 +345,19 @@ class AXPBaseWidgetComponent extends AXPWidgetCoreElement {
|
|
|
272
345
|
this._status = signal(AXPWidgetStatus.Rendering, ...(ngDevMode ? [{ debugName: "_status" }] : /* istanbul ignore next */ []));
|
|
273
346
|
this.status = this._status.asReadonly();
|
|
274
347
|
this.onStatusChanged = new BehaviorSubject(this._status());
|
|
348
|
+
/** Depth counter so nested async hydration calls share one busy state. */
|
|
349
|
+
this.hydrationDepth = 0;
|
|
350
|
+
this._isHydrating = signal(false, ...(ngDevMode ? [{ debugName: "_isHydrating" }] : /* istanbul ignore next */ []));
|
|
351
|
+
this.isHydrating = this._isHydrating.asReadonly();
|
|
275
352
|
this.#statusEffect = effect(() => {
|
|
276
353
|
this.onStatusChanged.next(this.status());
|
|
277
354
|
}, ...(ngDevMode ? [{ debugName: "#statusEffect" }] : /* istanbul ignore next */ []));
|
|
278
|
-
this.isBusy = computed(() =>
|
|
355
|
+
this.isBusy = computed(() => this.status() === AXPWidgetStatus.Rendering || this.isHydrating(), ...(ngDevMode ? [{ debugName: "isBusy" }] : /* istanbul ignore next */ []));
|
|
279
356
|
this._children = signal(this.token.node.children ?? [], ...(ngDevMode ? [{ debugName: "_children" }] : /* istanbul ignore next */ []));
|
|
280
357
|
this.children = this._children.asReadonly();
|
|
281
358
|
}
|
|
359
|
+
/** Unregisters this widget from the container registry when the instance is destroyed. */
|
|
360
|
+
#unregisterFromLayoutOnDestroy;
|
|
282
361
|
get id() {
|
|
283
362
|
return this._id;
|
|
284
363
|
}
|
|
@@ -294,7 +373,26 @@ class AXPBaseWidgetComponent extends AXPWidgetCoreElement {
|
|
|
294
373
|
}
|
|
295
374
|
setStatus(status) {
|
|
296
375
|
this._status.set(status);
|
|
297
|
-
this.layoutService.
|
|
376
|
+
this.layoutService.notifyWidgetStateChanged();
|
|
377
|
+
}
|
|
378
|
+
/** Marks async data loading (lookup, select, file fetch, etc.). */
|
|
379
|
+
beginHydration() {
|
|
380
|
+
this.hydrationDepth += 1;
|
|
381
|
+
if (this.hydrationDepth === 1) {
|
|
382
|
+
this._isHydrating.set(true);
|
|
383
|
+
this.layoutService.notifyWidgetStateChanged();
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
/** Clears hydration started by {@link beginHydration}. */
|
|
387
|
+
endHydration() {
|
|
388
|
+
if (this.hydrationDepth <= 0) {
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
this.hydrationDepth -= 1;
|
|
392
|
+
if (this.hydrationDepth === 0) {
|
|
393
|
+
this._isHydrating.set(false);
|
|
394
|
+
this.layoutService.notifyWidgetStateChanged();
|
|
395
|
+
}
|
|
298
396
|
}
|
|
299
397
|
setOptions(values) {
|
|
300
398
|
const oldValue = this.options();
|
|
@@ -376,7 +474,7 @@ class AXPValueWidgetComponent extends AXPLayoutBaseWidgetComponent {
|
|
|
376
474
|
this.detectFullPath();
|
|
377
475
|
// Set defaultValue if it exists and the path doesn't exist in context
|
|
378
476
|
if (!isNil(this.defaultValue) && this.fullPath() && !this.contextService.hasValue(this.fullPath())) {
|
|
379
|
-
this.
|
|
477
|
+
this.applyDefaultValue(this.defaultValue);
|
|
380
478
|
}
|
|
381
479
|
}
|
|
382
480
|
//
|
|
@@ -389,6 +487,22 @@ class AXPValueWidgetComponent extends AXPLayoutBaseWidgetComponent {
|
|
|
389
487
|
}
|
|
390
488
|
return rawValue;
|
|
391
489
|
}
|
|
490
|
+
/** Applies schema default without marking the form as user-edited. */
|
|
491
|
+
applyDefaultValue(value) {
|
|
492
|
+
if (this.node.valueTransforms?.setter) {
|
|
493
|
+
value = this.node.valueTransforms?.setter(value);
|
|
494
|
+
}
|
|
495
|
+
const path = this.fullPath();
|
|
496
|
+
if (!path) {
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
value = isUndefined(value) ? null : value;
|
|
500
|
+
if (isNil(value)) {
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
this.contextService.update(path, value, { origin: 'system' });
|
|
504
|
+
this.onValueChanged.next({ sender: this });
|
|
505
|
+
}
|
|
392
506
|
setValue(value) {
|
|
393
507
|
if (this.node.valueTransforms?.setter) {
|
|
394
508
|
value = this.node.valueTransforms?.setter(value);
|
|
@@ -403,10 +517,18 @@ class AXPValueWidgetComponent extends AXPLayoutBaseWidgetComponent {
|
|
|
403
517
|
return;
|
|
404
518
|
}
|
|
405
519
|
const savedAtPath = this.contextService.getSavedValue(path);
|
|
520
|
+
const widgetName = this.name ?? this._id ?? 'value-widget';
|
|
406
521
|
if (this.contextService.isSavedCommitted() &&
|
|
407
522
|
isSelectionValueEqual(value, savedAtPath) &&
|
|
408
|
-
!
|
|
409
|
-
|
|
523
|
+
!isFormValueEqual(value, savedAtPath)) {
|
|
524
|
+
AXPDebugService.instance?.trace(FORM_DIRTY_TRACE_NS, widgetName, 'setValue restore saved shape', () => ({
|
|
525
|
+
path,
|
|
526
|
+
widget: widgetName,
|
|
527
|
+
incomingValue: value,
|
|
528
|
+
savedAtPath,
|
|
529
|
+
oldValue,
|
|
530
|
+
}));
|
|
531
|
+
this.contextService.update(path, cloneDeep(savedAtPath), { origin: 'system' });
|
|
410
532
|
this.onValueChanged.next({ sender: this });
|
|
411
533
|
return;
|
|
412
534
|
}
|
|
@@ -417,9 +539,32 @@ class AXPValueWidgetComponent extends AXPLayoutBaseWidgetComponent {
|
|
|
417
539
|
oldValue.some((v, i) => v !== value[i]);
|
|
418
540
|
const isArrayLengthChange = Array.isArray(value) && (!Array.isArray(oldValue) || value.length !== oldValue.length);
|
|
419
541
|
if (!isArrayReorder && !isArrayLengthChange && isFormValueEqual(oldValue, value)) {
|
|
542
|
+
AXPDebugService.instance?.trace(FORM_DIRTY_TRACE_NS, widgetName, 'setValue skipped (isFormValueEqual)', () => ({
|
|
543
|
+
path,
|
|
544
|
+
widget: widgetName,
|
|
545
|
+
value,
|
|
546
|
+
oldValue,
|
|
547
|
+
savedAtPath,
|
|
548
|
+
selectionEqualToSaved: this.contextService.isSavedCommitted()
|
|
549
|
+
? isSelectionValueEqual(value, savedAtPath)
|
|
550
|
+
: null,
|
|
551
|
+
}));
|
|
420
552
|
return;
|
|
421
553
|
}
|
|
422
|
-
|
|
554
|
+
AXPDebugService.instance?.trace(FORM_DIRTY_TRACE_NS, widgetName, 'setValue user update', () => ({
|
|
555
|
+
path,
|
|
556
|
+
widget: widgetName,
|
|
557
|
+
value,
|
|
558
|
+
oldValue,
|
|
559
|
+
savedAtPath,
|
|
560
|
+
selectionEqualToSaved: this.contextService.isSavedCommitted()
|
|
561
|
+
? isSelectionValueEqual(value, savedAtPath)
|
|
562
|
+
: null,
|
|
563
|
+
formValueEqualToSaved: this.contextService.isSavedCommitted()
|
|
564
|
+
? isFormValueEqual(value, savedAtPath)
|
|
565
|
+
: null,
|
|
566
|
+
}));
|
|
567
|
+
this.contextService.update(path, value, { origin: 'user' });
|
|
423
568
|
this.onValueChanged.next({ sender: this });
|
|
424
569
|
}
|
|
425
570
|
detectFullPath() {
|
|
@@ -497,77 +642,89 @@ class AXPDataListWidgetComponent extends AXPValueWidgetComponent {
|
|
|
497
642
|
// Reset string reference when dataSource changes to non-string
|
|
498
643
|
this.lastStringDataSourceRef = null;
|
|
499
644
|
}
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
if (
|
|
505
|
-
|
|
645
|
+
this.beginHydration();
|
|
646
|
+
try {
|
|
647
|
+
// Process dataSource creation in untracked context
|
|
648
|
+
await untracked(async () => {
|
|
649
|
+
if (rawValue instanceof AXDataSource) {
|
|
650
|
+
// For AXDataSource instances, check if config changed
|
|
651
|
+
if (!isEqual(rawValue.config, this.dataSource().config)) {
|
|
652
|
+
this.dataSource.set(rawValue);
|
|
653
|
+
}
|
|
506
654
|
}
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
const ds = new AXDataSource({
|
|
511
|
-
key: this.valueField(),
|
|
512
|
-
pageSize: 10,
|
|
513
|
-
load: async (e) => {
|
|
514
|
-
const raw = this.options()['dataSource'];
|
|
515
|
-
const skip = e.skip ?? 0;
|
|
516
|
-
const take = e.take ?? raw.length;
|
|
517
|
-
return {
|
|
518
|
-
items: raw.slice(skip, skip + take),
|
|
519
|
-
total: raw.length,
|
|
520
|
-
};
|
|
521
|
-
},
|
|
522
|
-
byKey: (key) => {
|
|
523
|
-
const raw = this.options()['dataSource'];
|
|
524
|
-
const item = raw.filter((c) => c[this.valueField()] == key);
|
|
525
|
-
return Promise.resolve(item[0]);
|
|
526
|
-
},
|
|
527
|
-
});
|
|
528
|
-
this.dataSource.set(ds);
|
|
529
|
-
}
|
|
530
|
-
// resolve data source by name (string or object with id)
|
|
531
|
-
else if (rawValue && (typeof rawValue == 'string' || typeof rawValue == 'object')) {
|
|
532
|
-
const id = typeof rawValue == 'object' ? rawValue['id'] : rawValue;
|
|
533
|
-
const c = await this.dataService.get(id);
|
|
534
|
-
if (this.mode == 'designer' && c?.samples?.length) {
|
|
535
|
-
this.dataSource.set(convertArrayToDataSource(c.samples, {
|
|
655
|
+
// static array datasource - always recreate when options change
|
|
656
|
+
else if (Array.isArray(rawValue)) {
|
|
657
|
+
const ds = new AXDataSource({
|
|
536
658
|
key: this.valueField(),
|
|
537
|
-
pageSize:
|
|
538
|
-
|
|
659
|
+
pageSize: 10,
|
|
660
|
+
load: async (e) => {
|
|
661
|
+
const raw = this.options()['dataSource'];
|
|
662
|
+
const skip = e.skip ?? 0;
|
|
663
|
+
const take = e.take ?? raw.length;
|
|
664
|
+
return {
|
|
665
|
+
items: raw.slice(skip, skip + take),
|
|
666
|
+
total: raw.length,
|
|
667
|
+
};
|
|
668
|
+
},
|
|
669
|
+
byKey: (key) => {
|
|
670
|
+
const raw = this.options()['dataSource'];
|
|
671
|
+
const item = raw.filter((c) => c[this.valueField()] == key);
|
|
672
|
+
return Promise.resolve(item[0]);
|
|
673
|
+
},
|
|
674
|
+
});
|
|
675
|
+
this.dataSource.set(ds);
|
|
539
676
|
}
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
677
|
+
// resolve data source by name (string or object with id)
|
|
678
|
+
else if (rawValue && (typeof rawValue == 'string' || typeof rawValue == 'object')) {
|
|
679
|
+
const id = typeof rawValue == 'object' ? rawValue['id'] : rawValue;
|
|
680
|
+
const c = await this.dataService.get(id);
|
|
681
|
+
if (this.mode == 'designer' && c?.samples?.length) {
|
|
682
|
+
this.dataSource.set(convertArrayToDataSource(c.samples, {
|
|
683
|
+
key: this.valueField(),
|
|
684
|
+
pageSize: 500,
|
|
685
|
+
}));
|
|
548
686
|
}
|
|
549
|
-
// empty datasource
|
|
550
687
|
else {
|
|
551
|
-
|
|
688
|
+
const ds = c?.source();
|
|
689
|
+
if (ds && ds instanceof Promise) {
|
|
690
|
+
const d = await ds;
|
|
691
|
+
this.dataSource.set(d);
|
|
692
|
+
}
|
|
693
|
+
else if (ds) {
|
|
694
|
+
this.dataSource.set(ds);
|
|
695
|
+
}
|
|
696
|
+
// empty datasource
|
|
697
|
+
else {
|
|
698
|
+
this.dataSource.set(convertArrayToDataSource([]));
|
|
699
|
+
}
|
|
552
700
|
}
|
|
553
701
|
}
|
|
702
|
+
// empty datasource
|
|
703
|
+
else {
|
|
704
|
+
this.dataSource.set(convertArrayToDataSource([]));
|
|
705
|
+
}
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
finally {
|
|
709
|
+
this.endHydration();
|
|
710
|
+
}
|
|
711
|
+
}, ...(ngDevMode ? [{ debugName: "rf" }] : /* istanbul ignore next */ []));
|
|
712
|
+
this.effect2 = effect(async () => {
|
|
713
|
+
this.beginHydration();
|
|
714
|
+
try {
|
|
715
|
+
const value = this.getValue();
|
|
716
|
+
const items = [];
|
|
717
|
+
if (Array.isArray(value)) {
|
|
718
|
+
items.push(...(await Promise.all(value.map((item) => this.extractItem(item)))));
|
|
554
719
|
}
|
|
555
|
-
// empty datasource
|
|
556
720
|
else {
|
|
557
|
-
this.
|
|
721
|
+
items.push(await this.extractItem(value));
|
|
558
722
|
}
|
|
559
|
-
|
|
560
|
-
}, ...(ngDevMode ? [{ debugName: "rf" }] : /* istanbul ignore next */ []));
|
|
561
|
-
this.effect2 = effect(async () => {
|
|
562
|
-
const value = this.resolveSelectedItemsValue();
|
|
563
|
-
const items = [];
|
|
564
|
-
if (Array.isArray(value)) {
|
|
565
|
-
items.push(...(await Promise.all(value.map((item) => this.extractItem(item)))));
|
|
723
|
+
this.selectedItems.set(items.filter((c) => c != null));
|
|
566
724
|
}
|
|
567
|
-
|
|
568
|
-
|
|
725
|
+
finally {
|
|
726
|
+
this.endHydration();
|
|
569
727
|
}
|
|
570
|
-
this.selectedItems.set(items.filter((c) => c != null));
|
|
571
728
|
}, ...(ngDevMode ? [{ debugName: "effect2" }] : /* istanbul ignore next */ []));
|
|
572
729
|
}
|
|
573
730
|
//#endregion
|
|
@@ -1402,12 +1559,18 @@ class AXPWidgetColumnRendererComponent extends AXDataTableColumnComponent {
|
|
|
1402
1559
|
], viewQueries: [{ propertyName: "_contentFooterTemplate", first: true, predicate: ["footer"], descendants: true }, { propertyName: "_contentCellTemplate", first: true, predicate: ["cell"], descendants: true }, { propertyName: "_contentHeaderTemplate", first: true, predicate: ["header"], descendants: true }], usesInheritance: true, usesOnChanges: true, ngImport: i0, template: `
|
|
1403
1560
|
<ng-template #header>
|
|
1404
1561
|
<div
|
|
1405
|
-
class="axp-widget-column-header ax-w-full ax-flex ax-items-center ax-
|
|
1562
|
+
class="axp-widget-column-header ax-w-full ax-flex ax-items-center ax-select-none"
|
|
1406
1563
|
[class.axp-widget-column-header--sortable]="sortEnabled"
|
|
1407
|
-
[
|
|
1564
|
+
[class.axp-widget-column-header--sort-active]="headerSortDirection === 'asc' || headerSortDirection === 'desc'"
|
|
1565
|
+
[class.ax-gap-1]="!sortEnabled"
|
|
1566
|
+
[style.justify-content]="sortEnabled ? 'flex-start' : getHeaderJustifyContent()"
|
|
1408
1567
|
(click)="onHeaderClick($event)"
|
|
1409
1568
|
>
|
|
1410
|
-
<span
|
|
1569
|
+
<span
|
|
1570
|
+
class="axp-widget-column-header__caption ax-truncate"
|
|
1571
|
+
[class.axp-widget-column-header__caption--with-sort]="sortEnabled"
|
|
1572
|
+
[style.text-align]="sortEnabled ? getHeaderTextAlign() : null"
|
|
1573
|
+
>{{ caption | translate | async }}</span>
|
|
1411
1574
|
@if (sortEnabled) {
|
|
1412
1575
|
<span class="axp-widget-column-header__sort ax-flex ax-items-center ax-gap-0.5 ax-shrink-0">
|
|
1413
1576
|
@if (headerSortPriority !== undefined && headerSortPriority > 1) {
|
|
@@ -1460,19 +1623,25 @@ class AXPWidgetColumnRendererComponent extends AXDataTableColumnComponent {
|
|
|
1460
1623
|
</div>
|
|
1461
1624
|
</ng-template>
|
|
1462
1625
|
<ng-template #footer></ng-template>
|
|
1463
|
-
`, isInline: true, styles: [".axp-widget-column-header--sortable{cursor:pointer}.axp-widget-column-header__caption{min-width:0}.axp-widget-column-header__sort{opacity:0;transition:opacity .15s ease}.axp-widget-column-header--sortable:hover .axp-widget-column-header__sort,.axp-widget-column-header--sortable:focus-within .axp-widget-column-header__sort{opacity:1}.axp-widget-column-header__sort-priority{line-height:1}:host-context(.ax-data-table-head-cell.ax-interactive:hover) .axp-widget-column-header__sort,:host-context(.ax-data-table-head-cell.ax-interactive:focus-within) .axp-widget-column-header__sort{opacity:1}\n"], dependencies: [{ kind: "directive", type: i1.NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }, { kind: "pipe", type: i1.AsyncPipe, name: "async" }, { kind: "pipe", type: i2.AXTranslatorPipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
1626
|
+
`, isInline: true, styles: [".axp-widget-column-header--sortable{cursor:pointer}.axp-widget-column-header__caption{min-width:0}.axp-widget-column-header__caption--with-sort{flex:1 1 0}.axp-widget-column-header__sort{opacity:0;transition:opacity .15s ease}.axp-widget-column-header--sortable:hover .axp-widget-column-header__sort,.axp-widget-column-header--sortable:focus-within .axp-widget-column-header__sort,.axp-widget-column-header--sort-active .axp-widget-column-header__sort{opacity:1}.axp-widget-column-header__sort-priority{line-height:1}:host-context(.ax-data-table-head-cell.ax-interactive:hover) .axp-widget-column-header__sort,:host-context(.ax-data-table-head-cell.ax-interactive:focus-within) .axp-widget-column-header__sort,:host-context(.ax-data-table-head-cell) .axp-widget-column-header--sort-active .axp-widget-column-header__sort{opacity:1}\n"], dependencies: [{ kind: "directive", type: i1.NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }, { kind: "pipe", type: i1.AsyncPipe, name: "async" }, { kind: "pipe", type: i2.AXTranslatorPipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
1464
1627
|
}
|
|
1465
1628
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPWidgetColumnRendererComponent, decorators: [{
|
|
1466
1629
|
type: Component,
|
|
1467
1630
|
args: [{ selector: 'axp-widget-column-renderer', template: `
|
|
1468
1631
|
<ng-template #header>
|
|
1469
1632
|
<div
|
|
1470
|
-
class="axp-widget-column-header ax-w-full ax-flex ax-items-center ax-
|
|
1633
|
+
class="axp-widget-column-header ax-w-full ax-flex ax-items-center ax-select-none"
|
|
1471
1634
|
[class.axp-widget-column-header--sortable]="sortEnabled"
|
|
1472
|
-
[
|
|
1635
|
+
[class.axp-widget-column-header--sort-active]="headerSortDirection === 'asc' || headerSortDirection === 'desc'"
|
|
1636
|
+
[class.ax-gap-1]="!sortEnabled"
|
|
1637
|
+
[style.justify-content]="sortEnabled ? 'flex-start' : getHeaderJustifyContent()"
|
|
1473
1638
|
(click)="onHeaderClick($event)"
|
|
1474
1639
|
>
|
|
1475
|
-
<span
|
|
1640
|
+
<span
|
|
1641
|
+
class="axp-widget-column-header__caption ax-truncate"
|
|
1642
|
+
[class.axp-widget-column-header__caption--with-sort]="sortEnabled"
|
|
1643
|
+
[style.text-align]="sortEnabled ? getHeaderTextAlign() : null"
|
|
1644
|
+
>{{ caption | translate | async }}</span>
|
|
1476
1645
|
@if (sortEnabled) {
|
|
1477
1646
|
<span class="axp-widget-column-header__sort ax-flex ax-items-center ax-gap-0.5 ax-shrink-0">
|
|
1478
1647
|
@if (headerSortPriority !== undefined && headerSortPriority > 1) {
|
|
@@ -1528,7 +1697,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
|
|
|
1528
1697
|
`, providers: [
|
|
1529
1698
|
AXPWidgetCoreService,
|
|
1530
1699
|
{ provide: AXDataTableColumnComponent, useExisting: AXPWidgetColumnRendererComponent },
|
|
1531
|
-
], changeDetection: ChangeDetectionStrategy.OnPush, inputs: ['caption'], standalone: false, styles: [".axp-widget-column-header--sortable{cursor:pointer}.axp-widget-column-header__caption{min-width:0}.axp-widget-column-header__sort{opacity:0;transition:opacity .15s ease}.axp-widget-column-header--sortable:hover .axp-widget-column-header__sort,.axp-widget-column-header--sortable:focus-within .axp-widget-column-header__sort{opacity:1}.axp-widget-column-header__sort-priority{line-height:1}:host-context(.ax-data-table-head-cell.ax-interactive:hover) .axp-widget-column-header__sort,:host-context(.ax-data-table-head-cell.ax-interactive:focus-within) .axp-widget-column-header__sort{opacity:1}\n"] }]
|
|
1700
|
+
], changeDetection: ChangeDetectionStrategy.OnPush, inputs: ['caption'], standalone: false, styles: [".axp-widget-column-header--sortable{cursor:pointer}.axp-widget-column-header__caption{min-width:0}.axp-widget-column-header__caption--with-sort{flex:1 1 0}.axp-widget-column-header__sort{opacity:0;transition:opacity .15s ease}.axp-widget-column-header--sortable:hover .axp-widget-column-header__sort,.axp-widget-column-header--sortable:focus-within .axp-widget-column-header__sort,.axp-widget-column-header--sort-active .axp-widget-column-header__sort{opacity:1}.axp-widget-column-header__sort-priority{line-height:1}:host-context(.ax-data-table-head-cell.ax-interactive:hover) .axp-widget-column-header__sort,:host-context(.ax-data-table-head-cell.ax-interactive:focus-within) .axp-widget-column-header__sort,:host-context(.ax-data-table-head-cell) .axp-widget-column-header--sort-active .axp-widget-column-header__sort{opacity:1}\n"] }]
|
|
1532
1701
|
}], propDecorators: { customExpandIcon: [{
|
|
1533
1702
|
type: Input
|
|
1534
1703
|
}], customCollapseIcon: [{
|
|
@@ -1567,8 +1736,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
|
|
|
1567
1736
|
args: ['header']
|
|
1568
1737
|
}] } });
|
|
1569
1738
|
|
|
1570
|
-
/**
|
|
1739
|
+
/** Continuous idle period required after widgets settle before committing baseline. */
|
|
1571
1740
|
const FORM_SAVED_BASELINE_IDLE_MS = 500;
|
|
1741
|
+
/** Poll interval while waiting for widgets to settle (~20s max). */
|
|
1742
|
+
const FORM_SAVED_BASELINE_POLL_MS = 250;
|
|
1743
|
+
/** Max poll attempts for settle / ready (~20s); applies even when widgets are registered. */
|
|
1744
|
+
const FORM_SAVED_BASELINE_MAX_SETTLE_POLLS = 80;
|
|
1572
1745
|
class AXPWidgetContainerComponent {
|
|
1573
1746
|
set context(value) {
|
|
1574
1747
|
this.syncExternalContext(value);
|
|
@@ -1579,7 +1752,9 @@ class AXPWidgetContainerComponent {
|
|
|
1579
1752
|
constructor() {
|
|
1580
1753
|
this.contextService = inject(AXPContextStore);
|
|
1581
1754
|
this.builderService = inject(AXPWidgetCoreService);
|
|
1755
|
+
this.debug = inject(AXPDebugService);
|
|
1582
1756
|
this.onContextChanged = new EventEmitter();
|
|
1757
|
+
this.wasHydrating = false;
|
|
1583
1758
|
this.isSavedCommitted = computed(() => this.contextService.isSavedCommitted(), ...(ngDevMode ? [{ debugName: "isSavedCommitted" }] : /* istanbul ignore next */ []));
|
|
1584
1759
|
this.isFormDirty = computed(() => {
|
|
1585
1760
|
if (!this.contextService.isSavedCommitted()) {
|
|
@@ -1590,6 +1765,15 @@ class AXPWidgetContainerComponent {
|
|
|
1590
1765
|
}, ...(ngDevMode ? [{ debugName: "isFormDirty" }] : /* istanbul ignore next */ []));
|
|
1591
1766
|
this.status = computed(() => this.builderService.status(), ...(ngDevMode ? [{ debugName: "status" }] : /* istanbul ignore next */ []));
|
|
1592
1767
|
this.isBusy = computed(() => this.builderService.isBusy(), ...(ngDevMode ? [{ debugName: "isBusy" }] : /* istanbul ignore next */ []));
|
|
1768
|
+
this.isHydrating = computed(() => this.builderService.isHydrating(), ...(ngDevMode ? [{ debugName: "isHydrating" }] : /* istanbul ignore next */ []));
|
|
1769
|
+
/** True when registered widgets exist and none are rendering or hydrating. */
|
|
1770
|
+
this.isWidgetsReady = computed(() => {
|
|
1771
|
+
const count = this.builderService.registeredWidgetsCount();
|
|
1772
|
+
if (count === 0) {
|
|
1773
|
+
return false;
|
|
1774
|
+
}
|
|
1775
|
+
return this.builderService.isStructureReady() && !this.builderService.isBusy();
|
|
1776
|
+
}, ...(ngDevMode ? [{ debugName: "isWidgetsReady" }] : /* istanbul ignore next */ []));
|
|
1593
1777
|
effect(() => {
|
|
1594
1778
|
const isDirty = this.isFormDirty();
|
|
1595
1779
|
const data = this.contextService.data();
|
|
@@ -1598,6 +1782,16 @@ class AXPWidgetContainerComponent {
|
|
|
1598
1782
|
return;
|
|
1599
1783
|
}
|
|
1600
1784
|
this.lastEmittedFormDirty = isDirty;
|
|
1785
|
+
this.debug.trace(FORM_DIRTY_TRACE_NS, 'widget-container', 'isFormDirty changed', () => ({
|
|
1786
|
+
isFormDirty: isDirty,
|
|
1787
|
+
userEditedSinceCommit: this.contextService.hasUserEditedSinceCommit(),
|
|
1788
|
+
isSavedCommitted: this.contextService.isSavedCommitted(),
|
|
1789
|
+
changePath: changeEvent.path,
|
|
1790
|
+
changeOrigin: changeEvent.origin,
|
|
1791
|
+
dirtyPaths: this.contextService.isSavedCommitted()
|
|
1792
|
+
? summarizeFormContextDirtyDiff(this.contextService.data(), this.contextService.saved())
|
|
1793
|
+
: [],
|
|
1794
|
+
}));
|
|
1601
1795
|
this.onContextChanged.emit({
|
|
1602
1796
|
...changeEvent,
|
|
1603
1797
|
data: cloneDeep(data),
|
|
@@ -1608,11 +1802,26 @@ class AXPWidgetContainerComponent {
|
|
|
1608
1802
|
if (this.contextService.isSavedCommitted()) {
|
|
1609
1803
|
return;
|
|
1610
1804
|
}
|
|
1805
|
+
this.builderService.registeredWidgetsCount();
|
|
1806
|
+
this.builderService.isHydrating();
|
|
1807
|
+
this.builderService.isStructureReady();
|
|
1611
1808
|
if (this.builderService.registeredWidgetsCount() === 0) {
|
|
1612
1809
|
return;
|
|
1613
1810
|
}
|
|
1614
1811
|
untracked(() => void this.settleSavedBaseline());
|
|
1615
1812
|
});
|
|
1813
|
+
effect(() => {
|
|
1814
|
+
const hydrating = this.builderService.isHydrating();
|
|
1815
|
+
const savedCommitted = this.contextService.isSavedCommitted();
|
|
1816
|
+
if (!savedCommitted) {
|
|
1817
|
+
this.wasHydrating = hydrating;
|
|
1818
|
+
return;
|
|
1819
|
+
}
|
|
1820
|
+
if (this.wasHydrating && !hydrating) {
|
|
1821
|
+
untracked(() => this.scheduleRebaselineAfterHydration());
|
|
1822
|
+
}
|
|
1823
|
+
this.wasHydrating = hydrating;
|
|
1824
|
+
});
|
|
1616
1825
|
}
|
|
1617
1826
|
refresh() {
|
|
1618
1827
|
this.builderService.refresh();
|
|
@@ -1630,6 +1839,7 @@ class AXPWidgetContainerComponent {
|
|
|
1630
1839
|
return;
|
|
1631
1840
|
}
|
|
1632
1841
|
if (this.contextService.isSavedCommitted()) {
|
|
1842
|
+
this.builderService.clearWidgets();
|
|
1633
1843
|
this.contextService.set(next);
|
|
1634
1844
|
void this.settleSavedBaseline();
|
|
1635
1845
|
return;
|
|
@@ -1637,28 +1847,77 @@ class AXPWidgetContainerComponent {
|
|
|
1637
1847
|
if (this.contextService.isChanged()) {
|
|
1638
1848
|
return;
|
|
1639
1849
|
}
|
|
1850
|
+
this.builderService.clearWidgets();
|
|
1640
1851
|
this.contextService.set(next);
|
|
1641
1852
|
void this.settleSavedBaseline();
|
|
1642
1853
|
}
|
|
1643
1854
|
/** Commits the current data as saved after widget hydration settles. */
|
|
1644
1855
|
settleSavedBaseline() {
|
|
1856
|
+
if (this.settlePromise) {
|
|
1857
|
+
return this.settlePromise;
|
|
1858
|
+
}
|
|
1645
1859
|
this.clearSavedBaselineTimer();
|
|
1860
|
+
this.settlePromise = this.waitUntilSettled()
|
|
1861
|
+
.then(() => {
|
|
1862
|
+
this.contextService.commitSaved();
|
|
1863
|
+
this.lastEmittedFormDirty = undefined;
|
|
1864
|
+
})
|
|
1865
|
+
.finally(() => {
|
|
1866
|
+
this.settlePromise = undefined;
|
|
1867
|
+
});
|
|
1868
|
+
return this.settlePromise;
|
|
1869
|
+
}
|
|
1870
|
+
/** Resolves when widgets stay registered, structure-ready, and idle for {@link FORM_SAVED_BASELINE_IDLE_MS}. */
|
|
1871
|
+
waitUntilSettled() {
|
|
1646
1872
|
return new Promise((resolve) => {
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1873
|
+
let idleRetries = 0;
|
|
1874
|
+
let stableTimer;
|
|
1875
|
+
let pollTimer;
|
|
1876
|
+
const finish = (reason) => {
|
|
1877
|
+
const snapshot = this.builderService.getWidgetStateSnapshot();
|
|
1878
|
+
this.debug.trace(OVERLAY_WIDGET_SETTLE_TRACE_NS, 'settle', 'Completed', () => ({
|
|
1879
|
+
reason,
|
|
1880
|
+
idleRetries,
|
|
1881
|
+
...snapshot,
|
|
1882
|
+
}));
|
|
1883
|
+
if (pollTimer) {
|
|
1884
|
+
clearTimeout(pollTimer);
|
|
1885
|
+
}
|
|
1886
|
+
if (stableTimer) {
|
|
1887
|
+
clearTimeout(stableTimer);
|
|
1651
1888
|
}
|
|
1652
|
-
this.contextService.commitSaved();
|
|
1653
|
-
this.lastEmittedFormDirty = undefined;
|
|
1654
|
-
this.savedBaselineTimer = undefined;
|
|
1655
1889
|
resolve();
|
|
1656
1890
|
};
|
|
1657
|
-
this.
|
|
1891
|
+
this.debug.trace(OVERLAY_WIDGET_SETTLE_TRACE_NS, 'settle', 'Started');
|
|
1892
|
+
const poll = () => {
|
|
1893
|
+
const snapshot = this.builderService.getWidgetStateSnapshot();
|
|
1894
|
+
const isSettled = snapshot.count > 0 &&
|
|
1895
|
+
snapshot.isStructureReady &&
|
|
1896
|
+
!snapshot.isHydrating &&
|
|
1897
|
+
!snapshot.isRendering;
|
|
1898
|
+
if (isSettled) {
|
|
1899
|
+
if (!stableTimer) {
|
|
1900
|
+
stableTimer = setTimeout(() => finish('stable-idle'), FORM_SAVED_BASELINE_IDLE_MS);
|
|
1901
|
+
}
|
|
1902
|
+
return;
|
|
1903
|
+
}
|
|
1904
|
+
if (stableTimer) {
|
|
1905
|
+
clearTimeout(stableTimer);
|
|
1906
|
+
stableTimer = undefined;
|
|
1907
|
+
}
|
|
1908
|
+
idleRetries += 1;
|
|
1909
|
+
if (idleRetries >= FORM_SAVED_BASELINE_MAX_SETTLE_POLLS) {
|
|
1910
|
+
finish('max-polls');
|
|
1911
|
+
return;
|
|
1912
|
+
}
|
|
1913
|
+
pollTimer = setTimeout(poll, FORM_SAVED_BASELINE_POLL_MS);
|
|
1914
|
+
};
|
|
1915
|
+
pollTimer = setTimeout(poll, FORM_SAVED_BASELINE_POLL_MS);
|
|
1658
1916
|
});
|
|
1659
1917
|
}
|
|
1660
1918
|
async replaceContext(value) {
|
|
1661
1919
|
this.clearSavedBaselineTimer();
|
|
1920
|
+
this.settlePromise = undefined;
|
|
1662
1921
|
this.contextService.set(cloneDeep(value));
|
|
1663
1922
|
// Commit immediately so save/replace is clean even when `set()` no-ops (data already matches).
|
|
1664
1923
|
this.contextService.commitSaved();
|
|
@@ -1667,9 +1926,28 @@ class AXPWidgetContainerComponent {
|
|
|
1667
1926
|
}
|
|
1668
1927
|
revertAndSettle() {
|
|
1669
1928
|
this.clearSavedBaselineTimer();
|
|
1929
|
+
this.settlePromise = undefined;
|
|
1930
|
+
this.debug.trace(FORM_DIRTY_TRACE_NS, 'widget-container', 'revertAndSettle');
|
|
1670
1931
|
this.contextService.revertToSaved();
|
|
1671
1932
|
this.lastEmittedFormDirty = undefined;
|
|
1672
1933
|
}
|
|
1934
|
+
/** Re-commits baseline after post-load hydration when the user has not edited the form. */
|
|
1935
|
+
scheduleRebaselineAfterHydration() {
|
|
1936
|
+
this.clearSavedBaselineTimer();
|
|
1937
|
+
this.savedBaselineTimer = setTimeout(() => {
|
|
1938
|
+
this.savedBaselineTimer = undefined;
|
|
1939
|
+
if (this.contextService.hasUserEditedSinceCommit()) {
|
|
1940
|
+
return;
|
|
1941
|
+
}
|
|
1942
|
+
void this.waitUntilSettled().then(() => {
|
|
1943
|
+
if (this.contextService.hasUserEditedSinceCommit()) {
|
|
1944
|
+
return;
|
|
1945
|
+
}
|
|
1946
|
+
this.contextService.commitSaved();
|
|
1947
|
+
this.lastEmittedFormDirty = undefined;
|
|
1948
|
+
});
|
|
1949
|
+
}, FORM_SAVED_BASELINE_IDLE_MS);
|
|
1950
|
+
}
|
|
1673
1951
|
clearSavedBaselineTimer() {
|
|
1674
1952
|
if (this.savedBaselineTimer) {
|
|
1675
1953
|
clearTimeout(this.savedBaselineTimer);
|
|
@@ -2229,7 +2507,6 @@ class AXPWidgetRendererDirective {
|
|
|
2229
2507
|
this.instance.parent = this.parentNode();
|
|
2230
2508
|
this.instance.index = this.index();
|
|
2231
2509
|
this.instance.mode = this.mode();
|
|
2232
|
-
this.instance.setStatus(AXPWidgetStatus.Rendered);
|
|
2233
2510
|
// Update the component ref signal for reactive access
|
|
2234
2511
|
this._componentRefSignal.set(this.componentRef);
|
|
2235
2512
|
this.instance?.onOptionsChanged?.pipe(this.unsubscriber.takeUntilDestroy).subscribe((c) => {
|
|
@@ -2504,7 +2781,7 @@ class AXPWidgetRendererDirective {
|
|
|
2504
2781
|
return value;
|
|
2505
2782
|
},
|
|
2506
2783
|
set: (path, value) => {
|
|
2507
|
-
this.contextService.update(path, value);
|
|
2784
|
+
this.contextService.update(path, value, { origin: 'user' });
|
|
2508
2785
|
},
|
|
2509
2786
|
data: () => {
|
|
2510
2787
|
return this.contextService.data();
|