@praxisui/dynamic-form 10.0.0-rc.18 → 10.0.0-rc.19
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
|
@@ -29,6 +29,14 @@ Peer dependencies:
|
|
|
29
29
|
- `@praxisui/ai`, `@praxisui/core`, `@praxisui/dynamic-fields`, `@praxisui/metadata-editor`, `@praxisui/rich-content`, `@praxisui/settings-panel`, `@praxisui/visual-builder` `^9.0.0-beta.12`
|
|
30
30
|
- `rxjs` `^7.8.0`
|
|
31
31
|
|
|
32
|
+
## Original item revision on submit
|
|
33
|
+
|
|
34
|
+
In edit mode, the form keeps a single strong `ETag` together with the item representation returned by its hydration GET (`readUrl`, custom `getById` endpoint or ordinary resource GET). Automatic PUT/PATCH sends that exact tag as `If-Match`; it never performs another GET during submit to acquire a newer revision. A successful response that contains the corresponding item representation replaces both cached data and revision from that same response, including server normalization. A command acknowledgment or incomplete representation cannot rotate the original item snapshot. A shared CRUD cache read by another consumer cannot replace the form's original revision.
|
|
35
|
+
|
|
36
|
+
The snapshot is transient and belongs to the resource identity, read/write target and API context. Preparation, reads and writes share a generation: switching away and back, reloading or destroying the surface cannot make an old callback current again. Obsolete automatic responses do not emit submit result events, run result hooks, publish field errors or reset a newer submit's loading state. If an obsolete preparation hook retained the reused form group, the draft requires explicit reload before another write. Changing those bindings requires hydration again. Missing or weak read ETags are not converted into versions; unversioned endpoints remain usable, while a versioned host can reject a missing precondition with `428`. After `412`/`428`, or a successful versioned write without its next ETag, automatic submission requires an explicit reload. Closing and reopening a surface or retrying its initialization loads the new representation; retrying submit alone cannot silently refresh the revision.
|
|
37
|
+
|
|
38
|
+
Explicit `submitResourceVersion` and the existing cross-resource version target inputs retain their command semantics and precedence. They remain host-owned inputs; same-resource editing needs no additional authoring fields. Backend authorization, concurrency and lifecycle policy remain authoritative.
|
|
39
|
+
|
|
32
40
|
## Reactive backend determinations
|
|
33
41
|
|
|
34
42
|
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.
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": "1.0.0",
|
|
3
|
-
"generatedAt": "2026-10-
|
|
3
|
+
"generatedAt": "2026-10-15T00:20:34.117Z",
|
|
4
4
|
"packageName": "@praxisui/dynamic-form",
|
|
5
|
-
"packageVersion": "10.0.0-rc.
|
|
5
|
+
"packageVersion": "10.0.0-rc.19",
|
|
6
6
|
"sourceRegistry": "praxis-component-registry-ingestion",
|
|
7
7
|
"sourceRegistryVersion": "1.0.0",
|
|
8
8
|
"componentCount": 2,
|
|
@@ -3,7 +3,7 @@ import { Injectable, EventEmitter, inject, HostListener, Output, Input, Componen
|
|
|
3
3
|
import * as i1$2 from '@angular/common';
|
|
4
4
|
import { CommonModule } from '@angular/common';
|
|
5
5
|
import * as i2 from '@angular/common/http';
|
|
6
|
-
import { HttpHeaders, HttpContext } from '@angular/common/http';
|
|
6
|
+
import { HttpHeaders, HttpErrorResponse, HttpContext } from '@angular/common/http';
|
|
7
7
|
import * as i1$3 from '@angular/forms';
|
|
8
8
|
import { ReactiveFormsModule, FormsModule, FormGroup, Validators } from '@angular/forms';
|
|
9
9
|
import * as i4 from '@angular/material/icon';
|
|
@@ -23,7 +23,7 @@ import { MatBadgeModule } from '@angular/material/badge';
|
|
|
23
23
|
import { map, Subscription, merge, of, timer, NEVER, EMPTY, defer, TimeoutError, Subject, firstValueFrom, BehaviorSubject, throwError, combineLatest, Observable, race, from, 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
|
-
import { serializeEntityLookupValueForPayload, PraxisI18nService, PraxisIconButtonComponent, PraxisIconDirective, providePraxisI18nConfig, RULE_PROPERTY_SCHEMA, FIELD_METADATA_CAPABILITIES, migrateFormLayoutRule, PraxisJsonLogicService, normalizeFormLayoutItems, deepMerge, createDefaultFormConfig, normalizeFormConfig as normalizeFormConfig$1, ensureIds, createEmptyRichContentDocument, formIdentityIssues, resolveHidden, resolveOrder, resolveSpan, projectGroupedCommandPartialRows, isPraxisRuntimeGlobalActionEffect, ASYNC_CONFIG_STORAGE, withFormConfigSections, createFieldLayoutItem, getFormLayoutFieldNames, LoggerService, createCorporateLoggerConfig, ConsoleLoggerSink, ResourceDiscoveryService, GlobalActionService, SCHEMA_METADATA_CLIENT_OPTIONS, ResourceQuickConnectComponent, composeHeadersWithVersion, buildApiUrl, mapFieldDefinitionsToMetadata, reconcileFormConfig, applyLocalFormCustomizations, materializeFormLayoutFromMetadata, syncWithServerMetadata, PRAXIS_LOADING_CTX, evaluateFieldAccess, normalizeControlTypeKey, normalizePraxisEffectPolicy, buildPraxisEffectDistinctKey, getFormColumnFieldNames, resolveGroupedCommandPartialRowSpans, resolveOffset, buildSchemaIdStorageKeySegment, buildSchemaId, fetchWithETag, resolveControlTypeAlias, getTextTransformer, MemoryCacheAdapter, LocalStorageCacheAdapter, SchemaMetadataClient, DEFAULT_SCHEMA_REQUEST_TIMEOUT_MS, 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, IconPickerService, validateGlobalActionRefs, normalizeFormMetadata, DynamicFormService, projectComponentAuthoringManifest } from '@praxisui/core';
|
|
26
|
+
import { serializeEntityLookupValueForPayload, PraxisI18nService, PraxisIconButtonComponent, PraxisIconDirective, providePraxisI18nConfig, RULE_PROPERTY_SCHEMA, FIELD_METADATA_CAPABILITIES, migrateFormLayoutRule, PraxisJsonLogicService, normalizeFormLayoutItems, deepMerge, createDefaultFormConfig, normalizeFormConfig as normalizeFormConfig$1, ensureIds, createEmptyRichContentDocument, formIdentityIssues, resolveHidden, resolveOrder, resolveSpan, projectGroupedCommandPartialRows, isPraxisRuntimeGlobalActionEffect, ASYNC_CONFIG_STORAGE, withFormConfigSections, createFieldLayoutItem, getFormLayoutFieldNames, LoggerService, createCorporateLoggerConfig, ConsoleLoggerSink, ResourceDiscoveryService, GlobalActionService, SCHEMA_METADATA_CLIENT_OPTIONS, ResourceQuickConnectComponent, composeHeadersWithVersion, buildApiUrl, readStrongResourceEtag, ApiEndpoint, mapFieldDefinitionsToMetadata, reconcileFormConfig, applyLocalFormCustomizations, materializeFormLayoutFromMetadata, syncWithServerMetadata, PRAXIS_LOADING_CTX, evaluateFieldAccess, normalizeControlTypeKey, normalizePraxisEffectPolicy, buildPraxisEffectDistinctKey, getFormColumnFieldNames, resolveGroupedCommandPartialRowSpans, resolveOffset, buildSchemaIdStorageKeySegment, buildSchemaId, fetchWithETag, resolveControlTypeAlias, getTextTransformer, MemoryCacheAdapter, LocalStorageCacheAdapter, SchemaMetadataClient, DEFAULT_SCHEMA_REQUEST_TIMEOUT_MS, 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, IconPickerService, validateGlobalActionRefs, normalizeFormMetadata, DynamicFormService, projectComponentAuthoringManifest } from '@praxisui/core';
|
|
27
27
|
import * as i1$1 from '@praxisui/dynamic-fields';
|
|
28
28
|
import { getControlTypeCatalog, SelectOptionRegistry, ConfirmDialogComponent, DynamicFieldLoaderDirective } from '@praxisui/dynamic-fields';
|
|
29
29
|
import { BaseAiAdapter, AgenticAuthoringTurnClientService, PraxisAssistantSessionRegistryService, PraxisAssistantTurnOrchestratorService, createPraxisAssistantViewportLayout, PraxisAiAssistantShellComponent } from '@praxisui/ai';
|
|
@@ -114,6 +114,7 @@ const PRAXIS_DYNAMIC_FORM_I18N_CONFIG = {
|
|
|
114
114
|
'actions.invalidRequired': 'Preencha os campos obrigatórios para continuar.',
|
|
115
115
|
'actions.invalidRequiredWithFields': 'Preencha: {{fields}}.',
|
|
116
116
|
'actions.submitErrorTitle': 'Erro ao salvar',
|
|
117
|
+
'runtime.resourceVersion.reload': 'Recarregue os dados antes de salvar novamente.',
|
|
117
118
|
'runtime.reactiveDetermination.error': 'Não foi possível atualizar os campos relacionados. Revise os dados ou tente novamente.',
|
|
118
119
|
'runtime.reactiveDetermination.submitPending': 'Aguarde a atualização dos campos relacionados antes de salvar.',
|
|
119
120
|
'runtime.reactiveDetermination.submitTimeout': 'Os campos relacionados não estabilizaram a tempo. Revise os dados e tente salvar novamente.',
|
|
@@ -704,6 +705,7 @@ const PRAXIS_DYNAMIC_FORM_I18N_CONFIG = {
|
|
|
704
705
|
'actions.invalidRequired': 'Complete the required fields to continue.',
|
|
705
706
|
'actions.invalidRequiredWithFields': 'Complete: {{fields}}.',
|
|
706
707
|
'actions.submitErrorTitle': 'Save error',
|
|
708
|
+
'runtime.resourceVersion.reload': 'Reload the data before saving again.',
|
|
707
709
|
'config.tabs.mobileLabel': 'Editor section',
|
|
708
710
|
'config.tabs.general': 'General',
|
|
709
711
|
'config.tabs.schema': 'Schema Verification',
|
|
@@ -11442,6 +11444,8 @@ class PraxisDynamicFormDestroyedError extends Error {
|
|
|
11442
11444
|
this.name = 'PraxisDynamicFormDestroyedError';
|
|
11443
11445
|
}
|
|
11444
11446
|
}
|
|
11447
|
+
class PraxisDynamicFormResourceRevisionReloadError extends Error {
|
|
11448
|
+
}
|
|
11445
11449
|
const FORM_CONFIG_LOAD_TIMEOUT_MS = 1200;
|
|
11446
11450
|
const FORM_INPUT_PREFS_LOAD_TIMEOUT_MS = 1200;
|
|
11447
11451
|
const SCHEMA_GLOBAL_CONFIG_READY_TIMEOUT_MS = 1200;
|
|
@@ -11931,10 +11935,13 @@ class PraxisDynamicForm {
|
|
|
11931
11935
|
return this._customEndpoints;
|
|
11932
11936
|
}
|
|
11933
11937
|
set customEndpoints(value) {
|
|
11934
|
-
|
|
11935
|
-
|
|
11936
|
-
|
|
11938
|
+
// This input is a replacement snapshot. CRUD's public configureEndpoints remains a merge API.
|
|
11939
|
+
const removed = {};
|
|
11940
|
+
for (const key of Object.keys(this._customEndpoints)) {
|
|
11941
|
+
removed[key] = undefined;
|
|
11937
11942
|
}
|
|
11943
|
+
this._customEndpoints = value ?? {};
|
|
11944
|
+
this.crud.configureEndpoints({ ...removed, ...this._customEndpoints });
|
|
11938
11945
|
}
|
|
11939
11946
|
formSubmit = new EventEmitter();
|
|
11940
11947
|
formCancel = new EventEmitter();
|
|
@@ -11971,6 +11978,9 @@ class PraxisDynamicForm {
|
|
|
11971
11978
|
isLoading = false;
|
|
11972
11979
|
submitting = false;
|
|
11973
11980
|
submitPreparationInProgress = false;
|
|
11981
|
+
submitLifecycleSequence = 0;
|
|
11982
|
+
activeSubmitLifecycle = null;
|
|
11983
|
+
activeSubmitAutomatic = false;
|
|
11974
11984
|
submitError = null;
|
|
11975
11985
|
initializationStatus = 'idle';
|
|
11976
11986
|
currentErrorMessage = '';
|
|
@@ -12023,8 +12033,9 @@ class PraxisDynamicForm {
|
|
|
12023
12033
|
entitySelectionCleared = false;
|
|
12024
12034
|
hydratedEntityId = null;
|
|
12025
12035
|
loadedEntityData = null;
|
|
12026
|
-
|
|
12027
|
-
|
|
12036
|
+
loadedResourceRevision = null;
|
|
12037
|
+
resourceRevisionInvalidated = false;
|
|
12038
|
+
entityReadAttempt = 0;
|
|
12028
12039
|
entityHydrationPending = false;
|
|
12029
12040
|
entityReadFailed = false;
|
|
12030
12041
|
entityReadText(key) {
|
|
@@ -12552,16 +12563,25 @@ class PraxisDynamicForm {
|
|
|
12552
12563
|
const method = this.normalizeSubmitMethod(this.submitMethod);
|
|
12553
12564
|
const configuredSubmitUrl = String(this.submitUrl || '').trim();
|
|
12554
12565
|
if (!method && !configuredSubmitUrl) {
|
|
12555
|
-
|
|
12556
|
-
|
|
12557
|
-
|
|
12566
|
+
if (operation !== 'update')
|
|
12567
|
+
return this.crud.create(formData);
|
|
12568
|
+
const binding = this.resourceRevisionBinding();
|
|
12569
|
+
const revisionEpoch = this.entityReadAttempt;
|
|
12570
|
+
if (this.resourceRevisionNeedsReload(binding))
|
|
12571
|
+
return this.resourceRevisionReloadError();
|
|
12572
|
+
const originalVersion = this.loadedResourceRevision?.etag ?? null;
|
|
12573
|
+
return this.crud.updateHttpResponse(this.resourceId, formData, { resourceVersion: originalVersion }).pipe(filter(() => this.isResourceRevisionCurrent(binding, revisionEpoch)), tap((response) => this.captureSavedResourceRevision(response, binding, originalVersion != null, revisionEpoch)), map$1((response) => this.unwrapRestApiResponse(response.body)), catchError((error) => this.isResourceRevisionCurrent(binding, revisionEpoch)
|
|
12574
|
+
? this.rejectResourceRevision(error, binding, revisionEpoch) : EMPTY));
|
|
12558
12575
|
}
|
|
12559
12576
|
if (!method || !configuredSubmitUrl) {
|
|
12560
12577
|
return throwError(() => new Error('PraxisDynamicForm requires submitMethod and submitUrl together for canonical submit execution.'));
|
|
12561
12578
|
}
|
|
12562
12579
|
const resolvedUrl = this.resolveSubmitHref(configuredSubmitUrl);
|
|
12580
|
+
const automaticItemRevision = this.usesAutomaticItemRevision();
|
|
12581
|
+
const binding = this.resourceRevisionBinding();
|
|
12582
|
+
const revisionEpoch = this.entityReadAttempt;
|
|
12563
12583
|
return this.resolveSubmitResourceVersion(formData).pipe(switchMap((resolvedResourceVersion) => {
|
|
12564
|
-
let headers = composeHeadersWithVersion(this.resolveApiEntry()) ?? new HttpHeaders();
|
|
12584
|
+
let headers = (composeHeadersWithVersion(this.resolveApiEntry()) ?? new HttpHeaders()).delete('If-Match');
|
|
12565
12585
|
const idempotencyKey = String(this.submitIdempotencyKey || '').trim();
|
|
12566
12586
|
if (idempotencyKey) {
|
|
12567
12587
|
headers = headers.set('Idempotency-Key', idempotencyKey);
|
|
@@ -12573,28 +12593,15 @@ class PraxisDynamicForm {
|
|
|
12573
12593
|
if (resolvedResourceVersion) {
|
|
12574
12594
|
headers = headers.set('If-Match', this.formatResourceVersion(resolvedResourceVersion));
|
|
12575
12595
|
}
|
|
12576
|
-
|
|
12577
|
-
|
|
12578
|
-
|
|
12579
|
-
|
|
12580
|
-
|
|
12581
|
-
|
|
12582
|
-
|
|
12583
|
-
|
|
12584
|
-
|
|
12585
|
-
return this.http
|
|
12586
|
-
.put(resolvedUrl, formData, {
|
|
12587
|
-
headers,
|
|
12588
|
-
observe: 'response',
|
|
12589
|
-
})
|
|
12590
|
-
.pipe(map$1((response) => this.unwrapAndCaptureSubmitResponse(response, true)), catchError((err) => this.handleVersionedSubmitError(err)));
|
|
12591
|
-
}
|
|
12592
|
-
return this.http
|
|
12593
|
-
.post(resolvedUrl, formData, {
|
|
12594
|
-
headers,
|
|
12595
|
-
observe: 'response',
|
|
12596
|
-
})
|
|
12597
|
-
.pipe(map$1((response) => this.unwrapAndCaptureSubmitResponse(response, this.mode === 'edit' || this.resourceId != null)), catchError((err) => this.handleVersionedSubmitError(err)));
|
|
12596
|
+
return this.http.request(method.toUpperCase(), resolvedUrl, {
|
|
12597
|
+
body: formData, headers, observe: 'response',
|
|
12598
|
+
}).pipe(filter(() => !automaticItemRevision || this.isResourceRevisionCurrent(binding, revisionEpoch)), tap((response) => {
|
|
12599
|
+
if (automaticItemRevision)
|
|
12600
|
+
this.captureSavedResourceRevision(response, binding, resolvedResourceVersion != null, revisionEpoch);
|
|
12601
|
+
}), map$1((response) => this.unwrapRestApiResponse(response.body)), catchError((error) => automaticItemRevision
|
|
12602
|
+
? this.isResourceRevisionCurrent(binding, revisionEpoch)
|
|
12603
|
+
? this.rejectResourceRevision(error, binding, revisionEpoch) : EMPTY
|
|
12604
|
+
: throwError(() => error)));
|
|
12598
12605
|
}));
|
|
12599
12606
|
}
|
|
12600
12607
|
resolveSubmitResourceVersion(formData) {
|
|
@@ -12605,7 +12612,13 @@ class PraxisDynamicForm {
|
|
|
12605
12612
|
const targetResourceKey = String(this.submitResourceVersionTargetResourceKey ?? '').trim();
|
|
12606
12613
|
const targetIdField = String(this.submitResourceVersionTargetIdField ?? '').trim();
|
|
12607
12614
|
if (!targetResourceKey && !targetIdField) {
|
|
12608
|
-
|
|
12615
|
+
const method = this.normalizeSubmitMethod(this.submitMethod);
|
|
12616
|
+
if (method !== 'put' && method !== 'patch')
|
|
12617
|
+
return of(null);
|
|
12618
|
+
const binding = this.resourceRevisionBinding();
|
|
12619
|
+
if (this.resourceRevisionNeedsReload(binding))
|
|
12620
|
+
return this.resourceRevisionReloadError();
|
|
12621
|
+
return of(this.loadedResourceRevision?.etag ?? null);
|
|
12609
12622
|
}
|
|
12610
12623
|
if (!targetResourceKey || !targetIdField) {
|
|
12611
12624
|
return throwError(() => new Error('PraxisDynamicForm requires both submitResourceVersionTargetResourceKey and submitResourceVersionTargetIdField.'));
|
|
@@ -12653,91 +12666,91 @@ class PraxisDynamicForm {
|
|
|
12653
12666
|
: throwError(() => new Error(`PraxisDynamicForm did not receive an ETag for cross-resource target "${targetResourceKey}".`));
|
|
12654
12667
|
}));
|
|
12655
12668
|
}
|
|
12656
|
-
|
|
12657
|
-
|
|
12658
|
-
const
|
|
12659
|
-
|
|
12660
|
-
|
|
12661
|
-
|
|
12662
|
-
|
|
12663
|
-
|
|
12664
|
-
|
|
12665
|
-
: null
|
|
12666
|
-
|
|
12667
|
-
|
|
12668
|
-
|
|
12669
|
-
|
|
12670
|
-
|
|
12671
|
-
|
|
12672
|
-
|
|
12673
|
-
|
|
12674
|
-
null
|
|
12675
|
-
|
|
12676
|
-
|
|
12677
|
-
|
|
12678
|
-
|
|
12679
|
-
|
|
12680
|
-
'headers' in response;
|
|
12681
|
-
const etagHeader = isHttpResponse
|
|
12682
|
-
? this.normalizeResourceVersionToken(response.headers?.get?.('ETag'))
|
|
12683
|
-
: null;
|
|
12684
|
-
const unwrapped = this.unwrapRestApiResponse(isHttpResponse ? response.body : response);
|
|
12685
|
-
this.captureUpdatedEntityResourceVersion(unwrapped, etagHeader, isEntityMutation);
|
|
12686
|
-
return unwrapped;
|
|
12669
|
+
/** Identity and server context are captured with the representation, never reconstructed at submit by GET. */
|
|
12670
|
+
resourceRevisionBinding() {
|
|
12671
|
+
const entry = this.resolveApiEntry();
|
|
12672
|
+
const headers = composeHeadersWithVersion(entry);
|
|
12673
|
+
const context = this.enterpriseRuntimeContext?.snapshot;
|
|
12674
|
+
return JSON.stringify({
|
|
12675
|
+
id: this.resourceId ?? this.pendingEntityId ?? null,
|
|
12676
|
+
resourcePath: this.resourcePath ?? null,
|
|
12677
|
+
readUrl: this.readUrl ?? null,
|
|
12678
|
+
customEndpoints: this.customEndpoints ?? null,
|
|
12679
|
+
submitUrl: this.submitUrl ?? null,
|
|
12680
|
+
submitMethod: this.normalizeSubmitMethod(this.submitMethod),
|
|
12681
|
+
origin: buildApiUrl(entry),
|
|
12682
|
+
headers: headers?.keys().sort().map((name) => [name, headers?.getAll(name)]),
|
|
12683
|
+
user: context?.user.userId ?? null,
|
|
12684
|
+
state: context?.state ?? null,
|
|
12685
|
+
selectionVersion: context?.selectionVersion ?? null,
|
|
12686
|
+
contextVersion: context?.effectiveContext?.contextVersion ?? null,
|
|
12687
|
+
organization: context?.effectiveContext?.activeOrganizationId ?? null,
|
|
12688
|
+
tenant: context?.effectiveContext?.activeTenant.tenantId ?? null,
|
|
12689
|
+
environment: context?.effectiveContext?.environment ?? null,
|
|
12690
|
+
profile: context?.effectiveContext?.activeProfileId ?? null,
|
|
12691
|
+
module: context?.effectiveContext?.activeModuleKey ?? null,
|
|
12692
|
+
});
|
|
12687
12693
|
}
|
|
12688
|
-
|
|
12689
|
-
|
|
12690
|
-
|
|
12691
|
-
|
|
12692
|
-
|
|
12693
|
-
|
|
12694
|
-
|
|
12695
|
-
|
|
12696
|
-
|
|
12697
|
-
|
|
12698
|
-
|
|
12699
|
-
(isEntityMutation && entityId != null
|
|
12700
|
-
? this.normalizeResourceVersionToken(this.crud?.getResourceVersion?.(entityId))
|
|
12701
|
-
: null);
|
|
12702
|
-
if (!nextVersion) {
|
|
12694
|
+
usesAutomaticItemRevision() {
|
|
12695
|
+
return this.mode !== 'create' && this.resolveSubmitOperation() !== 'create'
|
|
12696
|
+
&& !String(this.submitResourceVersion ?? '').trim()
|
|
12697
|
+
&& !String(this.submitResourceVersionTargetResourceKey ?? '').trim()
|
|
12698
|
+
&& !String(this.submitResourceVersionTargetIdField ?? '').trim();
|
|
12699
|
+
}
|
|
12700
|
+
isResourceRevisionCurrent(binding, epoch) {
|
|
12701
|
+
return !this.isDestroyed && epoch === this.entityReadAttempt && binding === this.resourceRevisionBinding();
|
|
12702
|
+
}
|
|
12703
|
+
invalidateAutomaticSubmitUi() {
|
|
12704
|
+
if (!this.activeSubmitAutomatic)
|
|
12703
12705
|
return;
|
|
12706
|
+
this.activeSubmitLifecycle = null;
|
|
12707
|
+
this.activeSubmitAutomatic = false;
|
|
12708
|
+
this.submitting = false;
|
|
12709
|
+
this.submitPreparationInProgress = false;
|
|
12710
|
+
try {
|
|
12711
|
+
this._activeSnack?.dismiss();
|
|
12704
12712
|
}
|
|
12705
|
-
|
|
12706
|
-
|
|
12707
|
-
|
|
12708
|
-
|
|
12709
|
-
|
|
12710
|
-
|
|
12711
|
-
|
|
12712
|
-
|
|
12713
|
-
|
|
12714
|
-
|
|
12715
|
-
|
|
12716
|
-
|
|
12717
|
-
|
|
12718
|
-
|
|
12719
|
-
|
|
12720
|
-
|
|
12721
|
-
|
|
12722
|
-
|
|
12723
|
-
if (
|
|
12724
|
-
|
|
12713
|
+
catch { }
|
|
12714
|
+
this._activeSnack = null;
|
|
12715
|
+
}
|
|
12716
|
+
resourceRevisionNeedsReload(binding) {
|
|
12717
|
+
return this.resourceRevisionInvalidated
|
|
12718
|
+
|| this.loadedResourceRevision != null && this.loadedResourceRevision.binding !== binding;
|
|
12719
|
+
}
|
|
12720
|
+
resourceRevisionReloadError() {
|
|
12721
|
+
return throwError(() => new PraxisDynamicFormResourceRevisionReloadError(this.i18n.t('runtime.resourceVersion.reload', undefined, 'Recarregue os dados antes de salvar novamente.', PRAXIS_DYNAMIC_FORM_I18N_NAMESPACE)));
|
|
12722
|
+
}
|
|
12723
|
+
captureSavedResourceRevision(response, binding, required, epoch) {
|
|
12724
|
+
if (epoch !== this.entityReadAttempt || binding !== this.resourceRevisionBinding())
|
|
12725
|
+
return;
|
|
12726
|
+
const data = this.unwrapRestApiResponse(response.body);
|
|
12727
|
+
const representsOriginal = data != null && typeof data === 'object' && !Array.isArray(data)
|
|
12728
|
+
&& Object.keys(this.loadedEntityData ?? {}).every((key) => Object.prototype.hasOwnProperty.call(data, key))
|
|
12729
|
+
&& (!Object.prototype.hasOwnProperty.call(data, 'id') || this.resourceId == null
|
|
12730
|
+
|| String(data['id']) === String(this.resourceId));
|
|
12731
|
+
if (!representsOriginal) {
|
|
12732
|
+
// A command acknowledgment/partial response is not a new item representation.
|
|
12733
|
+
this.loadedResourceRevision = null;
|
|
12734
|
+
this.loadedEntityId = null;
|
|
12735
|
+
this.loadedEntityData = null;
|
|
12736
|
+
this.resourceRevisionInvalidated = required;
|
|
12737
|
+
return;
|
|
12725
12738
|
}
|
|
12739
|
+
const normalized = this.normalizeFormPatchValue(data);
|
|
12740
|
+
this.loadedEntityData = normalized;
|
|
12741
|
+
this.loadedEntityId = this.resourceId ?? this.pendingEntityId;
|
|
12742
|
+
this.form.patchValue(normalized);
|
|
12743
|
+
const etag = readStrongResourceEtag(response.headers.get('ETag'));
|
|
12744
|
+
this.loadedResourceRevision = { binding, etag };
|
|
12745
|
+
this.resourceRevisionInvalidated = required && etag == null;
|
|
12726
12746
|
}
|
|
12727
|
-
|
|
12728
|
-
if (
|
|
12729
|
-
|
|
12730
|
-
|
|
12731
|
-
|
|
12732
|
-
this.loadedEntityResourceVersion = null;
|
|
12733
|
-
this.submitResourceVersion = null;
|
|
12734
|
-
const entityId = this.resourceId ?? this.pendingEntityId ?? this.loadedEntityId ?? null;
|
|
12735
|
-
if (entityId != null) {
|
|
12736
|
-
this.crud?.setResourceVersion?.(entityId, null);
|
|
12737
|
-
}
|
|
12738
|
-
}
|
|
12747
|
+
rejectResourceRevision(error, binding, epoch) {
|
|
12748
|
+
if (error instanceof HttpErrorResponse && (error.status === 412 || error.status === 428)
|
|
12749
|
+
&& epoch === this.entityReadAttempt && binding === this.resourceRevisionBinding()) {
|
|
12750
|
+
this.loadedResourceRevision = null;
|
|
12751
|
+
this.resourceRevisionInvalidated = true;
|
|
12739
12752
|
}
|
|
12740
|
-
return throwError(() =>
|
|
12753
|
+
return throwError(() => error);
|
|
12741
12754
|
}
|
|
12742
12755
|
normalizeResourceVersionToken(raw) {
|
|
12743
12756
|
if (typeof raw !== 'string' && typeof raw !== 'number') {
|
|
@@ -12846,7 +12859,7 @@ class PraxisDynamicForm {
|
|
|
12846
12859
|
return;
|
|
12847
12860
|
}
|
|
12848
12861
|
this.crud.configure(this.resourcePath, {
|
|
12849
|
-
endpointKey: this.apiEndpointKey
|
|
12862
|
+
endpointKey: this.apiEndpointKey || ApiEndpoint.Default,
|
|
12850
12863
|
apiUrlEntry: this.apiUrlEntry ?? null,
|
|
12851
12864
|
});
|
|
12852
12865
|
}
|
|
@@ -13024,19 +13037,29 @@ class PraxisDynamicForm {
|
|
|
13024
13037
|
}
|
|
13025
13038
|
this.retryInitialization();
|
|
13026
13039
|
}
|
|
13027
|
-
|
|
13028
|
-
|
|
13029
|
-
|
|
13030
|
-
|
|
13031
|
-
|
|
13032
|
-
|
|
13033
|
-
|
|
13034
|
-
|
|
13035
|
-
}
|
|
13036
|
-
this.ensureActiveTabValid();
|
|
13040
|
+
resourceRevisionContextScope() {
|
|
13041
|
+
const context = this.enterpriseRuntimeContext?.snapshot;
|
|
13042
|
+
return JSON.stringify([context?.state, context?.selectionVersion, context?.effectiveContext?.contextVersion,
|
|
13043
|
+
context?.effectiveContext?.activeOrganizationId, context?.user.userId, context?.effectiveContext?.activeTenant.tenantId,
|
|
13044
|
+
context?.effectiveContext?.environment, context?.effectiveContext?.activeProfileId, context?.effectiveContext?.activeModuleKey]);
|
|
13045
|
+
}
|
|
13046
|
+
subscribeToRuntimeContextChanges() {
|
|
13047
|
+
let lastScope = this.resourceRevisionContextScope();
|
|
13037
13048
|
this.enterpriseRuntimeContext?.contextChanges$
|
|
13038
13049
|
.pipe(takeUntil(this.destroy$), skip(1))
|
|
13039
13050
|
.subscribe(() => {
|
|
13051
|
+
const scope = this.resourceRevisionContextScope();
|
|
13052
|
+
if (scope !== lastScope) {
|
|
13053
|
+
lastScope = scope;
|
|
13054
|
+
this.invalidateAutomaticSubmitUi();
|
|
13055
|
+
this.entityReadAttempt++;
|
|
13056
|
+
this.loadedResourceRevision = null;
|
|
13057
|
+
this.resourceRevisionInvalidated = this.mode !== 'create';
|
|
13058
|
+
this.loadedEntityId = null;
|
|
13059
|
+
this.loadedEntityData = null;
|
|
13060
|
+
this.hydratedEntityId = null;
|
|
13061
|
+
this.entityHydrationPending = false;
|
|
13062
|
+
}
|
|
13040
13063
|
this.runtimeFieldMetadata = null;
|
|
13041
13064
|
if (this.hasBuiltFormFromConfig && this.hasRenderableConfig()) {
|
|
13042
13065
|
this.buildFormFromConfig({
|
|
@@ -13048,6 +13071,18 @@ class PraxisDynamicForm {
|
|
|
13048
13071
|
this.cdr.markForCheck();
|
|
13049
13072
|
}
|
|
13050
13073
|
});
|
|
13074
|
+
}
|
|
13075
|
+
ngOnInit() {
|
|
13076
|
+
this.debugLog('[PDF] ngOnInit gating', {
|
|
13077
|
+
formId: this.formId,
|
|
13078
|
+
resourcePath: this.resourcePath,
|
|
13079
|
+
});
|
|
13080
|
+
this.inputsLoaded = !this.formId;
|
|
13081
|
+
if (!this.formId && !this.resourcePath && this.hasRenderableConfig()) {
|
|
13082
|
+
this.initializeStandaloneConfig();
|
|
13083
|
+
}
|
|
13084
|
+
this.ensureActiveTabValid();
|
|
13085
|
+
this.subscribeToRuntimeContextChanges();
|
|
13051
13086
|
// Load persisted connection if not provided via input
|
|
13052
13087
|
const connKey = this.connectionKey();
|
|
13053
13088
|
if (!this.resourcePath && connKey) {
|
|
@@ -13163,6 +13198,18 @@ class PraxisDynamicForm {
|
|
|
13163
13198
|
}
|
|
13164
13199
|
}
|
|
13165
13200
|
ngOnChanges(changes) {
|
|
13201
|
+
const revisionIdentityChanged = ['resourceId', 'resourcePath', 'readUrl', 'customEndpoints',
|
|
13202
|
+
'apiEndpointKey', 'apiUrlEntry', 'submitUrl', 'submitMethod', 'mode']
|
|
13203
|
+
.some((key) => changes[key] && !changes[key].firstChange);
|
|
13204
|
+
if (revisionIdentityChanged) {
|
|
13205
|
+
this.invalidateAutomaticSubmitUi();
|
|
13206
|
+
this.entityReadAttempt++;
|
|
13207
|
+
this.loadedResourceRevision = null;
|
|
13208
|
+
this.resourceRevisionInvalidated = this.mode !== 'create';
|
|
13209
|
+
this.loadedEntityId = null;
|
|
13210
|
+
this.loadedEntityData = null;
|
|
13211
|
+
this.hydratedEntityId = null;
|
|
13212
|
+
}
|
|
13166
13213
|
// Assert invalid combos: presentationModeGlobal=true is only effective in view mode
|
|
13167
13214
|
if ((changes['mode'] || changes['presentationModeGlobal']) &&
|
|
13168
13215
|
this.mode !== 'view' &&
|
|
@@ -13180,12 +13227,12 @@ class PraxisDynamicForm {
|
|
|
13180
13227
|
this.canvasState.setSelectedElement(null);
|
|
13181
13228
|
this.cdr.detectChanges();
|
|
13182
13229
|
}
|
|
13183
|
-
if (changes['resourcePath'] && this.resourcePath) {
|
|
13230
|
+
if ((changes['resourcePath'] || changes['apiEndpointKey'] || changes['apiUrlEntry'] || changes['customEndpoints']) && this.resourcePath) {
|
|
13184
13231
|
this.debugLog('[PDF] ngOnChanges resourcePath=', this.resourcePath, ' -> configure()');
|
|
13185
13232
|
this.saveConnection('input');
|
|
13186
13233
|
this.configureCrudRuntime();
|
|
13187
13234
|
// A new resource supersedes schema work for the previous resource.
|
|
13188
|
-
if (!changes['resourcePath'].firstChange && this.formId) {
|
|
13235
|
+
if (changes['resourcePath'] && !changes['resourcePath'].firstChange && this.formId) {
|
|
13189
13236
|
this.retryInitialization();
|
|
13190
13237
|
}
|
|
13191
13238
|
else if (!this.isInitialized && !this.isLoading && this.formId) {
|
|
@@ -13211,8 +13258,6 @@ class PraxisDynamicForm {
|
|
|
13211
13258
|
this.entityReadCancel$.next();
|
|
13212
13259
|
this.loadedEntityId = null;
|
|
13213
13260
|
this.loadedEntityData = null;
|
|
13214
|
-
this.loadedEntityResourceVersion = null;
|
|
13215
|
-
this.entityVersionInvalidated = false;
|
|
13216
13261
|
this.hydratedEntityId = null;
|
|
13217
13262
|
this.entityHydrationPending = false;
|
|
13218
13263
|
}
|
|
@@ -13232,9 +13277,6 @@ class PraxisDynamicForm {
|
|
|
13232
13277
|
this.loadEntity();
|
|
13233
13278
|
}
|
|
13234
13279
|
}
|
|
13235
|
-
if (changes['submitResourceVersion']) {
|
|
13236
|
-
this.entityVersionInvalidated = false;
|
|
13237
|
-
}
|
|
13238
13280
|
if (changes['initialValue'] &&
|
|
13239
13281
|
!changes['initialValue'].firstChange &&
|
|
13240
13282
|
this.usesInitialValueAsAuthoritativeSnapshot() &&
|
|
@@ -13247,9 +13289,7 @@ class PraxisDynamicForm {
|
|
|
13247
13289
|
this.form.markAsPristine();
|
|
13248
13290
|
this.form.markAsUntouched();
|
|
13249
13291
|
this.loadedEntityData = normalized;
|
|
13250
|
-
this.
|
|
13251
|
-
this.extractResourceVersionCandidate(this.initialValue);
|
|
13252
|
-
this.entityVersionInvalidated = false;
|
|
13292
|
+
this.loadedResourceRevision = null;
|
|
13253
13293
|
this.hydratedEntityId = this.pendingEntityId;
|
|
13254
13294
|
this.flushEntityHydrationState();
|
|
13255
13295
|
this.emitPayloadPreview('entity-hydrated');
|
|
@@ -13266,13 +13306,15 @@ class PraxisDynamicForm {
|
|
|
13266
13306
|
this.debugLog('[PDF] ngOnChanges readUrl=', this.readUrl);
|
|
13267
13307
|
this.loadedEntityId = null;
|
|
13268
13308
|
this.loadedEntityData = null;
|
|
13269
|
-
this.loadedEntityResourceVersion = null;
|
|
13270
|
-
this.entityVersionInvalidated = false;
|
|
13271
13309
|
this.hydratedEntityId = null;
|
|
13272
13310
|
if (this.config.fieldMetadata?.length && this.pendingEntityId != null) {
|
|
13273
13311
|
this.loadEntity();
|
|
13274
13312
|
}
|
|
13275
13313
|
}
|
|
13314
|
+
if (revisionIdentityChanged && this.config.fieldMetadata?.length && this.pendingEntityId != null
|
|
13315
|
+
&& !changes['resourceId'] && !changes['mode'] && !changes['readUrl']) {
|
|
13316
|
+
this.loadEntity();
|
|
13317
|
+
}
|
|
13276
13318
|
if (changes['config'] || changes['presentationModeGlobal'] || changes['domainRules']) {
|
|
13277
13319
|
let shouldRebuildFromExternalConfig = false;
|
|
13278
13320
|
let externalConfigChanged = false;
|
|
@@ -13869,7 +13911,8 @@ class PraxisDynamicForm {
|
|
|
13869
13911
|
return;
|
|
13870
13912
|
}
|
|
13871
13913
|
// Avoid duplicate network requests but allow re-patching after form build
|
|
13872
|
-
if (this.loadedEntityId === this.pendingEntityId
|
|
13914
|
+
if (this.loadedEntityId === this.pendingEntityId && !this.resourceRevisionInvalidated
|
|
13915
|
+
&& (this.loadedResourceRevision == null || this.loadedResourceRevision.binding === this.resourceRevisionBinding())) {
|
|
13873
13916
|
if (this.loadedEntityData && Object.keys(this.form.controls).length) {
|
|
13874
13917
|
this.entityHydrationPending = false;
|
|
13875
13918
|
this.form.patchValue(this.normalizeFormPatchValue(this.loadedEntityData));
|
|
@@ -13899,9 +13942,11 @@ class PraxisDynamicForm {
|
|
|
13899
13942
|
// A new identity or read projection supersedes any in-flight entity read.
|
|
13900
13943
|
this.entityReadCancel$.next();
|
|
13901
13944
|
this.loadedEntityData = null;
|
|
13902
|
-
this.loadedEntityResourceVersion = null;
|
|
13903
|
-
this.entityVersionInvalidated = false;
|
|
13904
13945
|
this.loadedEntityId = this.pendingEntityId;
|
|
13946
|
+
const binding = this.resourceRevisionBinding();
|
|
13947
|
+
const attempt = ++this.entityReadAttempt;
|
|
13948
|
+
this.loadedResourceRevision = null;
|
|
13949
|
+
this.resourceRevisionInvalidated = true;
|
|
13905
13950
|
this.entityHydrationPending = true;
|
|
13906
13951
|
this.entityReadFailed = false;
|
|
13907
13952
|
this.flushEntityHydrationState();
|
|
@@ -13914,39 +13959,18 @@ class PraxisDynamicForm {
|
|
|
13914
13959
|
headers: composeHeadersWithVersion(this.resolveApiEntry()),
|
|
13915
13960
|
observe: 'response',
|
|
13916
13961
|
})
|
|
13917
|
-
|
|
13918
|
-
const isHttpResponse = response &&
|
|
13919
|
-
typeof response === 'object' &&
|
|
13920
|
-
'body' in response &&
|
|
13921
|
-
'headers' in response;
|
|
13922
|
-
const etagHeader = isHttpResponse
|
|
13923
|
-
? this.normalizeResourceVersionToken(response.headers?.get?.('ETag'))
|
|
13924
|
-
: null;
|
|
13925
|
-
if (etagHeader) {
|
|
13926
|
-
this.loadedEntityResourceVersion = etagHeader;
|
|
13927
|
-
if (this.pendingEntityId != null) {
|
|
13928
|
-
this.crud?.setResourceVersion?.(this.pendingEntityId, etagHeader);
|
|
13929
|
-
}
|
|
13930
|
-
}
|
|
13931
|
-
return this.unwrapRestApiResponse(isHttpResponse ? response.body : response);
|
|
13932
|
-
}))
|
|
13933
|
-
: this.crud.getById(this.pendingEntityId, { httpContext });
|
|
13962
|
+
: this.crud.getByIdHttpResponse(this.pendingEntityId, { httpContext });
|
|
13934
13963
|
request$
|
|
13935
13964
|
.pipe(takeUntil(this.entityReadCancel$), takeUntil(this.destroy$))
|
|
13936
13965
|
.subscribe({
|
|
13937
|
-
next: (
|
|
13966
|
+
next: (response) => {
|
|
13967
|
+
if (attempt !== this.entityReadAttempt || binding !== this.resourceRevisionBinding())
|
|
13968
|
+
return;
|
|
13969
|
+
const data = this.unwrapRestApiResponse(response.body);
|
|
13938
13970
|
this.entityHydrationPending = false;
|
|
13939
13971
|
if (data && typeof data === 'object' && !Array.isArray(data)) {
|
|
13940
|
-
|
|
13941
|
-
|
|
13942
|
-
(this.pendingEntityId != null
|
|
13943
|
-
? this.normalizeResourceVersionToken(this.crud?.getResourceVersion?.(this.pendingEntityId))
|
|
13944
|
-
: null);
|
|
13945
|
-
this.loadedEntityResourceVersion = capturedVersion;
|
|
13946
|
-
this.entityVersionInvalidated = false;
|
|
13947
|
-
if (capturedVersion && this.pendingEntityId != null) {
|
|
13948
|
-
this.crud?.setResourceVersion?.(this.pendingEntityId, capturedVersion);
|
|
13949
|
-
}
|
|
13972
|
+
this.loadedResourceRevision = { binding, etag: readStrongResourceEtag(response.headers.get('ETag')) };
|
|
13973
|
+
this.resourceRevisionInvalidated = false;
|
|
13950
13974
|
const normalized = this.normalizeFormPatchValue(data);
|
|
13951
13975
|
this.loadedEntityData = normalized;
|
|
13952
13976
|
if (Object.keys(this.form.controls).length) {
|
|
@@ -13965,17 +13989,21 @@ class PraxisDynamicForm {
|
|
|
13965
13989
|
else {
|
|
13966
13990
|
this.entityReadFailed = true;
|
|
13967
13991
|
this.loadedEntityId = null;
|
|
13968
|
-
this.
|
|
13992
|
+
this.loadedResourceRevision = null;
|
|
13993
|
+
this.resourceRevisionInvalidated = true;
|
|
13969
13994
|
this.warnLog('Invalid entity data received:', data, { actionId: 'entity.load', throttleKey: 'praxis-dynamic-form:invalid-entity-data' });
|
|
13970
13995
|
this.emitLoadingState('data', 'error', 'Dados inválidos recebidos.');
|
|
13971
13996
|
this.flushEntityHydrationState();
|
|
13972
13997
|
}
|
|
13973
13998
|
},
|
|
13974
13999
|
error: (err) => {
|
|
14000
|
+
if (attempt !== this.entityReadAttempt || binding !== this.resourceRevisionBinding())
|
|
14001
|
+
return;
|
|
13975
14002
|
this.entityHydrationPending = false;
|
|
13976
14003
|
this.entityReadFailed = true;
|
|
13977
14004
|
this.loadedEntityId = null;
|
|
13978
|
-
this.
|
|
14005
|
+
this.loadedResourceRevision = null;
|
|
14006
|
+
this.resourceRevisionInvalidated = true;
|
|
13979
14007
|
this.flushEntityHydrationState();
|
|
13980
14008
|
this.emitLoadingState('data', 'error', 'Erro ao carregar dados.', err);
|
|
13981
14009
|
},
|
|
@@ -13986,15 +14014,8 @@ class PraxisDynamicForm {
|
|
|
13986
14014
|
if (!normalized) {
|
|
13987
14015
|
return false;
|
|
13988
14016
|
}
|
|
13989
|
-
|
|
13990
|
-
|
|
13991
|
-
? this.normalizeResourceVersionToken(this.crud?.getResourceVersion?.(this.pendingEntityId))
|
|
13992
|
-
: null);
|
|
13993
|
-
this.loadedEntityResourceVersion = capturedVersion;
|
|
13994
|
-
this.entityVersionInvalidated = false;
|
|
13995
|
-
if (capturedVersion && this.pendingEntityId != null) {
|
|
13996
|
-
this.crud?.setResourceVersion?.(this.pendingEntityId, capturedVersion);
|
|
13997
|
-
}
|
|
14017
|
+
// An externally supplied DTO is not a paired HTTP read revision.
|
|
14018
|
+
this.loadedResourceRevision = null;
|
|
13998
14019
|
this.loadedEntityId = this.pendingEntityId;
|
|
13999
14020
|
this.loadedEntityData = normalized;
|
|
14000
14021
|
this.entityHydrationPending = false;
|
|
@@ -14159,7 +14180,7 @@ class PraxisDynamicForm {
|
|
|
14159
14180
|
connection.add(this.reactiveDeterminationRuntime.connect(this.form, this.schemaRootReactiveDeterminations.filter((definition) => definition.scope.formMode === this.mode
|
|
14160
14181
|
&& (!this.schemaRootOperationId
|
|
14161
14182
|
|| definition.scope.schemaOperationId === this.schemaRootOperationId)), (definition, requestBody, correlationId) => {
|
|
14162
|
-
let headers = composeHeadersWithVersion(this.resolveApiEntry()) ?? new HttpHeaders();
|
|
14183
|
+
let headers = (composeHeadersWithVersion(this.resolveApiEntry()) ?? new HttpHeaders()).delete('If-Match');
|
|
14163
14184
|
headers = headers.set('X-Correlation-Id', correlationId);
|
|
14164
14185
|
return this.http
|
|
14165
14186
|
.post(resolveReactiveDeterminationHref(definition.capability.href, this.resolveApiOrigin()), requestBody, { headers })
|
|
@@ -16658,30 +16679,59 @@ class PraxisDynamicForm {
|
|
|
16658
16679
|
async onSubmit() {
|
|
16659
16680
|
if (this.effectiveReadonly || this.effectivePresentation)
|
|
16660
16681
|
return;
|
|
16661
|
-
if (this.submitting || this.submitPreparationInProgress)
|
|
16682
|
+
if (this.isDestroyed || this.submitting || this.submitPreparationInProgress)
|
|
16662
16683
|
return;
|
|
16663
16684
|
if (this.entityHydrationPending || this.entityReadFailed) {
|
|
16664
16685
|
return;
|
|
16665
16686
|
}
|
|
16687
|
+
const lifecycle = ++this.submitLifecycleSequence;
|
|
16688
|
+
this.activeSubmitLifecycle = lifecycle;
|
|
16689
|
+
this.activeSubmitAutomatic = this.usesAutomaticItemRevision();
|
|
16666
16690
|
this.submitPreparationInProgress = true;
|
|
16667
16691
|
try {
|
|
16668
|
-
await this.prepareAndSubmit();
|
|
16692
|
+
await this.prepareAndSubmit(lifecycle);
|
|
16669
16693
|
}
|
|
16670
16694
|
finally {
|
|
16671
|
-
this.
|
|
16695
|
+
if (this.activeSubmitLifecycle === lifecycle)
|
|
16696
|
+
this.submitPreparationInProgress = false;
|
|
16672
16697
|
}
|
|
16673
16698
|
}
|
|
16674
|
-
async prepareAndSubmit() {
|
|
16699
|
+
async prepareAndSubmit(lifecycle) {
|
|
16675
16700
|
const submittingEntityId = this.resourceId;
|
|
16701
|
+
const binding = this.resourceRevisionBinding();
|
|
16702
|
+
const epoch = this.entityReadAttempt;
|
|
16703
|
+
const automatic = this.usesAutomaticItemRevision();
|
|
16704
|
+
const preparationForm = this.form;
|
|
16705
|
+
const preparationIsCurrent = () => {
|
|
16706
|
+
if (this.isDestroyed)
|
|
16707
|
+
return false;
|
|
16708
|
+
if (!automatic || this.isResourceRevisionCurrent(binding, epoch))
|
|
16709
|
+
return true;
|
|
16710
|
+
// A previously started hook can still hold the reused group. Its draft is no longer authoritative.
|
|
16711
|
+
if (preparationForm === this.form) {
|
|
16712
|
+
this.invalidateAutomaticSubmitUi();
|
|
16713
|
+
this.entityReadAttempt++;
|
|
16714
|
+
this.loadedResourceRevision = null;
|
|
16715
|
+
this.resourceRevisionInvalidated = true;
|
|
16716
|
+
}
|
|
16717
|
+
return false;
|
|
16718
|
+
};
|
|
16719
|
+
const waitForCurrentPreparation = async () => {
|
|
16720
|
+
const stable = await this.waitForReactiveDeterminationsBeforeSubmit(() => !this.isDestroyed && (!automatic || this.isResourceRevisionCurrent(binding, epoch)));
|
|
16721
|
+
const current = preparationIsCurrent();
|
|
16722
|
+
return current && stable;
|
|
16723
|
+
};
|
|
16676
16724
|
this.clearSubmitFieldErrors();
|
|
16677
|
-
if (!(await
|
|
16725
|
+
if (!(await waitForCurrentPreparation()))
|
|
16678
16726
|
return;
|
|
16679
16727
|
// Hooks: beforeValidate
|
|
16680
16728
|
try {
|
|
16681
16729
|
await this.runHooks('beforeValidate');
|
|
16682
16730
|
}
|
|
16683
16731
|
catch { }
|
|
16684
|
-
if (!(
|
|
16732
|
+
if (!preparationIsCurrent())
|
|
16733
|
+
return;
|
|
16734
|
+
if (!(await waitForCurrentPreparation()))
|
|
16685
16735
|
return;
|
|
16686
16736
|
if (this.form.invalid) {
|
|
16687
16737
|
// UX: marcar todos como touched para exibir erros
|
|
@@ -16723,6 +16773,8 @@ class PraxisDynamicForm {
|
|
|
16723
16773
|
await this.runHooks('afterValidate');
|
|
16724
16774
|
}
|
|
16725
16775
|
catch { }
|
|
16776
|
+
if (!preparationIsCurrent())
|
|
16777
|
+
return;
|
|
16726
16778
|
return;
|
|
16727
16779
|
}
|
|
16728
16780
|
// Hooks: afterValidate (valid)
|
|
@@ -16730,11 +16782,15 @@ class PraxisDynamicForm {
|
|
|
16730
16782
|
await this.runHooks('afterValidate');
|
|
16731
16783
|
}
|
|
16732
16784
|
catch { }
|
|
16733
|
-
if (!(
|
|
16785
|
+
if (!preparationIsCurrent())
|
|
16786
|
+
return;
|
|
16787
|
+
if (!(await waitForCurrentPreparation()))
|
|
16734
16788
|
return;
|
|
16735
16789
|
// Hooks: beforeSubmit (cancelable) — must run BEFORE snapshotting form value
|
|
16736
16790
|
try {
|
|
16737
16791
|
const res = await this.runHooks('beforeSubmit');
|
|
16792
|
+
if (!preparationIsCurrent())
|
|
16793
|
+
return;
|
|
16738
16794
|
if (res && res.stopped && res.stopped.outcome?.status === 'cancel') {
|
|
16739
16795
|
// Cancel submission path — focus field if provided
|
|
16740
16796
|
const field = res.stopped.outcome?.field;
|
|
@@ -16760,7 +16816,9 @@ class PraxisDynamicForm {
|
|
|
16760
16816
|
}
|
|
16761
16817
|
}
|
|
16762
16818
|
catch { }
|
|
16763
|
-
if (!(
|
|
16819
|
+
if (!preparationIsCurrent())
|
|
16820
|
+
return;
|
|
16821
|
+
if (!(await waitForCurrentPreparation()))
|
|
16764
16822
|
return;
|
|
16765
16823
|
if (this.form.invalid) {
|
|
16766
16824
|
this.form.markAllAsTouched();
|
|
@@ -16773,6 +16831,8 @@ class PraxisDynamicForm {
|
|
|
16773
16831
|
const rawFormData = this.form.getRawValue();
|
|
16774
16832
|
const formData = this.prepareCurrentSubmitPayload(rawFormData);
|
|
16775
16833
|
const operation = this.resolveSubmitOperation();
|
|
16834
|
+
if (!preparationIsCurrent())
|
|
16835
|
+
return;
|
|
16776
16836
|
this.formSubmit.emit({
|
|
16777
16837
|
stage: 'before',
|
|
16778
16838
|
formData,
|
|
@@ -16782,6 +16842,8 @@ class PraxisDynamicForm {
|
|
|
16782
16842
|
entityId: this.resourceId ?? undefined,
|
|
16783
16843
|
});
|
|
16784
16844
|
// Extra: pass snapshot to after hooks via extras when relevant
|
|
16845
|
+
if (!preparationIsCurrent())
|
|
16846
|
+
return;
|
|
16785
16847
|
const req$ = this.buildSubmitRequest(formData, operation);
|
|
16786
16848
|
// Mensagem de loading configurável durante o submit
|
|
16787
16849
|
try {
|
|
@@ -16791,17 +16853,30 @@ class PraxisDynamicForm {
|
|
|
16791
16853
|
}
|
|
16792
16854
|
}
|
|
16793
16855
|
catch { }
|
|
16856
|
+
const operationSnack = this._activeSnack;
|
|
16794
16857
|
this.submitting = true;
|
|
16795
16858
|
this.cdr.detectChanges();
|
|
16796
16859
|
req$
|
|
16797
16860
|
.pipe(takeUntil(this.destroy$), finalize(() => {
|
|
16861
|
+
try {
|
|
16862
|
+
operationSnack?.dismiss();
|
|
16863
|
+
}
|
|
16864
|
+
catch { }
|
|
16865
|
+
if (this._activeSnack === operationSnack)
|
|
16866
|
+
this._activeSnack = null;
|
|
16798
16867
|
setTimeout(() => {
|
|
16868
|
+
if (this.activeSubmitLifecycle !== lifecycle)
|
|
16869
|
+
return;
|
|
16799
16870
|
this.submitting = false;
|
|
16871
|
+
this.activeSubmitLifecycle = null;
|
|
16872
|
+
this.activeSubmitAutomatic = false;
|
|
16800
16873
|
this.cdr.detectChanges();
|
|
16801
16874
|
});
|
|
16802
16875
|
}))
|
|
16803
16876
|
.subscribe({
|
|
16804
16877
|
next: (result) => {
|
|
16878
|
+
if (this.activeSubmitLifecycle !== lifecycle || automatic && !this.isResourceRevisionCurrent(binding, epoch))
|
|
16879
|
+
return;
|
|
16805
16880
|
this.clearSubmitFieldErrors();
|
|
16806
16881
|
try {
|
|
16807
16882
|
const rp = (this.resourcePath || '').trim();
|
|
@@ -16812,8 +16887,9 @@ class PraxisDynamicForm {
|
|
|
16812
16887
|
catch { }
|
|
16813
16888
|
// Encerrar loading
|
|
16814
16889
|
try {
|
|
16815
|
-
|
|
16816
|
-
this._activeSnack
|
|
16890
|
+
operationSnack?.dismiss();
|
|
16891
|
+
if (this._activeSnack === operationSnack)
|
|
16892
|
+
this._activeSnack = null;
|
|
16817
16893
|
}
|
|
16818
16894
|
catch { }
|
|
16819
16895
|
this.formSubmit.emit({
|
|
@@ -16855,10 +16931,13 @@ class PraxisDynamicForm {
|
|
|
16855
16931
|
}
|
|
16856
16932
|
},
|
|
16857
16933
|
error: (error) => {
|
|
16934
|
+
if (this.activeSubmitLifecycle !== lifecycle || automatic && !this.isResourceRevisionCurrent(binding, epoch))
|
|
16935
|
+
return;
|
|
16858
16936
|
// Encerrar loading
|
|
16859
16937
|
try {
|
|
16860
|
-
|
|
16861
|
-
this._activeSnack
|
|
16938
|
+
operationSnack?.dismiss();
|
|
16939
|
+
if (this._activeSnack === operationSnack)
|
|
16940
|
+
this._activeSnack = null;
|
|
16862
16941
|
}
|
|
16863
16942
|
catch { }
|
|
16864
16943
|
this.formSubmit.emit({
|
|
@@ -16882,7 +16961,8 @@ class PraxisDynamicForm {
|
|
|
16882
16961
|
...detail,
|
|
16883
16962
|
}));
|
|
16884
16963
|
this.applySubmitFieldErrors(normalizedError.details);
|
|
16885
|
-
const errorMsg =
|
|
16964
|
+
const errorMsg = error instanceof PraxisDynamicFormResourceRevisionReloadError
|
|
16965
|
+
? error.message : this.resolveSubmitErrorMessage(normalizedError, operation);
|
|
16886
16966
|
this.submitError = errorMsg;
|
|
16887
16967
|
this.snackBar.open(errorMsg, undefined, {
|
|
16888
16968
|
duration: 4000,
|
|
@@ -16893,7 +16973,9 @@ class PraxisDynamicForm {
|
|
|
16893
16973
|
},
|
|
16894
16974
|
});
|
|
16895
16975
|
}
|
|
16896
|
-
async waitForReactiveDeterminationsBeforeSubmit() {
|
|
16976
|
+
async waitForReactiveDeterminationsBeforeSubmit(stillCurrent = () => true) {
|
|
16977
|
+
if (!stillCurrent())
|
|
16978
|
+
return false;
|
|
16897
16979
|
const pendingNotice = this.reactiveDeterminationRuntime.hasPending(this.form)
|
|
16898
16980
|
? this.snackBar.open(this.i18n.t('runtime.reactiveDetermination.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 })
|
|
16899
16981
|
: null;
|
|
@@ -16902,6 +16984,8 @@ class PraxisDynamicForm {
|
|
|
16902
16984
|
pendingNotice?.dismiss();
|
|
16903
16985
|
}
|
|
16904
16986
|
catch { }
|
|
16987
|
+
if (!stillCurrent())
|
|
16988
|
+
return false;
|
|
16905
16989
|
if (stable)
|
|
16906
16990
|
return true;
|
|
16907
16991
|
this.snackBar.open(this.i18n.t('runtime.reactiveDetermination.submitUnsatisfied', undefined, 'Os campos relacionados não foram atualizados com sucesso. Revise os dados e tente novamente.', PRAXIS_DYNAMIC_FORM_I18N_NAMESPACE), this.i18n.t('common.dismiss', undefined, 'Fechar', PRAXIS_DYNAMIC_FORM_I18N_NAMESPACE), { duration: 7000 });
|
|
@@ -20691,6 +20775,9 @@ class PraxisDynamicForm {
|
|
|
20691
20775
|
ngOnDestroy() {
|
|
20692
20776
|
this.isDestroyed = true;
|
|
20693
20777
|
this.cancelSchemaReads();
|
|
20778
|
+
this.entityReadAttempt++;
|
|
20779
|
+
this.invalidateAutomaticSubmitUi();
|
|
20780
|
+
this.activeSubmitLifecycle = null;
|
|
20694
20781
|
this.resetSubmitErrorState();
|
|
20695
20782
|
this.assistantSessions.removeContextSession(this.buildAiAssistantContextSnapshot());
|
|
20696
20783
|
this.aiAssistantStateSubscription?.unsubscribe();
|
|
@@ -21099,6 +21186,10 @@ class PraxisDynamicForm {
|
|
|
21099
21186
|
this.cancelSchemaReads();
|
|
21100
21187
|
this.entityReadCancel$.next();
|
|
21101
21188
|
this.isLoading = false;
|
|
21189
|
+
// Reset state for retry; only a successful new read clears a rejected revision.
|
|
21190
|
+
this.invalidateAutomaticSubmitUi();
|
|
21191
|
+
this.entityReadAttempt++;
|
|
21192
|
+
this.loadedResourceRevision = null;
|
|
21102
21193
|
this.isInitialized = false;
|
|
21103
21194
|
this.schemaCache = null;
|
|
21104
21195
|
this.loadedEntityId = null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@praxisui/dynamic-form",
|
|
3
|
-
"version": "10.0.0-rc.
|
|
3
|
+
"version": "10.0.0-rc.19",
|
|
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": "^10.0.0-rc.
|
|
13
|
-
"@praxisui/dynamic-fields": "^10.0.0-rc.
|
|
14
|
-
"@praxisui/metadata-editor": "^10.0.0-rc.
|
|
15
|
-
"@praxisui/rich-content": "^10.0.0-rc.
|
|
16
|
-
"@praxisui/settings-panel": "^10.0.0-rc.
|
|
17
|
-
"@praxisui/visual-builder": "^10.0.0-rc.
|
|
18
|
-
"@praxisui/core": "^10.0.0-rc.
|
|
12
|
+
"@praxisui/ai": "^10.0.0-rc.19",
|
|
13
|
+
"@praxisui/dynamic-fields": "^10.0.0-rc.19",
|
|
14
|
+
"@praxisui/metadata-editor": "^10.0.0-rc.19",
|
|
15
|
+
"@praxisui/rich-content": "^10.0.0-rc.19",
|
|
16
|
+
"@praxisui/settings-panel": "^10.0.0-rc.19",
|
|
17
|
+
"@praxisui/visual-builder": "^10.0.0-rc.19",
|
|
18
|
+
"@praxisui/core": "^10.0.0-rc.19",
|
|
19
19
|
"rxjs": "^7.8.0"
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
@@ -1148,3 +1148,10 @@ O editor nativo, em **Geral → Sem registro selecionado**, materializa o contra
|
|
|
1148
1148
|
As opções de aparência ficam em disclosure avançado: `tone` (`neutral`, `primary`, `secondary`), `variant` (`card`, `inline`, `panel`, `transparent`), `alignment` (`start`, `center`), `density` (`compact`, `comfortable`) e `iconContainer` (`none`, `circle`, `soft`). Ausência de opção herda o padrão do componente; JSON inválido produz diagnóstico.
|
|
1149
1149
|
|
|
1150
1150
|
A orientação aparece somente em `mode=view`, sem registro selecionado nem snapshot local. Criar e Editar preservam a configuração sem exibi-la. Desligar grava `null`, que impede reativação por uma preferência antiga; substituir um documento que omite o binding também limpa a orientação. Em `input-first`, a página governa o valor. Filter Form não expõe essa seção, pois não possui contexto de seleção equivalente.
|
|
1151
|
+
### Item revision custody
|
|
1152
|
+
|
|
1153
|
+
Automatic edit PUT/PATCH sends the strong ETag from the same hydration GET that supplied the form values, including `readUrl` and custom `getById`. A successful response containing the corresponding item representation replaces both cached data and revision together, including server normalization. A command acknowledgment or incomplete representation cannot rotate that snapshot. Missing/weak ETags are not synthesized; 412/428 requires explicit reload before another automatic submit.
|
|
1154
|
+
|
|
1155
|
+
Item/API-context changes, reload and destruction invalidate the transient read generation, including identity or enterprise-scope A→B→A. Automatic preparation captures its original binding/generation before asynchronous hooks or determinations. An obsolete preparation does not submit; if its hook retained the reused form group, another submit requires explicit reload to reconcile any contaminated draft. Obsolete success/error responses cannot publish formSubmit stages, run submit result hooks, change fields/errors, close the new surface or reset a newer submit's loading/snackbar state.
|
|
1156
|
+
|
|
1157
|
+
This adds no persisted configuration field. Explicit `submitResourceVersion` and cross-resource version-target inputs retain their command precedence and response/event semantics; their responses do not hydrate or rotate the originating item snapshot.
|
|
@@ -982,6 +982,9 @@ declare class PraxisDynamicForm implements OnInit, OnChanges, OnDestroy {
|
|
|
982
982
|
isLoading: boolean;
|
|
983
983
|
submitting: boolean;
|
|
984
984
|
private submitPreparationInProgress;
|
|
985
|
+
private submitLifecycleSequence;
|
|
986
|
+
private activeSubmitLifecycle;
|
|
987
|
+
private activeSubmitAutomatic;
|
|
985
988
|
submitError: string | null;
|
|
986
989
|
initializationStatus: 'idle' | 'loading' | 'success' | 'error';
|
|
987
990
|
currentErrorMessage: string;
|
|
@@ -1016,8 +1019,9 @@ declare class PraxisDynamicForm implements OnInit, OnChanges, OnDestroy {
|
|
|
1016
1019
|
private entitySelectionCleared;
|
|
1017
1020
|
private hydratedEntityId;
|
|
1018
1021
|
private loadedEntityData;
|
|
1019
|
-
private
|
|
1020
|
-
private
|
|
1022
|
+
private loadedResourceRevision;
|
|
1023
|
+
private resourceRevisionInvalidated;
|
|
1024
|
+
private entityReadAttempt;
|
|
1021
1025
|
protected entityHydrationPending: boolean;
|
|
1022
1026
|
protected entityReadFailed: boolean;
|
|
1023
1027
|
protected entityReadText(key: 'loading' | 'error' | 'retry' | 'region' | 'success'): string;
|
|
@@ -1138,10 +1142,15 @@ declare class PraxisDynamicForm implements OnInit, OnChanges, OnDestroy {
|
|
|
1138
1142
|
private resolveSubmitOperation;
|
|
1139
1143
|
private buildSubmitRequest;
|
|
1140
1144
|
private resolveSubmitResourceVersion;
|
|
1141
|
-
|
|
1142
|
-
private
|
|
1143
|
-
private
|
|
1144
|
-
private
|
|
1145
|
+
/** Identity and server context are captured with the representation, never reconstructed at submit by GET. */
|
|
1146
|
+
private resourceRevisionBinding;
|
|
1147
|
+
private usesAutomaticItemRevision;
|
|
1148
|
+
private isResourceRevisionCurrent;
|
|
1149
|
+
private invalidateAutomaticSubmitUi;
|
|
1150
|
+
private resourceRevisionNeedsReload;
|
|
1151
|
+
private resourceRevisionReloadError;
|
|
1152
|
+
private captureSavedResourceRevision;
|
|
1153
|
+
private rejectResourceRevision;
|
|
1145
1154
|
private normalizeResourceVersionToken;
|
|
1146
1155
|
private extractResourceVersionCandidate;
|
|
1147
1156
|
private normalizeSubmitMethod;
|
|
@@ -1170,6 +1179,8 @@ declare class PraxisDynamicForm implements OnInit, OnChanges, OnDestroy {
|
|
|
1170
1179
|
private initializeStandaloneConfig;
|
|
1171
1180
|
private initializeDisconnectedEmptyState;
|
|
1172
1181
|
private reloadForModeChange;
|
|
1182
|
+
private resourceRevisionContextScope;
|
|
1183
|
+
private subscribeToRuntimeContextChanges;
|
|
1173
1184
|
ngOnInit(): void;
|
|
1174
1185
|
ngOnChanges(changes: SimpleChanges): void;
|
|
1175
1186
|
private blurAllInputs;
|