@praxisui/dynamic-form 9.0.5 → 9.0.7
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 +3 -3
- package/ai/component-registry.json +2 -2
- package/docs/dynamic-form-authoring-document-semantics.md +4 -1
- package/fesm2022/praxisui-dynamic-form.mjs +218 -39
- package/package.json +8 -8
- package/src/lib/praxis-dynamic-form.json-api.md +2 -2
- package/types/praxisui-dynamic-form.d.ts +8 -0
package/README.md
CHANGED
|
@@ -31,15 +31,15 @@ Peer dependencies:
|
|
|
31
31
|
|
|
32
32
|
## Reactive backend determinations
|
|
33
33
|
|
|
34
|
-
When the exact request schema from `/schemas/filtered` publishes root-level `x-ui.reactiveDeterminations`, the form
|
|
34
|
+
When the exact request schema from `/schemas/filtered` publishes root-level `x-ui.reactiveDeterminations`, the form executes the Metadata-compiled backend capability after a valid source changes and applies server-derived draft values. After edit/create hydration, an already populated source is also validated before the draft can be reported as stable; an empty initial source settles as not applicable without an HTTP call. The projection is one closed graph: if any definition is malformed, the complete projection is rejected instead of partially executing. It remains backend-owned, tenant-neutral and transient: it is never copied to `FormConfig` and cannot be authored as a callback, URL or local write policy.
|
|
35
35
|
|
|
36
36
|
Triggers, source fields and request/response bindings use JSON Pointer. The runtime supports nested object bindings, accepts at most 64 total input/output bindings and rejects duplicate or hierarchically overlapping pointers. It resolves the capability only against the configured API origin and rejects protocol-relative, encoded-separator or cross-origin targets. Output bindings have one closed meaning: server-derived draft values replace their targets. Metadata cannot select `if-pristine`, `if-empty` or another incidental UI policy.
|
|
37
37
|
|
|
38
|
-
The runtime waits until hydration finishes and invalidates the previous generation as soon as a newer relevant value or validation status arrives, before the new debounce completes. `sourcePaths` is an intentional trigger subset of `inputs`: changing a non-trigger input invalidates the current draft, emits `input-changed-awaiting-trigger`, and blocks submit/navigation until a source trigger recalculates it. Every configured response path is resolved before target controls are written; all nested targets are populated before change observers are notified. Chained determinations receive one logical batch signal after the complete commit, rather than one execution per output control. Their dependency order uses hierarchical input/output JSON Pointer overlap, and the canonical projection rejects cycles and overlapping multi-writers. Downstream waits while upstream is pending and runs only after authoritative outputs or confirmed unchanged inputs. Null/undefined upstream outputs and failed, invalid or cancelled upstream generations settle downstream as skipped instead of invoking it with missing or stale data. If a control rejects a write, the runtime attempts to restore earlier writes and reports a terminal failure. This is a coordinated client-side batch, not a transaction or an ACID guarantee.
|
|
38
|
+
The runtime waits until hydration finishes and invalidates the previous generation as soon as a newer relevant value or validation status arrives, before the new debounce completes. `sourcePaths` is an intentional trigger subset of `inputs`: changing a non-trigger input invalidates the current draft, emits `input-changed-awaiting-trigger`, and blocks submit/navigation until a source trigger recalculates it. Every configured response path is resolved before target controls are written; all nested targets are populated before change observers are notified. Chained determinations receive one logical batch signal after the complete commit, rather than one execution per output control. Their dependency order uses hierarchical input/output JSON Pointer overlap, and the canonical projection rejects cycles and overlapping multi-writers. Downstream waits while upstream is pending and runs only after authoritative outputs or confirmed unchanged inputs. Null/undefined upstream outputs and failed, invalid or cancelled upstream generations settle downstream as skipped instead of invoking it with missing or stale data. If a control rejects a write, the runtime attempts to restore earlier writes and reports a terminal failure. This is a coordinated client-side batch, not a transaction or an ACID guarantee. Each capability remains a separate HTTP request, so an activation may occur between two chained previews; only a host command that resolves and revalidates one aggregate can claim snapshot consistency for the complete business decision.
|
|
39
39
|
|
|
40
40
|
A determination remains pending through debounce, asynchronous validation and capability execution. Submit attempts wait with a bounded timeout and repeat the stability check after hooks that may mutate controls. Output controls are disabled transiently as server-owned draft values, but remain present in `getRawValue()`, `valueChange` and submit payloads; this state is not persisted in `FormConfig`.
|
|
41
41
|
|
|
42
|
-
Hosts can observe metadata-only evidence through `reactiveDeterminationExecuted` and aggregate state through `reactiveDeterminationPendingChange`. Before navigating or closing a composed surface, inspect `hasPendingReactiveDeterminations()` and `hasUnsatisfiedReactiveDeterminations()` and await `waitForReactiveDeterminations(timeoutMs?)`; `false` means timeout or that the latest generation did not produce a current authoritative draft, and must block the transition. Cancelled pending correlations terminate as `skipped/superseded`. No event or method exposes form or response values.
|
|
42
|
+
Hosts can observe metadata-only evidence through `reactiveDeterminationExecuted` and aggregate state through `reactiveDeterminationPendingChange`. Before navigating or closing a composed surface, inspect `hasPendingReactiveDeterminations()` and `hasUnsatisfiedReactiveDeterminations()` and await `waitForReactiveDeterminations(timeoutMs?)`; `false` means timeout or that the latest generation did not produce a current authoritative draft, and must block the transition. A programmatic change to an owned output invalidates the previous authority and triggers a new determination. Cancelled pending correlations terminate as `skipped/superseded`. No event or method exposes form or response values.
|
|
43
43
|
|
|
44
44
|
Use `optionSource.dependsOn` for dependent option lists and workflow actions for persistent transitions. Recommendations, side-effect refresh and backend validation are separate capabilities. The final backend command must still authorize, recompute or validate fiscal, pricing, eligibility, compliance and approval outcomes.
|
|
45
45
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": "1.0.0",
|
|
3
|
-
"generatedAt": "2026-08-
|
|
3
|
+
"generatedAt": "2026-08-20T16:41:04.405Z",
|
|
4
4
|
"packageName": "@praxisui/dynamic-form",
|
|
5
|
-
"packageVersion": "9.0.
|
|
5
|
+
"packageVersion": "9.0.7",
|
|
6
6
|
"sourceRegistry": "praxis-component-registry-ingestion",
|
|
7
7
|
"sourceRegistryVersion": "1.0.0",
|
|
8
8
|
"componentCount": 2,
|
|
@@ -4,7 +4,7 @@ doc_type: "adr"
|
|
|
4
4
|
component: "praxis-dynamic-form"
|
|
5
5
|
status: "accepted"
|
|
6
6
|
owner: "praxis-ui"
|
|
7
|
-
last_updated: "2026-
|
|
7
|
+
last_updated: "2026-08-19"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# Dynamic Form Authoring Document Semantics
|
|
@@ -39,6 +39,9 @@ Essa regra evita perda silenciosa do contrato canonico de Entity Lookup durante
|
|
|
39
39
|
- `Apply` e `Save` vindos do editor operam em modo `replace-all`.
|
|
40
40
|
- `reset` do editor limpa todos os modos persistidos do artefato.
|
|
41
41
|
- adapters legados continuam suportados sem limpeza destrutiva de contexto ausente.
|
|
42
|
+
- editores subordinados de seção, linha e coluna devem aplicar alterações pela localização estrutural capturada (`section`, `row`, `column`), nunca por busca global do primeiro `id` coincidente;
|
|
43
|
+
- IDs de linha devem ser únicos dentro da seção e IDs de coluna devem ser únicos dentro da linha;
|
|
44
|
+
- o estado `dirty` desses editores é reversível: restaurar o snapshot inicial deve desabilitar novamente Apply/Save.
|
|
42
45
|
|
|
43
46
|
## Visual Blocks Inside Layout
|
|
44
47
|
|
|
@@ -20,7 +20,7 @@ import * as i9 from '@angular/material/snack-bar';
|
|
|
20
20
|
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
|
-
import { map, Subscription, merge, timer, NEVER, EMPTY, defer, TimeoutError,
|
|
23
|
+
import { map, Subscription, merge, of, timer, NEVER, EMPTY, defer, TimeoutError, Subject, firstValueFrom, BehaviorSubject, throwError, combineLatest, debounceTime as debounceTime$1, takeUntil as takeUntil$1 } from 'rxjs';
|
|
24
24
|
import { filter, map as map$1, switchMap, take, timeout, throwIfEmpty, tap, catchError, ignoreElements, finalize, takeUntil, skip, debounceTime, 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, ResourceDiscoveryService, 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, normalizeReactiveDeterminations, 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';
|
|
@@ -265,6 +265,7 @@ class ReactiveDeterminationRuntimeService {
|
|
|
265
265
|
const determinationKey = determinationKeyFor(definitionIndex, definition);
|
|
266
266
|
let generation = 0;
|
|
267
267
|
let lastSuccessfulFingerprint = null;
|
|
268
|
+
let lastSuccessfulOutputFingerprint = null;
|
|
268
269
|
const triggerControls = this.resolveControls(form, definition.trigger.sourcePaths);
|
|
269
270
|
const inputControls = this.resolveControls(form, definition.inputs.map((binding) => binding.fieldPath));
|
|
270
271
|
const targetControls = this.resolveControls(form, definition.outputs.map((binding) => binding.fieldPath));
|
|
@@ -296,11 +297,24 @@ class ReactiveDeterminationRuntimeService {
|
|
|
296
297
|
}
|
|
297
298
|
composite.add(this.claimTargets(form, targetControls));
|
|
298
299
|
const triggerControlSet = new Set(triggerControls);
|
|
299
|
-
const
|
|
300
|
+
const targetControlSet = new Set(targetControls);
|
|
301
|
+
const dependencyObservedControls = [...new Set([...triggerControls, ...inputControls])];
|
|
302
|
+
const directlyObservedControls = [
|
|
303
|
+
...new Set([...dependencyObservedControls, ...targetControls]),
|
|
304
|
+
];
|
|
300
305
|
const committedControls$ = this.getOrCreateCommittedControls(form);
|
|
301
|
-
const changes$ = merge(
|
|
306
|
+
const changes$ = merge(of({
|
|
307
|
+
shouldExecute: triggerControls.some((control) => hasMeaningfulSourceValue(control.value)),
|
|
308
|
+
initial: true,
|
|
309
|
+
}), ...directlyObservedControls.flatMap((control) => [control.valueChanges, control.statusChanges].map((changes) => changes.pipe(filter(() => !this.publishingCommitByForm.has(form)), map$1(() => ({
|
|
310
|
+
shouldExecute: triggerControlSet.has(control) || targetControlSet.has(control),
|
|
311
|
+
initial: false,
|
|
312
|
+
}))))), committedControls$.pipe(filter((committedControls) => dependencyObservedControls.some((control) => committedControls.has(control))), map$1((committedControls) => ({
|
|
313
|
+
shouldExecute: triggerControls.some((control) => committedControls.has(control)),
|
|
314
|
+
initial: false,
|
|
315
|
+
}))));
|
|
302
316
|
const determinationSubscription = changes$
|
|
303
|
-
.pipe(map$1((shouldExecute) => {
|
|
317
|
+
.pipe(map$1(({ shouldExecute, initial }) => {
|
|
304
318
|
const currentGeneration = ++generation;
|
|
305
319
|
const correlationId = createCorrelationId();
|
|
306
320
|
// Publish the new authority before switchMap finalizes the old one.
|
|
@@ -308,6 +322,10 @@ class ReactiveDeterminationRuntimeService {
|
|
|
308
322
|
if (shouldExecute) {
|
|
309
323
|
publishEvent(this.event(definition, 'pending', correlationId, 0));
|
|
310
324
|
}
|
|
325
|
+
else if (initial) {
|
|
326
|
+
this.satisfyGeneration(form, determinationKey, currentGeneration);
|
|
327
|
+
publishEvent(this.event(definition, 'skipped', correlationId, 0, 'initial-source-empty'));
|
|
328
|
+
}
|
|
311
329
|
else {
|
|
312
330
|
publishEvent(this.event(definition, 'skipped', correlationId, 0, 'input-changed-awaiting-trigger'));
|
|
313
331
|
}
|
|
@@ -340,7 +358,10 @@ class ReactiveDeterminationRuntimeService {
|
|
|
340
358
|
publishEvent(this.event(definition, 'error', signal.correlationId, 0, 'request-binding-invalid'));
|
|
341
359
|
return EMPTY;
|
|
342
360
|
}
|
|
343
|
-
|
|
361
|
+
const currentOutputFingerprint = this.buildOutputFingerprint(form, definition);
|
|
362
|
+
if (invocation.fingerprint === lastSuccessfulFingerprint &&
|
|
363
|
+
currentOutputFingerprint !== null &&
|
|
364
|
+
currentOutputFingerprint === lastSuccessfulOutputFingerprint) {
|
|
344
365
|
this.satisfyGeneration(form, determinationKey, signal.generation);
|
|
345
366
|
publishEvent(this.event(definition, 'skipped', signal.correlationId, 0, 'unchanged-inputs'));
|
|
346
367
|
return EMPTY;
|
|
@@ -358,6 +379,7 @@ class ReactiveDeterminationRuntimeService {
|
|
|
358
379
|
this.commit(form, commit);
|
|
359
380
|
this.satisfyGeneration(form, determinationKey, signal.generation);
|
|
360
381
|
lastSuccessfulFingerprint = invocation.fingerprint;
|
|
382
|
+
lastSuccessfulOutputFingerprint = this.buildOutputFingerprint(form, definition);
|
|
361
383
|
publishEvent(this.event(definition, 'success', signal.correlationId, Date.now() - startedAt, undefined, commit.map((entry) => entry.targetPath)));
|
|
362
384
|
}), catchError((error) => {
|
|
363
385
|
if (this.isCurrentGeneration(form, determinationKey, signal.generation)) {
|
|
@@ -467,6 +489,16 @@ class ReactiveDeterminationRuntimeService {
|
|
|
467
489
|
}
|
|
468
490
|
return entries;
|
|
469
491
|
}
|
|
492
|
+
buildOutputFingerprint(form, definition) {
|
|
493
|
+
const values = [];
|
|
494
|
+
for (const binding of definition.outputs) {
|
|
495
|
+
const control = form.get(jsonPointerSegments(binding.fieldPath));
|
|
496
|
+
if (!control)
|
|
497
|
+
return null;
|
|
498
|
+
values.push(control.value);
|
|
499
|
+
}
|
|
500
|
+
return stableFingerprint(values);
|
|
501
|
+
}
|
|
470
502
|
commit(form, entries) {
|
|
471
503
|
// Reactive Determination outputs have one closed semantic: server-derived
|
|
472
504
|
// draft values. Metadata cannot choose an overwrite policy.
|
|
@@ -703,6 +735,15 @@ function stableFingerprint(value) {
|
|
|
703
735
|
return String(value);
|
|
704
736
|
}
|
|
705
737
|
}
|
|
738
|
+
function hasMeaningfulSourceValue(value) {
|
|
739
|
+
if (value === null || value === undefined)
|
|
740
|
+
return false;
|
|
741
|
+
if (typeof value === 'string')
|
|
742
|
+
return value.trim().length > 0;
|
|
743
|
+
if (Array.isArray(value))
|
|
744
|
+
return value.length > 0;
|
|
745
|
+
return true;
|
|
746
|
+
}
|
|
706
747
|
function createCorrelationId() {
|
|
707
748
|
const runtimeCrypto = globalThis.crypto;
|
|
708
749
|
if (runtimeCrypto && typeof runtimeCrypto.randomUUID === 'function') {
|
|
@@ -11112,6 +11153,12 @@ class PraxisDynamicFormDestroyedError extends Error {
|
|
|
11112
11153
|
const FORM_CONFIG_LOAD_TIMEOUT_MS = 1200;
|
|
11113
11154
|
const FORM_INPUT_PREFS_LOAD_TIMEOUT_MS = 1200;
|
|
11114
11155
|
const SCHEMA_GLOBAL_CONFIG_READY_TIMEOUT_MS = 1200;
|
|
11156
|
+
class InvalidReactiveDeterminationProjectionError extends Error {
|
|
11157
|
+
constructor() {
|
|
11158
|
+
super('Invalid x-ui.reactiveDeterminations projection.');
|
|
11159
|
+
this.name = 'InvalidReactiveDeterminationProjectionError';
|
|
11160
|
+
}
|
|
11161
|
+
}
|
|
11115
11162
|
const PRAXIS_DYNAMIC_FORM_FALLBACK_LOGGER = new LoggerService(createCorporateLoggerConfig('dev'), [new ConsoleLoggerSink()]);
|
|
11116
11163
|
class PraxisDynamicForm {
|
|
11117
11164
|
crud;
|
|
@@ -17506,7 +17553,7 @@ class PraxisDynamicForm {
|
|
|
17506
17553
|
this.debugLog('[PDF] settingsPanel.open(FieldMetadataEditor)', { fieldName, composedTitle });
|
|
17507
17554
|
}
|
|
17508
17555
|
catch { }
|
|
17509
|
-
const ref = this.settingsPanel.
|
|
17556
|
+
const ref = this.settingsPanel.openChild({
|
|
17510
17557
|
id: `field-meta.${this.formId}.${fieldName}`,
|
|
17511
17558
|
title: composedTitle,
|
|
17512
17559
|
titleIcon,
|
|
@@ -17607,31 +17654,37 @@ class PraxisDynamicForm {
|
|
|
17607
17654
|
openRowEditor(row) {
|
|
17608
17655
|
if (!row)
|
|
17609
17656
|
return;
|
|
17657
|
+
const location = this.resolveLayoutEditorLocation('row', row);
|
|
17658
|
+
if (!location)
|
|
17659
|
+
return;
|
|
17610
17660
|
Promise.resolve().then(function () { return rowEditor_component; }).then((m) => {
|
|
17611
17661
|
const { RowEditorComponent } = m;
|
|
17612
|
-
const ref = this.settingsPanel.
|
|
17662
|
+
const ref = this.settingsPanel.openChild({
|
|
17613
17663
|
id: `row.${this.formId || 'form'}.${row.id}`,
|
|
17614
17664
|
title: 'Configurar Linha',
|
|
17615
17665
|
titleIcon: 'view_stream',
|
|
17616
17666
|
content: { component: RowEditorComponent, inputs: { row } },
|
|
17617
17667
|
});
|
|
17618
|
-
ref.applied$.pipe(takeUntil(this.destroy$)).subscribe(() => this.
|
|
17619
|
-
ref.saved$.pipe(takeUntil(this.destroy$)).subscribe(() => this.
|
|
17668
|
+
ref.applied$.pipe(takeUntil(this.destroy$)).subscribe((value) => this.applyRowEditorValue(location, value));
|
|
17669
|
+
ref.saved$.pipe(takeUntil(this.destroy$)).subscribe((value) => this.applyRowEditorValue(location, value));
|
|
17620
17670
|
});
|
|
17621
17671
|
}
|
|
17622
17672
|
openColumnEditor(column) {
|
|
17623
17673
|
if (!column)
|
|
17624
17674
|
return;
|
|
17675
|
+
const location = this.resolveLayoutEditorLocation('column', column);
|
|
17676
|
+
if (!location)
|
|
17677
|
+
return;
|
|
17625
17678
|
Promise.resolve().then(function () { return columnEditor_component; }).then((m) => {
|
|
17626
17679
|
const { ColumnEditorComponent } = m;
|
|
17627
|
-
const ref = this.settingsPanel.
|
|
17680
|
+
const ref = this.settingsPanel.openChild({
|
|
17628
17681
|
id: `column.${this.formId || 'form'}.${column.id}`,
|
|
17629
17682
|
title: 'Configurar Coluna',
|
|
17630
17683
|
titleIcon: 'view_column',
|
|
17631
17684
|
content: { component: ColumnEditorComponent, inputs: { column } },
|
|
17632
17685
|
});
|
|
17633
|
-
ref.applied$.pipe(takeUntil(this.destroy$)).subscribe(() => this.
|
|
17634
|
-
ref.saved$.pipe(takeUntil(this.destroy$)).subscribe(() => this.
|
|
17686
|
+
ref.applied$.pipe(takeUntil(this.destroy$)).subscribe((value) => this.applyColumnEditorValue(location, value));
|
|
17687
|
+
ref.saved$.pipe(takeUntil(this.destroy$)).subscribe((value) => this.applyColumnEditorValue(location, value));
|
|
17635
17688
|
});
|
|
17636
17689
|
}
|
|
17637
17690
|
openSectionEditor(section, focusTarget) {
|
|
@@ -17639,7 +17692,7 @@ class PraxisDynamicForm {
|
|
|
17639
17692
|
return;
|
|
17640
17693
|
Promise.resolve().then(function () { return sectionEditor_component; }).then((m) => {
|
|
17641
17694
|
const { SectionEditorComponent } = m;
|
|
17642
|
-
const ref = this.settingsPanel.
|
|
17695
|
+
const ref = this.settingsPanel.openChild({
|
|
17643
17696
|
id: `section.${this.formId || 'form'}.${section.id}`,
|
|
17644
17697
|
title: section.title || 'Configurar seção',
|
|
17645
17698
|
titleIcon: 'view_agenda',
|
|
@@ -17656,6 +17709,71 @@ class PraxisDynamicForm {
|
|
|
17656
17709
|
});
|
|
17657
17710
|
});
|
|
17658
17711
|
}
|
|
17712
|
+
applyRowEditorValue(location, value) {
|
|
17713
|
+
this.applyNestedLayoutEditorValue('row', location, value);
|
|
17714
|
+
}
|
|
17715
|
+
applyColumnEditorValue(location, value) {
|
|
17716
|
+
this.applyNestedLayoutEditorValue('column', location, value);
|
|
17717
|
+
}
|
|
17718
|
+
resolveLayoutEditorLocation(kind, target) {
|
|
17719
|
+
const sections = this.config?.sections || [];
|
|
17720
|
+
for (let sectionIndex = 0; sectionIndex < sections.length; sectionIndex++) {
|
|
17721
|
+
const section = sections[sectionIndex];
|
|
17722
|
+
for (let rowIndex = 0; rowIndex < (section.rows || []).length; rowIndex++) {
|
|
17723
|
+
const row = section.rows[rowIndex];
|
|
17724
|
+
if (kind === 'row' && row === target) {
|
|
17725
|
+
return { sectionIndex, rowIndex, sectionId: section.id, rowId: row.id };
|
|
17726
|
+
}
|
|
17727
|
+
if (kind === 'column') {
|
|
17728
|
+
const columnIndex = (row.columns || []).findIndex((candidate) => candidate === target);
|
|
17729
|
+
if (columnIndex >= 0) {
|
|
17730
|
+
return {
|
|
17731
|
+
sectionIndex,
|
|
17732
|
+
rowIndex,
|
|
17733
|
+
columnIndex,
|
|
17734
|
+
sectionId: section.id,
|
|
17735
|
+
rowId: row.id,
|
|
17736
|
+
columnId: row.columns[columnIndex]?.id,
|
|
17737
|
+
};
|
|
17738
|
+
}
|
|
17739
|
+
}
|
|
17740
|
+
}
|
|
17741
|
+
}
|
|
17742
|
+
return undefined;
|
|
17743
|
+
}
|
|
17744
|
+
applyNestedLayoutEditorValue(kind, location, value) {
|
|
17745
|
+
if (!value || typeof value !== 'object')
|
|
17746
|
+
return;
|
|
17747
|
+
const sourceSections = this.config?.sections || [];
|
|
17748
|
+
const targetSection = sourceSections[location.sectionIndex];
|
|
17749
|
+
const targetRow = targetSection?.rows?.[location.rowIndex];
|
|
17750
|
+
const targetColumn = targetRow?.columns?.[location.columnIndex ?? -1];
|
|
17751
|
+
if (!targetSection ||
|
|
17752
|
+
targetSection.id !== location.sectionId ||
|
|
17753
|
+
!targetRow ||
|
|
17754
|
+
targetRow.id !== location.rowId ||
|
|
17755
|
+
(kind === 'column' && (!targetColumn || targetColumn.id !== location.columnId)))
|
|
17756
|
+
return;
|
|
17757
|
+
const sections = structuredClone(sourceSections);
|
|
17758
|
+
const positionedRow = sections[location.sectionIndex].rows[location.rowIndex];
|
|
17759
|
+
if (kind === 'row') {
|
|
17760
|
+
sections[location.sectionIndex].rows[location.rowIndex] = {
|
|
17761
|
+
...positionedRow,
|
|
17762
|
+
...structuredClone(value),
|
|
17763
|
+
};
|
|
17764
|
+
}
|
|
17765
|
+
else {
|
|
17766
|
+
const columnIndex = location.columnIndex;
|
|
17767
|
+
positionedRow.columns[columnIndex] = {
|
|
17768
|
+
...positionedRow.columns[columnIndex],
|
|
17769
|
+
...structuredClone(value),
|
|
17770
|
+
};
|
|
17771
|
+
}
|
|
17772
|
+
this.config = { ...this.config, sections };
|
|
17773
|
+
this.configChange.emit(this.config);
|
|
17774
|
+
this.emitConfigPatchChange();
|
|
17775
|
+
this.cdr.detectChanges();
|
|
17776
|
+
}
|
|
17659
17777
|
applySectionEditorValue(section, value) {
|
|
17660
17778
|
if (!section || !value || typeof value !== 'object') {
|
|
17661
17779
|
this.cdr.detectChanges();
|
|
@@ -20058,15 +20176,24 @@ class PraxisDynamicForm {
|
|
|
20058
20176
|
if (hooks && typeof hooks === 'object') {
|
|
20059
20177
|
this.schemaRootHooks = hooks;
|
|
20060
20178
|
}
|
|
20179
|
+
const rawDeterminations = rootUi && typeof rootUi === 'object'
|
|
20180
|
+
? rootUi.reactiveDeterminations
|
|
20181
|
+
: undefined;
|
|
20061
20182
|
const normalizedDeterminations = ctx.schemaType === 'request'
|
|
20062
|
-
? normalizeReactiveDeterminations(
|
|
20063
|
-
? rootUi.reactiveDeterminations
|
|
20064
|
-
: undefined)
|
|
20183
|
+
? normalizeReactiveDeterminations(rawDeterminations)
|
|
20065
20184
|
: [];
|
|
20185
|
+
if (ctx.schemaType === 'request' && rawDeterminations !== undefined && (!Array.isArray(rawDeterminations)
|
|
20186
|
+
|| normalizedDeterminations.length !== rawDeterminations.length)) {
|
|
20187
|
+
throw new InvalidReactiveDeterminationProjectionError();
|
|
20188
|
+
}
|
|
20066
20189
|
this.schemaRootReactiveDeterminations = normalizedDeterminations.filter((definition) => !this.schemaRootOperationId
|
|
20067
20190
|
|| definition.scope.schemaOperationId === this.schemaRootOperationId);
|
|
20068
20191
|
}
|
|
20069
|
-
catch {
|
|
20192
|
+
catch (error) {
|
|
20193
|
+
if (error instanceof InvalidReactiveDeterminationProjectionError) {
|
|
20194
|
+
throw error;
|
|
20195
|
+
}
|
|
20196
|
+
}
|
|
20070
20197
|
const schemaId = buildSchemaId(schemaContext);
|
|
20071
20198
|
this.lastSchemaMeta = { schemaId, serverHash: entry.schemaHash, context: schemaContext };
|
|
20072
20199
|
this.schemaCache = defs;
|
|
@@ -20091,6 +20218,9 @@ class PraxisDynamicForm {
|
|
|
20091
20218
|
return defs;
|
|
20092
20219
|
}
|
|
20093
20220
|
catch (error) {
|
|
20221
|
+
if (error instanceof InvalidReactiveDeterminationProjectionError) {
|
|
20222
|
+
throw error;
|
|
20223
|
+
}
|
|
20094
20224
|
if (this.isDestroyed || error instanceof PraxisDynamicFormDestroyedError) {
|
|
20095
20225
|
throw new PraxisDynamicFormDestroyedError();
|
|
20096
20226
|
}
|
|
@@ -20473,6 +20603,30 @@ class FormConfigService {
|
|
|
20473
20603
|
else {
|
|
20474
20604
|
ids.add(section.id);
|
|
20475
20605
|
}
|
|
20606
|
+
const rowIds = new Set();
|
|
20607
|
+
for (const row of section.rows || []) {
|
|
20608
|
+
if (!row.id) {
|
|
20609
|
+
errors.push(`Row id is required in section: ${section.id}`);
|
|
20610
|
+
}
|
|
20611
|
+
else if (rowIds.has(row.id)) {
|
|
20612
|
+
errors.push(`Duplicate row id in section ${section.id}: ${row.id}`);
|
|
20613
|
+
}
|
|
20614
|
+
else {
|
|
20615
|
+
rowIds.add(row.id);
|
|
20616
|
+
}
|
|
20617
|
+
const columnIds = new Set();
|
|
20618
|
+
for (const column of row.columns || []) {
|
|
20619
|
+
if (!column.id) {
|
|
20620
|
+
errors.push(`Column id is required in row: ${row.id}`);
|
|
20621
|
+
}
|
|
20622
|
+
else if (columnIds.has(column.id)) {
|
|
20623
|
+
errors.push(`Duplicate column id in row ${row.id}: ${column.id}`);
|
|
20624
|
+
}
|
|
20625
|
+
else {
|
|
20626
|
+
columnIds.add(column.id);
|
|
20627
|
+
}
|
|
20628
|
+
}
|
|
20629
|
+
}
|
|
20476
20630
|
}
|
|
20477
20631
|
return errors;
|
|
20478
20632
|
}
|
|
@@ -22715,7 +22869,7 @@ class SectionConfiguratorComponent {
|
|
|
22715
22869
|
const SectionEditorComponent = m.SectionEditorComponent;
|
|
22716
22870
|
if (!SectionEditorComponent)
|
|
22717
22871
|
return;
|
|
22718
|
-
const ref = this.settingsPanel.
|
|
22872
|
+
const ref = this.settingsPanel.openChild({
|
|
22719
22873
|
id: `layout.section.${this.section.id || this.sectionIndex}`,
|
|
22720
22874
|
title: this.section.title || `Seção ${this.sectionIndex + 1}`,
|
|
22721
22875
|
titleIcon: 'view_agenda',
|
|
@@ -22724,8 +22878,8 @@ class SectionConfiguratorComponent {
|
|
|
22724
22878
|
inputs: { section: this.section, fieldMetadata: this.fieldMetadata },
|
|
22725
22879
|
},
|
|
22726
22880
|
});
|
|
22727
|
-
ref.applied$?.subscribe(() => this.onSectionUpdated());
|
|
22728
|
-
ref.saved$?.subscribe(() => this.onSectionUpdated());
|
|
22881
|
+
ref.applied$?.subscribe((value) => this.onSectionUpdated(value));
|
|
22882
|
+
ref.saved$?.subscribe((value) => this.onSectionUpdated(value));
|
|
22729
22883
|
});
|
|
22730
22884
|
}
|
|
22731
22885
|
getSectionHeaderPreview() {
|
|
@@ -23317,7 +23471,7 @@ class LayoutEditorComponent {
|
|
|
23317
23471
|
this.applyFieldMetadataPatch(fieldName, patch);
|
|
23318
23472
|
};
|
|
23319
23473
|
const title = this.t('field.editMetadataTitle').replace('{field}', field.label || field.name);
|
|
23320
|
-
const ref = this.settingsPanel.
|
|
23474
|
+
const ref = this.settingsPanel.openChild({
|
|
23321
23475
|
id: `layout.fieldMetadata.${fieldName}`,
|
|
23322
23476
|
title,
|
|
23323
23477
|
titleIcon: 'edit_note',
|
|
@@ -23367,7 +23521,7 @@ class LayoutEditorComponent {
|
|
|
23367
23521
|
if (!section?.id || !row?.id || !column?.id || item?.kind !== 'richContent') {
|
|
23368
23522
|
return;
|
|
23369
23523
|
}
|
|
23370
|
-
const ref = this.settingsPanel.
|
|
23524
|
+
const ref = this.settingsPanel.openChild({
|
|
23371
23525
|
id: `layout.richContent.${section.id}.${row.id}.${column.id}.${item.id}`,
|
|
23372
23526
|
title: this.t('visualBlock.editTitle'),
|
|
23373
23527
|
titleIcon: 'notes',
|
|
@@ -30894,24 +31048,45 @@ class PraxisDynamicFormConfigEditor {
|
|
|
30894
31048
|
onLayoutSelect(event) {
|
|
30895
31049
|
if (event && event.type === 'column' && event.openEditor) {
|
|
30896
31050
|
const column = withFormConfigSections(this.editedConfig).sections[event.sectionIndex]?.rows[event.rowIndex]?.columns[event.columnIndex];
|
|
30897
|
-
this.openColumnEditor(column
|
|
31051
|
+
this.openColumnEditor(column, {
|
|
31052
|
+
sectionIndex: event.sectionIndex,
|
|
31053
|
+
rowIndex: event.rowIndex,
|
|
31054
|
+
columnIndex: event.columnIndex,
|
|
31055
|
+
});
|
|
30898
31056
|
}
|
|
30899
31057
|
}
|
|
30900
|
-
openColumnEditor(column) {
|
|
31058
|
+
openColumnEditor(column, location) {
|
|
30901
31059
|
if (!column)
|
|
30902
31060
|
return;
|
|
30903
31061
|
Promise.resolve().then(function () { return columnEditor_component; }).then((m) => {
|
|
30904
31062
|
const { ColumnEditorComponent } = m;
|
|
30905
|
-
const ref = this.settingsPanel.
|
|
31063
|
+
const ref = this.settingsPanel.openChild({
|
|
30906
31064
|
id: `column.${this.formId || 'form'}.${column.id}`,
|
|
30907
31065
|
title: this.tx('config.layout.columnEditor.title', 'Configurar Coluna'),
|
|
30908
31066
|
titleIcon: 'view_column',
|
|
30909
31067
|
content: { component: ColumnEditorComponent, inputs: { column } },
|
|
30910
31068
|
});
|
|
30911
|
-
ref.applied$.pipe(takeUntil(this.destroy$)).subscribe(() => this.
|
|
30912
|
-
ref.saved$.pipe(takeUntil(this.destroy$)).subscribe(() => this.
|
|
31069
|
+
ref.applied$.pipe(takeUntil(this.destroy$)).subscribe((value) => this.applyColumnEditorValue(column, location, value));
|
|
31070
|
+
ref.saved$.pipe(takeUntil(this.destroy$)).subscribe((value) => this.applyColumnEditorValue(column, location, value));
|
|
30913
31071
|
});
|
|
30914
31072
|
}
|
|
31073
|
+
applyColumnEditorValue(column, location, value) {
|
|
31074
|
+
if (!column || !value || typeof value !== 'object')
|
|
31075
|
+
return;
|
|
31076
|
+
const sourceSections = this.editedConfig.sections || [];
|
|
31077
|
+
const targetColumn = sourceSections[location.sectionIndex]?.rows?.[location.rowIndex]
|
|
31078
|
+
?.columns?.[location.columnIndex];
|
|
31079
|
+
if (!targetColumn || targetColumn.id !== column.id)
|
|
31080
|
+
return;
|
|
31081
|
+
const sections = structuredClone(sourceSections);
|
|
31082
|
+
const row = sections[location.sectionIndex].rows[location.rowIndex];
|
|
31083
|
+
row.columns[location.columnIndex] = {
|
|
31084
|
+
...row.columns[location.columnIndex],
|
|
31085
|
+
...structuredClone(value),
|
|
31086
|
+
};
|
|
31087
|
+
this.onConfigChange({ ...this.editedConfig, sections });
|
|
31088
|
+
this.cdr.detectChanges();
|
|
31089
|
+
}
|
|
30915
31090
|
// Cascatas: aplicar patch granular aos campos
|
|
30916
31091
|
onCascadeApply(patch) {
|
|
30917
31092
|
const fields = this.editedConfig.fieldMetadata || [];
|
|
@@ -32903,13 +33078,14 @@ class SectionEditorComponent {
|
|
|
32903
33078
|
});
|
|
32904
33079
|
customActionValue = '__custom__';
|
|
32905
33080
|
destroy$ = new Subject();
|
|
33081
|
+
initialValueSig = '';
|
|
32906
33082
|
titleInput;
|
|
32907
33083
|
descriptionInput;
|
|
32908
33084
|
constructor(fb, data, iconPicker) {
|
|
32909
33085
|
this.fb = fb;
|
|
32910
33086
|
this.data = data;
|
|
32911
33087
|
this.iconPicker = iconPicker;
|
|
32912
|
-
this.section = this.data.section;
|
|
33088
|
+
this.section = structuredClone(this.data.section);
|
|
32913
33089
|
this.fieldMetadata = this.data.fieldMetadata || this.fieldMetadata;
|
|
32914
33090
|
}
|
|
32915
33091
|
ngOnInit() {
|
|
@@ -32958,9 +33134,11 @@ class SectionEditorComponent {
|
|
|
32958
33134
|
headerActions: this.fb.array((this.normalizeHeaderActions(this.section.headerActions) ?? []).map((action) => this.createHeaderActionGroup(action))),
|
|
32959
33135
|
});
|
|
32960
33136
|
this.syncCollapsedControlState(!!this.form.get('collapsible')?.value);
|
|
33137
|
+
this.initialValueSig = JSON.stringify(this.normalizeSectionFormValue(this.form.getRawValue()));
|
|
32961
33138
|
this.form.valueChanges.pipe(takeUntil(this.destroy$)).subscribe(() => {
|
|
32962
33139
|
this.applyNormalizedFormValue();
|
|
32963
|
-
this.isDirty$.next(
|
|
33140
|
+
this.isDirty$.next(JSON.stringify(this.normalizeSectionFormValue(this.form.getRawValue())) !==
|
|
33141
|
+
this.initialValueSig);
|
|
32964
33142
|
this.isValid$.next(this.form.valid);
|
|
32965
33143
|
});
|
|
32966
33144
|
this.form.get('collapsible')?.valueChanges.pipe(takeUntil(this.destroy$)).subscribe((canCollapse) => {
|
|
@@ -32972,6 +33150,9 @@ class SectionEditorComponent {
|
|
|
32972
33150
|
}
|
|
32973
33151
|
ngAfterViewInit() {
|
|
32974
33152
|
setTimeout(() => {
|
|
33153
|
+
if (!this.isDirty$.value) {
|
|
33154
|
+
this.initialValueSig = JSON.stringify(this.normalizeSectionFormValue(this.form.getRawValue()));
|
|
33155
|
+
}
|
|
32975
33156
|
if (this.focusTarget === 'title') {
|
|
32976
33157
|
this.titleInput?.nativeElement?.focus?.();
|
|
32977
33158
|
}
|
|
@@ -33287,17 +33468,14 @@ class SectionEditorComponent {
|
|
|
33287
33468
|
return;
|
|
33288
33469
|
this.form.patchValue({ titleColor: v, descriptionColor: v });
|
|
33289
33470
|
Object.assign(this.section, { titleColor: v, descriptionColor: v });
|
|
33290
|
-
this.isDirty$.next(true);
|
|
33291
33471
|
}
|
|
33292
33472
|
onTitleStyleChange(style) {
|
|
33293
33473
|
this.section.titleStyle = style;
|
|
33294
33474
|
this.form.get('titleStyle')?.setValue(style);
|
|
33295
|
-
this.isDirty$.next(true);
|
|
33296
33475
|
}
|
|
33297
33476
|
onDescriptionStyleChange(style) {
|
|
33298
33477
|
this.section.descriptionStyle = style;
|
|
33299
33478
|
this.form.get('descriptionStyle')?.setValue(style);
|
|
33300
|
-
this.isDirty$.next(true);
|
|
33301
33479
|
}
|
|
33302
33480
|
trackFieldOption(index, field) {
|
|
33303
33481
|
return field.value || String(index);
|
|
@@ -34866,7 +35044,7 @@ class RowEditorComponent {
|
|
|
34866
35044
|
constructor(fb, data) {
|
|
34867
35045
|
this.fb = fb;
|
|
34868
35046
|
this.data = data;
|
|
34869
|
-
this.row = data.row;
|
|
35047
|
+
this.row = structuredClone(data.row);
|
|
34870
35048
|
}
|
|
34871
35049
|
ngOnInit() {
|
|
34872
35050
|
const r = this.data.row;
|
|
@@ -34891,7 +35069,6 @@ class RowEditorComponent {
|
|
|
34891
35069
|
}
|
|
34892
35070
|
catch { }
|
|
34893
35071
|
this.form.valueChanges.pipe(takeUntil(this.destroy$)).subscribe(() => {
|
|
34894
|
-
Object.assign(this.data.row, this.form.value);
|
|
34895
35072
|
let dirty = true;
|
|
34896
35073
|
try {
|
|
34897
35074
|
const nowSig = JSON.stringify(this.form.getRawValue());
|
|
@@ -34907,7 +35084,7 @@ class RowEditorComponent {
|
|
|
34907
35084
|
this.destroy$.complete();
|
|
34908
35085
|
}
|
|
34909
35086
|
getSettingsValue() {
|
|
34910
|
-
return this.form.
|
|
35087
|
+
return { ...this.row, ...this.form.getRawValue() };
|
|
34911
35088
|
}
|
|
34912
35089
|
displayValue(value, fallback) {
|
|
34913
35090
|
if (value === null || value === undefined)
|
|
@@ -35237,11 +35414,12 @@ class ColumnEditorComponent {
|
|
|
35237
35414
|
isBusy$ = new BehaviorSubject(false);
|
|
35238
35415
|
destroy$ = new Subject();
|
|
35239
35416
|
_previewTimer;
|
|
35417
|
+
initialValueSig = '';
|
|
35240
35418
|
i18n = inject(PraxisI18nService);
|
|
35241
35419
|
constructor(fb, data) {
|
|
35242
35420
|
this.fb = fb;
|
|
35243
35421
|
this.data = data;
|
|
35244
|
-
this.column = data.column;
|
|
35422
|
+
this.column = structuredClone(data.column);
|
|
35245
35423
|
}
|
|
35246
35424
|
ngOnInit() {
|
|
35247
35425
|
const col = this.data.column;
|
|
@@ -35279,6 +35457,7 @@ class ColumnEditorComponent {
|
|
|
35279
35457
|
className: [col.className ?? ''],
|
|
35280
35458
|
testId: [col.testId ?? ''],
|
|
35281
35459
|
});
|
|
35460
|
+
this.initialValueSig = JSON.stringify(this.form.getRawValue());
|
|
35282
35461
|
this.form.valueChanges.pipe(takeUntil(this.destroy$)).subscribe(() => {
|
|
35283
35462
|
// Evita NG0100: agendar a mutação para o próximo macrotask,
|
|
35284
35463
|
// fora do ciclo de detecção atual, mantendo o preview ao vivo.
|
|
@@ -35287,7 +35466,7 @@ class ColumnEditorComponent {
|
|
|
35287
35466
|
}
|
|
35288
35467
|
this._previewTimer = setTimeout(() => {
|
|
35289
35468
|
this.applyChanges();
|
|
35290
|
-
this.isDirty$.next(
|
|
35469
|
+
this.isDirty$.next(JSON.stringify(this.form.getRawValue()) !== this.initialValueSig);
|
|
35291
35470
|
this.isValid$.next(this.form.valid);
|
|
35292
35471
|
this._previewTimer = null;
|
|
35293
35472
|
}, 0);
|
|
@@ -35302,10 +35481,10 @@ class ColumnEditorComponent {
|
|
|
35302
35481
|
this.destroy$.complete();
|
|
35303
35482
|
}
|
|
35304
35483
|
applyChanges() {
|
|
35305
|
-
|
|
35484
|
+
this.column = { ...this.column, ...this.form.getRawValue() };
|
|
35306
35485
|
}
|
|
35307
35486
|
getSettingsValue() {
|
|
35308
|
-
return this.form.
|
|
35487
|
+
return { ...this.column, ...this.form.getRawValue() };
|
|
35309
35488
|
}
|
|
35310
35489
|
onBreakpointChange(bp) {
|
|
35311
35490
|
this.previewBreakpoint = bp;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@praxisui/dynamic-form",
|
|
3
|
-
"version": "9.0.
|
|
3
|
+
"version": "9.0.7",
|
|
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.
|
|
13
|
-
"@praxisui/dynamic-fields": "^9.0.
|
|
14
|
-
"@praxisui/metadata-editor": "^9.0.
|
|
15
|
-
"@praxisui/rich-content": "^9.0.
|
|
16
|
-
"@praxisui/settings-panel": "^9.0.
|
|
17
|
-
"@praxisui/visual-builder": "^9.0.
|
|
18
|
-
"@praxisui/core": "^9.0.
|
|
12
|
+
"@praxisui/ai": "^9.0.7",
|
|
13
|
+
"@praxisui/dynamic-fields": "^9.0.7",
|
|
14
|
+
"@praxisui/metadata-editor": "^9.0.7",
|
|
15
|
+
"@praxisui/rich-content": "^9.0.7",
|
|
16
|
+
"@praxisui/settings-panel": "^9.0.7",
|
|
17
|
+
"@praxisui/visual-builder": "^9.0.7",
|
|
18
|
+
"@praxisui/core": "^9.0.7",
|
|
19
19
|
"rxjs": "^7.8.0"
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
@@ -78,13 +78,13 @@ Este documento e a referencia canonica da API JSON de praxis-dynamic-form.
|
|
|
78
78
|
## Backend-owned reactive determinations
|
|
79
79
|
|
|
80
80
|
- `x-ui.reactiveDeterminations` e lido exclusivamente do request schema exato retornado por `/schemas/filtered`; nao integra nem e persistido em `FormConfig`.
|
|
81
|
-
- O runtime aceita somente o contrato fechado normalizado por `normalizeReactiveDeterminations`: trigger `on-change`, scope de create/edit, capability `POST`, no maximo 64 bindings totais, JSON Pointers sem duplicidade ou sobreposicao hierarquica e provenance estrutural.
|
|
81
|
+
- O runtime aceita somente o contrato fechado normalizado por `normalizeReactiveDeterminations`: trigger `on-change`, scope de create/edit, capability `POST`, no maximo 64 bindings totais, JSON Pointers sem duplicidade ou sobreposicao hierarquica e provenance estrutural. A lista e um unico grafo canonico: uma definicao invalida rejeita a projecao inteira, sem execucao parcial.
|
|
82
82
|
- `href`, request/response schemas e bindings sao compilados pelo Praxis Metadata. O Angular nao aceita callback, headers, method, URL ou write policy autorados pelo host/editor/LLM.
|
|
83
83
|
- Cada valor ou status relevante invalida imediatamente a geracao anterior, antes do debounce da proxima chamada; somente a geracao ainda autoritativa pode aplicar a resposta.
|
|
84
84
|
- Hrefs protocol-relative, com separadores codificados ou resolvidos fora da origem configurada da API falham de forma fechada.
|
|
85
85
|
- A resposta so e aplicada quando todos os bindings existem. Os controles alvo aninhados sao preenchidos antes da notificacao dos observers; determinacoes encadeadas recebem um unico sinal logico depois do commit completo, nao um disparo por output. A ordem da cadeia e derivada dos bindings existentes: downstream aguarda a geracao upstream e so executa apos outputs autoritativos ou inputs confirmados como inalterados. Output upstream `null`/`undefined` e geracao upstream com erro, invalida ou cancelada encerram downstream como `skipped`, sem chamada com dado ausente ou obsoleto. Se um controle rejeitar a escrita, o runtime tenta restaurar os anteriores e emite falha terminal. O resultado tem semantica fechada server-derived e substitui o draft alvo; o commit e um batch coordenado no cliente, sem promessa de transacao ACID.
|
|
86
86
|
- O estado permanece pending durante debounce, validacao assincrona e execucao. Uma tentativa de submit aguarda estabilizacao com timeout e repete a espera depois de hooks que podem alterar controles.
|
|
87
|
-
- Controles de output sao desabilitados transitoriamente como valores server-derived, sem mutar/persistir `FormConfig`; seus valores continuam em `getRawValue()`, `valueChange` e no payload de submit.
|
|
87
|
+
- Controles de output sao desabilitados transitoriamente como valores server-derived, sem mutar/persistir `FormConfig`; seus valores continuam em `getRawValue()`, `valueChange` e no payload de submit. Depois da hidratacao, fonte inicialmente preenchida entra em `pending` e e determinada antes de o draft ser considerado estavel; fonte inicial vazia encerra como `skipped/initial-source-empty`, satisfeita e sem HTTP. Alteracao programatica posterior em output owned invalida a autoridade anterior e reexecuta a capability.
|
|
88
88
|
- `reactiveDeterminationExecuted` publica apenas IDs, paths, provenance, estado (`pending`, `success`, `skipped` ou `error`), duracao, correlation ID e motivo. Valores do formulario e da resposta nao participam da evidencia.
|
|
89
89
|
- Hosts compostos recebem estado agregado por `reactiveDeterminationPendingChange`. Antes de navegar ou fechar, devem consultar `hasPendingReactiveDeterminations()` e `hasUnsatisfiedReactiveDeterminations()` e aguardar `waitForReactiveDeterminations(timeoutMs?)`; retorno `false` indica timeout ou geração mais recente sem draft autoritativo e bloqueia a transição. Correlações canceladas recebem terminal `skipped/superseded`.
|
|
90
90
|
- Cascatas de opcoes continuam em `optionSource.dependsOn`; recommendations, side-effect refresh, backend validation e workflow actions permanecem capacidades separadas.
|
|
@@ -380,6 +380,7 @@ declare class ReactiveDeterminationRuntimeService {
|
|
|
380
380
|
private claimTargets;
|
|
381
381
|
private buildInvocation;
|
|
382
382
|
private buildCommit;
|
|
383
|
+
private buildOutputFingerprint;
|
|
383
384
|
private commit;
|
|
384
385
|
private getOrCreateCommittedControls;
|
|
385
386
|
private event;
|
|
@@ -1357,6 +1358,10 @@ declare class PraxisDynamicForm implements OnInit, OnChanges, OnDestroy {
|
|
|
1357
1358
|
private openRowEditor;
|
|
1358
1359
|
private openColumnEditor;
|
|
1359
1360
|
openSectionEditor(section: any, focusTarget?: 'title' | 'description'): void;
|
|
1361
|
+
private applyRowEditorValue;
|
|
1362
|
+
private applyColumnEditorValue;
|
|
1363
|
+
private resolveLayoutEditorLocation;
|
|
1364
|
+
private applyNestedLayoutEditorValue;
|
|
1360
1365
|
private applySectionEditorValue;
|
|
1361
1366
|
private resolveControlTypeEditorial;
|
|
1362
1367
|
private getControlTypeIcon;
|
|
@@ -1788,6 +1793,7 @@ declare class PraxisDynamicFormConfigEditor implements SettingsValueProvider, On
|
|
|
1788
1793
|
ngOnDestroy(): void;
|
|
1789
1794
|
onLayoutSelect(event: any): void;
|
|
1790
1795
|
private openColumnEditor;
|
|
1796
|
+
private applyColumnEditorValue;
|
|
1791
1797
|
onCascadeApply(patch: Record<string, Partial<FieldDefinition>>): void;
|
|
1792
1798
|
private syncCascadeManagerFields;
|
|
1793
1799
|
private stripLegacy;
|
|
@@ -2469,6 +2475,7 @@ declare class SectionEditorComponent implements OnInit, AfterViewInit, OnDestroy
|
|
|
2469
2475
|
readonly globalActionCatalog: ActionCatalogOption[];
|
|
2470
2476
|
readonly customActionValue = "__custom__";
|
|
2471
2477
|
private destroy$;
|
|
2478
|
+
private initialValueSig;
|
|
2472
2479
|
titleInput?: ElementRef<HTMLInputElement>;
|
|
2473
2480
|
descriptionInput?: ElementRef<HTMLTextAreaElement>;
|
|
2474
2481
|
constructor(fb: FormBuilder, data: {
|
|
@@ -2592,6 +2599,7 @@ declare class ColumnEditorComponent implements OnInit, OnDestroy, SettingsValueP
|
|
|
2592
2599
|
isBusy$: BehaviorSubject<boolean>;
|
|
2593
2600
|
private destroy$;
|
|
2594
2601
|
private _previewTimer;
|
|
2602
|
+
private initialValueSig;
|
|
2595
2603
|
private readonly i18n;
|
|
2596
2604
|
constructor(fb: FormBuilder, data: {
|
|
2597
2605
|
column: FormColumn;
|