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