@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.
Files changed (55) hide show
  1. package/fesm2022/acorex-platform-auth.mjs +8 -2
  2. package/fesm2022/acorex-platform-auth.mjs.map +1 -1
  3. package/fesm2022/{acorex-platform-common-common-settings.provider-VfmZct9H.mjs → acorex-platform-common-common-settings.provider-DilV4dql.mjs} +15 -42
  4. package/fesm2022/acorex-platform-common-common-settings.provider-DilV4dql.mjs.map +1 -0
  5. package/fesm2022/acorex-platform-common.mjs +3299 -3200
  6. package/fesm2022/acorex-platform-common.mjs.map +1 -1
  7. package/fesm2022/acorex-platform-contracts.mjs +1 -0
  8. package/fesm2022/acorex-platform-contracts.mjs.map +1 -1
  9. package/fesm2022/acorex-platform-core.mjs +643 -30
  10. package/fesm2022/acorex-platform-core.mjs.map +1 -1
  11. package/fesm2022/acorex-platform-layout-builder.mjs +98 -189
  12. package/fesm2022/acorex-platform-layout-builder.mjs.map +1 -1
  13. 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
  14. package/fesm2022/acorex-platform-layout-components-binding-expression-editor-popup.component-RttFwaw9.mjs.map +1 -0
  15. package/fesm2022/acorex-platform-layout-components.mjs +125 -26
  16. package/fesm2022/acorex-platform-layout-components.mjs.map +1 -1
  17. package/fesm2022/acorex-platform-layout-designer.mjs +4 -4
  18. package/fesm2022/acorex-platform-layout-designer.mjs.map +1 -1
  19. package/fesm2022/acorex-platform-layout-entity-contracts.mjs +106 -12
  20. package/fesm2022/acorex-platform-layout-entity-contracts.mjs.map +1 -1
  21. package/fesm2022/acorex-platform-layout-entity.mjs +512 -155
  22. package/fesm2022/acorex-platform-layout-entity.mjs.map +1 -1
  23. package/fesm2022/acorex-platform-layout-views.mjs +921 -173
  24. package/fesm2022/acorex-platform-layout-views.mjs.map +1 -1
  25. package/fesm2022/acorex-platform-layout-widget-core.mjs +380 -103
  26. package/fesm2022/acorex-platform-layout-widget-core.mjs.map +1 -1
  27. 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
  28. 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
  29. 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
  30. 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
  31. package/fesm2022/acorex-platform-layout-widgets.mjs +575 -239
  32. package/fesm2022/acorex-platform-layout-widgets.mjs.map +1 -1
  33. package/fesm2022/acorex-platform-themes-default.mjs +232 -25
  34. package/fesm2022/acorex-platform-themes-default.mjs.map +1 -1
  35. package/fesm2022/{acorex-platform-themes-shared-settings.provider-BUwPqwUS.mjs → acorex-platform-themes-shared-settings.provider-UyKdkeyk.mjs} +48 -1
  36. package/fesm2022/acorex-platform-themes-shared-settings.provider-UyKdkeyk.mjs.map +1 -0
  37. package/fesm2022/acorex-platform-themes-shared.mjs +103 -24
  38. package/fesm2022/acorex-platform-themes-shared.mjs.map +1 -1
  39. package/package.json +1 -1
  40. package/types/acorex-platform-auth.d.ts +1 -0
  41. package/types/acorex-platform-common.d.ts +202 -151
  42. package/types/acorex-platform-contracts.d.ts +1 -0
  43. package/types/acorex-platform-core.d.ts +162 -7
  44. package/types/acorex-platform-layout-builder.d.ts +10 -20
  45. package/types/acorex-platform-layout-components.d.ts +14 -1
  46. package/types/acorex-platform-layout-entity-contracts.d.ts +31 -2
  47. package/types/acorex-platform-layout-entity.d.ts +91 -16
  48. package/types/acorex-platform-layout-views.d.ts +107 -20
  49. package/types/acorex-platform-layout-widget-core.d.ts +71 -9
  50. package/types/acorex-platform-layout-widgets.d.ts +48 -26
  51. package/types/acorex-platform-themes-default.d.ts +60 -6
  52. package/types/acorex-platform-themes-shared.d.ts +4 -0
  53. package/fesm2022/acorex-platform-common-common-settings.provider-VfmZct9H.mjs.map +0 -1
  54. package/fesm2022/acorex-platform-layout-components-binding-expression-editor-popup.component-CXEdvDTf.mjs.map +0 -1
  55. 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, effect, untracked, EventEmitter, Injector, ChangeDetectorRef, ViewChild, Input, Output, ChangeDetectionStrategy, Component, input, output, ViewContainerRef, Directive, NgModule } from '@angular/core';
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
- this.status$ = signal(AXPPageStatus.Rendering, ...(ngDevMode ? [{ debugName: "status$" }] : /* istanbul ignore next */ []));
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
- updateStatus() {
78
- this.status$.update(() => this.detectStatus());
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
- detectStatus() {
82
- const statuses = Array.from(this.widgets.values()).map((c) => c.status());
83
- if (statuses.some((status) => status === AXPWidgetStatus.Rendering)) {
84
- return AXPPageStatus.Rendering;
85
- }
86
- if (statuses.every((status) => status === AXPWidgetStatus.Rendered)) {
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
- if (statuses.some((status) => status === AXPWidgetStatus.Processing)) {
90
- return AXPPageStatus.Processing;
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(() => [AXPWidgetStatus.Rendering, AXPWidgetStatus.Processing].includes(this.status()), ...(ngDevMode ? [{ debugName: "isBusy" }] : /* istanbul ignore next */ []));
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.updateStatus();
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.setValue(this.defaultValue);
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
- !isEqual(oldValue, savedAtPath)) {
409
- this.contextService.update(path, cloneDeep(savedAtPath));
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
- this.contextService.update(path, value);
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
- // Process dataSource creation in untracked context
501
- await untracked(async () => {
502
- if (rawValue instanceof AXDataSource) {
503
- // For AXDataSource instances, check if config changed
504
- if (!isEqual(rawValue.config, this.dataSource().config)) {
505
- this.dataSource.set(rawValue);
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
- // static array datasource - always recreate when options change
509
- else if (Array.isArray(rawValue)) {
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: 500,
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
- else {
541
- const ds = c?.source();
542
- if (ds && ds instanceof Promise) {
543
- const d = await ds;
544
- this.dataSource.set(d);
545
- }
546
- else if (ds) {
547
- this.dataSource.set(ds);
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
- this.dataSource.set(convertArrayToDataSource([]));
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.dataSource.set(convertArrayToDataSource([]));
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
- else {
568
- items.push(await this.extractItem(value));
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-gap-1 ax-select-none"
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
- [style.justify-content]="getHeaderJustifyContent()"
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 class="axp-widget-column-header__caption ax-truncate">{{ caption | translate | async }}</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-gap-1 ax-select-none"
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
- [style.justify-content]="getHeaderJustifyContent()"
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 class="axp-widget-column-header__caption ax-truncate">{{ caption | translate | async }}</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
- /** Idle period after widgets register / hydrate before committing the saved baseline. */
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
- const commit = () => {
1648
- if (this.builderService.registeredWidgetsCount() === 0) {
1649
- this.savedBaselineTimer = setTimeout(commit, FORM_SAVED_BASELINE_IDLE_MS);
1650
- return;
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.savedBaselineTimer = setTimeout(commit, FORM_SAVED_BASELINE_IDLE_MS);
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();