@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.
Files changed (52) 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-core.mjs +621 -26
  8. package/fesm2022/acorex-platform-core.mjs.map +1 -1
  9. package/fesm2022/acorex-platform-layout-builder.mjs +98 -189
  10. package/fesm2022/acorex-platform-layout-builder.mjs.map +1 -1
  11. 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
  12. package/fesm2022/acorex-platform-layout-components-binding-expression-editor-popup.component-RttFwaw9.mjs.map +1 -0
  13. package/fesm2022/acorex-platform-layout-components.mjs +125 -26
  14. package/fesm2022/acorex-platform-layout-components.mjs.map +1 -1
  15. package/fesm2022/acorex-platform-layout-designer.mjs +4 -4
  16. package/fesm2022/acorex-platform-layout-designer.mjs.map +1 -1
  17. package/fesm2022/acorex-platform-layout-entity-contracts.mjs +106 -12
  18. package/fesm2022/acorex-platform-layout-entity-contracts.mjs.map +1 -1
  19. package/fesm2022/acorex-platform-layout-entity.mjs +419 -147
  20. package/fesm2022/acorex-platform-layout-entity.mjs.map +1 -1
  21. package/fesm2022/acorex-platform-layout-views.mjs +851 -162
  22. package/fesm2022/acorex-platform-layout-views.mjs.map +1 -1
  23. package/fesm2022/acorex-platform-layout-widget-core.mjs +378 -103
  24. package/fesm2022/acorex-platform-layout-widget-core.mjs.map +1 -1
  25. 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
  26. 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
  27. 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
  28. 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
  29. package/fesm2022/acorex-platform-layout-widgets.mjs +575 -239
  30. package/fesm2022/acorex-platform-layout-widgets.mjs.map +1 -1
  31. package/fesm2022/acorex-platform-themes-default.mjs +221 -24
  32. package/fesm2022/acorex-platform-themes-default.mjs.map +1 -1
  33. package/fesm2022/{acorex-platform-themes-shared-settings.provider-BUwPqwUS.mjs → acorex-platform-themes-shared-settings.provider-UyKdkeyk.mjs} +48 -1
  34. package/fesm2022/acorex-platform-themes-shared-settings.provider-UyKdkeyk.mjs.map +1 -0
  35. package/fesm2022/acorex-platform-themes-shared.mjs +103 -24
  36. package/fesm2022/acorex-platform-themes-shared.mjs.map +1 -1
  37. package/package.json +1 -1
  38. package/types/acorex-platform-auth.d.ts +1 -0
  39. package/types/acorex-platform-common.d.ts +202 -151
  40. package/types/acorex-platform-core.d.ts +161 -6
  41. package/types/acorex-platform-layout-builder.d.ts +10 -20
  42. package/types/acorex-platform-layout-components.d.ts +14 -1
  43. package/types/acorex-platform-layout-entity-contracts.d.ts +27 -2
  44. package/types/acorex-platform-layout-entity.d.ts +61 -12
  45. package/types/acorex-platform-layout-views.d.ts +101 -19
  46. package/types/acorex-platform-layout-widget-core.d.ts +65 -9
  47. package/types/acorex-platform-layout-widgets.d.ts +48 -26
  48. package/types/acorex-platform-themes-default.d.ts +56 -5
  49. package/types/acorex-platform-themes-shared.d.ts +4 -0
  50. package/fesm2022/acorex-platform-common-common-settings.provider-VfmZct9H.mjs.map +0 -1
  51. package/fesm2022/acorex-platform-layout-components-binding-expression-editor-popup.component-CXEdvDTf.mjs.map +0 -1
  52. 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,37 @@ 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
+ /** 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
- updateStatus() {
78
- this.status$.update(() => this.detectStatus());
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
- 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)) {
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
- if (statuses.some((status) => status === AXPWidgetStatus.Processing)) {
90
- return AXPPageStatus.Processing;
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(() => [AXPWidgetStatus.Rendering, AXPWidgetStatus.Processing].includes(this.status()), ...(ngDevMode ? [{ debugName: "isBusy" }] : /* istanbul ignore next */ []));
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.updateStatus();
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.setValue(this.defaultValue);
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
- !isEqual(oldValue, savedAtPath)) {
409
- this.contextService.update(path, cloneDeep(savedAtPath));
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
- this.contextService.update(path, value);
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
- // 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);
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
- // 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, {
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: 500,
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
- 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);
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
- this.dataSource.set(convertArrayToDataSource([]));
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.dataSource.set(convertArrayToDataSource([]));
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
- else {
568
- items.push(await this.extractItem(value));
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-gap-1 ax-select-none"
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
- [style.justify-content]="getHeaderJustifyContent()"
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 class="axp-widget-column-header__caption ax-truncate">{{ caption | translate | async }}</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-gap-1 ax-select-none"
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
- [style.justify-content]="getHeaderJustifyContent()"
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 class="axp-widget-column-header__caption ax-truncate">{{ caption | translate | async }}</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
- /** Idle period after widgets register / hydrate before committing the saved baseline. */
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
- const commit = () => {
1648
- if (this.builderService.registeredWidgetsCount() === 0) {
1649
- this.savedBaselineTimer = setTimeout(commit, FORM_SAVED_BASELINE_IDLE_MS);
1650
- return;
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.savedBaselineTimer = setTimeout(commit, FORM_SAVED_BASELINE_IDLE_MS);
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();