@praxisui/dynamic-form 9.0.5-rc.10 → 9.0.5-rc.12

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/README.md CHANGED
@@ -33,10 +33,14 @@ Peer dependencies:
33
33
 
34
34
  When `/schemas/filtered` publishes root-level `x-ui.formEffects`, the form can call a typed backend determination after a valid source field changes and fill mapped targets. The contract is backend-owned and transient: it is not copied to `FormConfig` and cannot be authored as a local callback.
35
35
 
36
- The runtime waits until entity/initial-value hydration finishes, debounces source changes, cancels stale requests, applies complete responses atomically and preserves user input by default with `if-pristine`. Hosts can observe metadata-only outcomes through `formEffectExecuted`; control and response values are not emitted in that diagnostic.
36
+ The runtime waits until entity/initial-value hydration finishes, tracks asynchronous source validation, debounces source changes, cancels stale requests, applies complete responses atomically and preserves user input by default with `if-pristine`. A determination is pending from the source change through completion, including the debounce window, and submit is held until that work finishes. Failures receive a localized default message.
37
+
38
+ Hosts can observe metadata-only outcomes through `formEffectExecuted`; control and response values are not emitted. Events use `pending`, `success`, `skipped` and `error`. A successful event can report `appliedTargetFields` and `preservedTargetFields` so operational telemetry distinguishes configured bindings from actual patches without exposing business values.
37
39
 
38
40
  Use `optionSource.dependsOn` for dependent option lists and a workflow action for persisted business transitions. Form effects are reserved for non-persisting queries such as postal-code or governed classification lookup.
39
41
 
42
+ For consequential corporate decisions, the determination is only a draft enrichment or preview. The final backend command must revalidate any fiscal, pricing, eligibility, compliance or approval outcome against its governed and versioned domain authority.
43
+
40
44
  ## Minimum Local Runtime
41
45
 
42
46
  Use a local `FormConfig` when the host already knows the sections and fields.
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": "1.0.0",
3
- "generatedAt": "2026-08-12T23:49:15.227Z",
3
+ "generatedAt": "2026-08-13T13:48:00.302Z",
4
4
  "packageName": "@praxisui/dynamic-form",
5
- "packageVersion": "9.0.5-rc.10",
5
+ "packageVersion": "9.0.5-rc.12",
6
6
  "sourceRegistry": "praxis-component-registry-ingestion",
7
7
  "sourceRegistryVersion": "1.0.0",
8
8
  "componentCount": 2,
@@ -355,7 +355,7 @@
355
355
  "name": "formEffectExecuted",
356
356
  "type": "FormEffectExecutionEvent",
357
357
  "required": false,
358
- "description": "Emite diagnostico sem valores sensiveis após executar, ignorar ou falhar uma determinação publicada pelo backend."
358
+ "description": "Emite diagnóstico sem valores sensíveis ao iniciar, executar, ignorar ou falhar uma determinação, distinguindo alvos aplicados e preservados."
359
359
  }
360
360
  ],
361
361
  "configSchemaId": "FormConfig",
@@ -21,7 +21,7 @@ import { MatSnackBarModule } from '@angular/material/snack-bar';
21
21
  import * as i19 from '@angular/material/badge';
22
22
  import { MatBadgeModule } from '@angular/material/badge';
23
23
  import { map, firstValueFrom, BehaviorSubject, Subject, Subscription, merge, EMPTY, throwError, combineLatest, debounceTime as debounceTime$1, takeUntil as takeUntil$1 } from 'rxjs';
24
- import { debounceTime, map as map$1, distinctUntilChanged, switchMap, catchError, takeUntil, skip, take, timeout, tap, startWith, finalize } from 'rxjs/operators';
24
+ import { tap, debounceTime, map as map$1, filter, switchMap, catchError, finalize, takeUntil, skip, take, timeout, startWith } from 'rxjs/operators';
25
25
  import * as i1 from '@praxisui/core';
26
26
  import { serializeEntityLookupValueForPayload, PraxisI18nService, PraxisIconButtonComponent, PraxisIconDirective, providePraxisI18nConfig, RULE_PROPERTY_SCHEMA, FIELD_METADATA_CAPABILITIES, migrateFormLayoutRule, PraxisJsonLogicService, normalizeFormLayoutItems, deepMerge, createDefaultFormConfig, normalizeFormConfig as normalizeFormConfig$1, ensureIds, createEmptyRichContentDocument, resolveHidden, resolveOrder, resolveSpan, projectGroupedCommandPartialRows, isPraxisRuntimeGlobalActionEffect, ASYNC_CONFIG_STORAGE, withFormConfigSections, createFieldLayoutItem, getFormLayoutFieldNames, LoggerService, createCorporateLoggerConfig, ConsoleLoggerSink, GlobalActionService, ResourceQuickConnectComponent, composeHeadersWithVersion, buildApiUrl, mapFieldDefinitionsToMetadata, reconcileFormConfig, materializeFormLayoutFromMetadata, syncWithServerMetadata, PRAXIS_LOADING_CTX, evaluateFieldAccess, normalizeControlTypeKey, normalizePraxisEffectPolicy, buildPraxisEffectDistinctKey, getFormColumnFieldNames, resolveGroupedCommandPartialRowSpans, resolveOffset, buildSchemaIdStorageKeySegment, buildSchemaId, fetchWithETag, resolveControlTypeAlias, getTextTransformer, MemoryCacheAdapter, LocalStorageCacheAdapter, SchemaMetadataClient, normalizeFormEffects, CONNECTION_STORAGE, API_URL, PRAXIS_LOADING_RENDERER, FORM_HOOKS_PRESETS, EmptyStateCardComponent, isValidFormConfig, ComponentMetadataRegistry, FieldControlType, isRequiredGlobalActionPayloadMissing, GLOBAL_ACTION_CATALOG, PRAXIS_GLOBAL_ACTION_CATALOG, getGlobalActionCatalog, SURFACE_OPEN_I18N_NAMESPACE, getGlobalActionUiSchema, getRequiredGlobalActionPayloadKeys, hasMeaningfulGlobalActionPayloadValue, SurfaceOpenActionEditorComponent, SURFACE_OPEN_I18N_CONFIG, validateGlobalActionRefs, normalizeFormMetadata } from '@praxisui/core';
27
27
  import * as i1$1 from '@praxisui/dynamic-fields';
@@ -631,6 +631,9 @@ const PRAXIS_DYNAMIC_FORM_I18N_CONFIG = {
631
631
  'layout.defaultSectionTitle': 'Informações',
632
632
  'actions.invalidRequired': 'Preencha os campos obrigatórios para continuar.',
633
633
  'actions.invalidRequiredWithFields': 'Preencha: {{fields}}.',
634
+ 'runtime.formEffect.error': 'Não foi possível atualizar os campos relacionados. Revise os dados ou tente novamente.',
635
+ 'runtime.formEffect.submitPending': 'Aguarde a atualização dos campos relacionados antes de salvar.',
636
+ 'common.dismiss': 'Fechar',
634
637
  'config.tabs.mobileLabel': 'Seção do editor',
635
638
  'config.tabs.general': 'Geral',
636
639
  'config.tabs.schema': 'Verificação de Schema',
@@ -1116,6 +1119,9 @@ const PRAXIS_DYNAMIC_FORM_I18N_CONFIG = {
1116
1119
  ]`,
1117
1120
  },
1118
1121
  'en-US': {
1122
+ 'runtime.formEffect.error': 'Related fields could not be updated. Review the data or try again.',
1123
+ 'runtime.formEffect.submitPending': 'Wait for related fields to finish updating before saving.',
1124
+ 'common.dismiss': 'Dismiss',
1119
1125
  'settings.title': 'Form Configuration',
1120
1126
  'layout.defaultSectionTitle': 'Information',
1121
1127
  'actions.invalidRequired': 'Complete the required fields to continue.',
@@ -10566,9 +10572,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
10566
10572
  * scripts, expressions, arbitrary URLs, headers, or methods supplied by consumers.
10567
10573
  */
10568
10574
  class FormEffectRuntimeService {
10575
+ pendingEffectsByForm = new WeakMap();
10576
+ hasPending(form) {
10577
+ return (this.pendingEffectsByForm.get(form)?.size ?? 0) > 0;
10578
+ }
10569
10579
  connect(form, effects, executor, onEvent) {
10570
10580
  const composite = new Subscription();
10571
10581
  for (const effect of effects) {
10582
+ let lastExecutedFingerprint = null;
10572
10583
  const sourceControls = effect.trigger.fields
10573
10584
  .map((field) => form.get(field))
10574
10585
  .filter((control) => !!control);
@@ -10576,29 +10587,40 @@ class FormEffectRuntimeService {
10576
10587
  onEvent?.(this.event(effect, 'skipped', createCorrelationId(), 0, 'source-control-missing'));
10577
10588
  continue;
10578
10589
  }
10579
- const sourceChanges$ = merge(...sourceControls.map((control) => control.valueChanges));
10590
+ const sourceChanges$ = merge(...sourceControls.flatMap((control) => [control.valueChanges, control.statusChanges]));
10580
10591
  composite.add(sourceChanges$
10581
- .pipe(debounceTime(effect.trigger.debounceMs), map$1(() => this.buildInvocation(form, effect, sourceControls)), distinctUntilChanged((previous, current) => previous.fingerprint === current.fingerprint), switchMap((invocation) => {
10582
- if (effect.trigger.requiresValidSources && sourceControls.some((control) => control.invalid)) {
10592
+ .pipe(tap(() => this.markPending(form, effect.id, true)), debounceTime(effect.trigger.debounceMs), map$1(() => this.buildInvocation(form, effect, sourceControls)), filter((invocation) => {
10593
+ if (effect.trigger.requiresValidSources &&
10594
+ sourceControls.some((control) => control.pending || control.invalid)) {
10595
+ this.markPending(form, effect.id, false);
10583
10596
  onEvent?.(this.event(effect, 'skipped', invocation.correlationId, 0, 'invalid-source'));
10584
- return EMPTY;
10597
+ return false;
10585
10598
  }
10599
+ if (invocation.fingerprint === lastExecutedFingerprint) {
10600
+ this.markPending(form, effect.id, false);
10601
+ return false;
10602
+ }
10603
+ return true;
10604
+ }), switchMap((invocation) => {
10605
+ lastExecutedFingerprint = invocation.fingerprint;
10586
10606
  const startedAt = Date.now();
10607
+ this.markPending(form, effect.id, true);
10608
+ onEvent?.(this.event(effect, 'pending', invocation.correlationId, 0));
10587
10609
  return executor(effect, invocation.body, invocation.correlationId).pipe(map$1((response) => ({ response, startedAt, correlationId: invocation.correlationId })), catchError(() => {
10588
10610
  onEvent?.(this.event(effect, 'error', invocation.correlationId, Date.now() - startedAt, 'operation-failed'));
10589
10611
  return EMPTY;
10590
- }));
10612
+ }), finalize(() => this.markPending(form, effect.id, false)));
10591
10613
  }))
10592
10614
  .subscribe(({ response, startedAt, correlationId }) => {
10593
- const patch = this.buildAtomicPatch(form, effect, response);
10594
- if (patch === null) {
10615
+ const result = this.buildAtomicPatch(form, effect, response);
10616
+ if (result === null) {
10595
10617
  onEvent?.(this.event(effect, 'error', correlationId, Date.now() - startedAt, 'response-binding-missing'));
10596
10618
  return;
10597
10619
  }
10598
- if (Object.keys(patch).length) {
10599
- form.patchValue(patch, { emitEvent: true });
10620
+ if (Object.keys(result.patch).length) {
10621
+ form.patchValue(result.patch, { emitEvent: true });
10600
10622
  }
10601
- onEvent?.(this.event(effect, 'success', correlationId, Date.now() - startedAt));
10623
+ onEvent?.(this.event(effect, 'success', correlationId, Date.now() - startedAt, undefined, Object.keys(result.patch), result.preserved));
10602
10624
  }));
10603
10625
  }
10604
10626
  return composite;
@@ -10621,22 +10643,25 @@ class FormEffectRuntimeService {
10621
10643
  return null;
10622
10644
  }
10623
10645
  const patch = {};
10646
+ const preserved = [];
10624
10647
  for (const binding of effect.outputs) {
10625
10648
  const control = form.get(binding.formField);
10626
10649
  if (!control) {
10627
10650
  return null;
10628
10651
  }
10629
10652
  if (binding.writePolicy === 'if-pristine' && !control.pristine) {
10653
+ preserved.push(binding.formField);
10630
10654
  continue;
10631
10655
  }
10632
10656
  if (binding.writePolicy === 'if-empty' && !isEmptyValue(control.value)) {
10657
+ preserved.push(binding.formField);
10633
10658
  continue;
10634
10659
  }
10635
10660
  patch[binding.formField] = response[binding.operationField];
10636
10661
  }
10637
- return patch;
10662
+ return { patch, preserved };
10638
10663
  }
10639
- event(effect, status, correlationId, durationMs, reason) {
10664
+ event(effect, status, correlationId, durationMs, reason, appliedTargetFields, preservedTargetFields) {
10640
10665
  return {
10641
10666
  effectId: effect.id,
10642
10667
  operationId: effect.operation.operationId,
@@ -10646,8 +10671,22 @@ class FormEffectRuntimeService {
10646
10671
  correlationId,
10647
10672
  durationMs,
10648
10673
  ...(reason ? { reason } : {}),
10674
+ ...(appliedTargetFields ? { appliedTargetFields } : {}),
10675
+ ...(preservedTargetFields ? { preservedTargetFields } : {}),
10649
10676
  };
10650
10677
  }
10678
+ markPending(form, effectId, pending) {
10679
+ const effects = this.pendingEffectsByForm.get(form) ?? new Set();
10680
+ if (pending) {
10681
+ effects.add(effectId);
10682
+ this.pendingEffectsByForm.set(form, effects);
10683
+ return;
10684
+ }
10685
+ effects.delete(effectId);
10686
+ if (!effects.size) {
10687
+ this.pendingEffectsByForm.delete(form);
10688
+ }
10689
+ }
10651
10690
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: FormEffectRuntimeService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
10652
10691
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: FormEffectRuntimeService, providedIn: 'root' });
10653
10692
  }
@@ -12812,9 +12851,15 @@ class PraxisDynamicForm {
12812
12851
  return this.http
12813
12852
  .post(this.resolveApiHref(effect.operation.path), requestBody, { headers })
12814
12853
  .pipe(map$1((response) => this.unwrapRestApiResponse(response)));
12815
- }, (event) => this.formEffectExecuted.emit(event));
12854
+ }, (event) => this.handleFormEffectExecutionEvent(event));
12816
12855
  });
12817
12856
  }
12857
+ handleFormEffectExecutionEvent(event) {
12858
+ this.formEffectExecuted.emit(event);
12859
+ if (event.status !== 'error')
12860
+ return;
12861
+ this.snackBar.open(this.i18n.t('runtime.formEffect.error', undefined, 'Não foi possível atualizar os campos relacionados. Revise os dados ou tente novamente.', PRAXIS_DYNAMIC_FORM_I18N_NAMESPACE), this.i18n.t('common.dismiss', undefined, 'Fechar', PRAXIS_DYNAMIC_FORM_I18N_NAMESPACE), { duration: 7000 });
12862
+ }
12818
12863
  resetDependencyPolicySubscription() {
12819
12864
  this.dependencyPolicySubscription?.unsubscribe();
12820
12865
  this.dependencyPolicySubscription = null;
@@ -15028,6 +15073,10 @@ class PraxisDynamicForm {
15028
15073
  if (this.entityHydrationPending) {
15029
15074
  return;
15030
15075
  }
15076
+ if (this.formEffectRuntime.hasPending(this.form)) {
15077
+ this.snackBar.open(this.i18n.t('runtime.formEffect.submitPending', undefined, 'Aguarde a atualização dos campos relacionados antes de salvar.', PRAXIS_DYNAMIC_FORM_I18N_NAMESPACE), this.i18n.t('common.dismiss', undefined, 'Fechar', PRAXIS_DYNAMIC_FORM_I18N_NAMESPACE), { duration: 5000 });
15078
+ return;
15079
+ }
15031
15080
  this.clearSubmitFieldErrors();
15032
15081
  // Hooks: beforeValidate
15033
15082
  try {
@@ -31253,7 +31302,7 @@ const PRAXIS_DYNAMIC_FORM_COMPONENT_METADATA = {
31253
31302
  { name: 'customAction', type: 'FormCustomActionEvent', description: 'Emite quando uma acao customizada e executada' },
31254
31303
  { name: 'actionConfirmation', type: 'FormActionConfirmationEvent', description: 'Resultado de confirmacoes de acoes' },
31255
31304
  { name: 'schemaStatusChange', type: '{ outdated: boolean; serverHash?: string; lastVerifiedAt?: string; formId?: string }', description: 'Emite quando o status do schema muda' },
31256
- { name: 'formEffectExecuted', type: 'FormEffectExecutionEvent', description: 'Emite diagnostico sem valores sensiveis após executar, ignorar ou falhar uma determinação publicada pelo backend.' },
31305
+ { name: 'formEffectExecuted', type: 'FormEffectExecutionEvent', description: 'Emite diagnóstico sem valores sensíveis ao iniciar, executar, ignorar ou falhar uma determinação, distinguindo alvos aplicados e preservados.' },
31257
31306
  ],
31258
31307
  actions: [
31259
31308
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@praxisui/dynamic-form",
3
- "version": "9.0.5-rc.10",
3
+ "version": "9.0.5-rc.12",
4
4
  "description": "Angular dynamic form engine for Praxis UI: metadata-driven forms, hooks, and services integrating @praxisui/* packages.",
5
5
  "peerDependencies": {
6
6
  "@angular/common": "^21.0.0",
@@ -9,13 +9,13 @@
9
9
  "@angular/forms": "^21.0.0",
10
10
  "@angular/material": "^21.0.0",
11
11
  "@angular/router": "^21.0.0",
12
- "@praxisui/ai": "^9.0.5-rc.10",
13
- "@praxisui/dynamic-fields": "^9.0.5-rc.10",
14
- "@praxisui/metadata-editor": "^9.0.5-rc.10",
15
- "@praxisui/rich-content": "^9.0.5-rc.10",
16
- "@praxisui/settings-panel": "^9.0.5-rc.10",
17
- "@praxisui/visual-builder": "^9.0.5-rc.10",
18
- "@praxisui/core": "^9.0.5-rc.10",
12
+ "@praxisui/ai": "^9.0.5-rc.12",
13
+ "@praxisui/dynamic-fields": "^9.0.5-rc.12",
14
+ "@praxisui/metadata-editor": "^9.0.5-rc.12",
15
+ "@praxisui/rich-content": "^9.0.5-rc.12",
16
+ "@praxisui/settings-panel": "^9.0.5-rc.12",
17
+ "@praxisui/visual-builder": "^9.0.5-rc.12",
18
+ "@praxisui/core": "^9.0.5-rc.12",
19
19
  "rxjs": "^7.8.0"
20
20
  },
21
21
  "dependencies": {
@@ -79,9 +79,9 @@ Este documento e a referencia canonica da API JSON de praxis-dynamic-form.
79
79
 
80
80
  - `x-ui.formEffects` e lido exclusivamente do schema raiz retornado por `/schemas/filtered`; nao integra nem e persistido em `FormConfig`.
81
81
  - O runtime aceita somente o contrato fechado normalizado por `normalizeFormEffects`: gatilho `value-change`, operação `POST`, bindings de entrada/saida e politicas `if-pristine`, `if-empty` ou `replace`.
82
- - Determinacoes nao executam na hidratacao inicial. Depois da hidratacao, mudanças nos controles fonte usam debounce e cancelamento latest-wins.
82
+ - Determinacoes nao executam na hidratacao inicial. Depois da hidratacao, mudancas nos controles fonte acompanham validacao assincrona, usam debounce e cancelamento latest-wins. O estado permanece pending inclusive durante o debounce, e o submit nao prossegue enquanto a determinacao mais recente estiver pendente.
83
83
  - A resposta so gera patch quando todos os bindings de saida existem; o patch e atomico e respeita a politica de escrita de cada alvo.
84
- - `formEffectExecuted` publica apenas IDs, nomes de campos, estado, duracao, correlation ID e motivo; valores do formulario e da resposta nao participam do diagnostico.
84
+ - `formEffectExecuted` publica apenas IDs, nomes de campos, estado (`pending`, `success`, `skipped` ou `error`), duracao, correlation ID e motivo. Em sucesso, pode distinguir alvos efetivamente aplicados e preservados; valores do formulario e da resposta nao participam do diagnostico.
85
85
  - Form effects sao backend-owned e nao aparecem como capacidade autoravel por editor ou IA. Cascatas de opcoes continuam em `optionSource.dependsOn`; transicoes persistentes continuam em workflow actions.
86
86
 
87
87
  ## AI assistant and semantic authoring boundary
@@ -367,10 +367,13 @@ type FormEffectExecutor = (effect: FormEffectDefinition, requestBody: Record<str
367
367
  * scripts, expressions, arbitrary URLs, headers, or methods supplied by consumers.
368
368
  */
369
369
  declare class FormEffectRuntimeService {
370
+ private readonly pendingEffectsByForm;
371
+ hasPending(form: FormGroup): boolean;
370
372
  connect(form: FormGroup, effects: FormEffectDefinition[], executor: FormEffectExecutor, onEvent?: (event: FormEffectExecutionEvent) => void): Subscription;
371
373
  private buildInvocation;
372
374
  private buildAtomicPatch;
373
375
  private event;
376
+ private markPending;
374
377
  static ɵfac: i0.ɵɵFactoryDeclaration<FormEffectRuntimeService, never>;
375
378
  static ɵprov: i0.ɵɵInjectableDeclaration<FormEffectRuntimeService>;
376
379
  }
@@ -1049,6 +1052,7 @@ declare class PraxisDynamicForm implements OnInit, OnChanges, OnDestroy {
1049
1052
  private resetFormValueChangesSubscription;
1050
1053
  private resetFormEffectSubscription;
1051
1054
  private scheduleFormEffectsActivation;
1055
+ private handleFormEffectExecutionEvent;
1052
1056
  private resetDependencyPolicySubscription;
1053
1057
  private setupEntityLookupDependencyPolicies;
1054
1058
  private applyEntityLookupDependencyPolicy;