@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-14T22:59:57.823Z",
3
+ "generatedAt": "2026-10-15T00:20:34.117Z",
4
4
  "packageName": "@praxisui/dynamic-form",
5
- "packageVersion": "10.0.0-rc.18",
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
- this._customEndpoints = value;
11935
- if (value && Object.keys(value).length > 0) {
11936
- this.crud.configureEndpoints(value);
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
- loadedEntityResourceVersion = null;
12027
- entityVersionInvalidated = false;
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
- return (operation === 'update'
12556
- ? this.crud.update(this.resourceId, formData)
12557
- : this.crud.create(formData)).pipe(map$1((response) => this.unwrapAndCaptureSubmitResponse(response, operation === 'update')), catchError((err) => this.handleVersionedSubmitError(err)));
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
- if (method === 'patch') {
12577
- return this.http
12578
- .patch(resolvedUrl, formData, {
12579
- headers,
12580
- observe: 'response',
12581
- })
12582
- .pipe(map$1((response) => this.unwrapAndCaptureSubmitResponse(response, true)), catchError((err) => this.handleVersionedSubmitError(err)));
12583
- }
12584
- if (method === 'put') {
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
- return of(this.resolveCurrentEntityResourceVersion(formData));
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
- resolveCurrentEntityResourceVersion(formData) {
12657
- const method = this.normalizeSubmitMethod(this.submitMethod);
12658
- const isEntityUpdate = this.mode === 'edit' || method === 'put' || method === 'patch';
12659
- if (!isEntityUpdate) {
12660
- return null;
12661
- }
12662
- const entityId = this.resourceId ?? this.pendingEntityId ?? this.loadedEntityId ?? null;
12663
- const crudCachedVersion = entityId != null
12664
- ? this.normalizeResourceVersionToken(this.crud?.getResourceVersion?.(entityId))
12665
- : null;
12666
- if (this.entityVersionInvalidated) {
12667
- return this.loadedEntityResourceVersion || crudCachedVersion || null;
12668
- }
12669
- return (this.loadedEntityResourceVersion ||
12670
- crudCachedVersion ||
12671
- this.extractResourceVersionCandidate(this.loadedEntityData) ||
12672
- this.extractResourceVersionCandidate(formData) ||
12673
- this.extractResourceVersionCandidate(this.initialValue) ||
12674
- null);
12675
- }
12676
- unwrapAndCaptureSubmitResponse(response, isEntityMutation) {
12677
- const isHttpResponse = response &&
12678
- typeof response === 'object' &&
12679
- 'body' in response &&
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
- captureUpdatedEntityResourceVersion(payload, etagHeader = null, isEntityMutation = true) {
12689
- const payloadId = payload && typeof payload === 'object' && !Array.isArray(payload)
12690
- ? payload['id'] ?? null
12691
- : null;
12692
- const entityId = this.resourceId ??
12693
- this.pendingEntityId ??
12694
- this.loadedEntityId ??
12695
- payloadId ??
12696
- null;
12697
- const nextVersion = etagHeader ??
12698
- this.extractResourceVersionCandidate(payload) ??
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
- this.entityVersionInvalidated = false;
12706
- if (isEntityMutation || entityId != null) {
12707
- this.loadedEntityResourceVersion = nextVersion;
12708
- }
12709
- if (this.submitResourceVersion != null &&
12710
- !this.submitResourceVersionTargetResourceKey &&
12711
- !this.submitResourceVersionTargetIdField) {
12712
- this.submitResourceVersion = nextVersion;
12713
- }
12714
- if (this.loadedEntityData &&
12715
- typeof this.loadedEntityData === 'object' &&
12716
- !Array.isArray(this.loadedEntityData) &&
12717
- 'resourceVersion' in this.loadedEntityData) {
12718
- this.loadedEntityData = {
12719
- ...this.loadedEntityData,
12720
- resourceVersion: nextVersion,
12721
- };
12722
- }
12723
- if (entityId != null) {
12724
- this.crud?.setResourceVersion?.(entityId, nextVersion);
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
- handleVersionedSubmitError(err) {
12728
- if (err?.status === 412 || err?.status === 428) {
12729
- if (!this.submitResourceVersionTargetResourceKey &&
12730
- !this.submitResourceVersionTargetIdField) {
12731
- this.entityVersionInvalidated = true;
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(() => err);
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 ?? undefined,
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
- ngOnInit() {
13028
- this.debugLog('[PDF] ngOnInit gating', {
13029
- formId: this.formId,
13030
- resourcePath: this.resourcePath,
13031
- });
13032
- this.inputsLoaded = !this.formId;
13033
- if (!this.formId && !this.resourcePath && this.hasRenderableConfig()) {
13034
- this.initializeStandaloneConfig();
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.loadedEntityResourceVersion =
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
- .pipe(map$1((response) => {
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: (data) => {
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
- const capturedVersion = this.loadedEntityResourceVersion ??
13941
- this.extractResourceVersionCandidate(data) ??
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.loadedEntityResourceVersion = null;
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.loadedEntityResourceVersion = null;
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
- const capturedVersion = this.extractResourceVersionCandidate(this.initialValue) ??
13990
- (this.pendingEntityId != null
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.submitPreparationInProgress = false;
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 this.waitForReactiveDeterminationsBeforeSubmit()))
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 (!(await this.waitForReactiveDeterminationsBeforeSubmit()))
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 (!(await this.waitForReactiveDeterminationsBeforeSubmit()))
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 (!(await this.waitForReactiveDeterminationsBeforeSubmit()))
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
- this._activeSnack?.dismiss();
16816
- this._activeSnack = null;
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
- this._activeSnack?.dismiss();
16861
- this._activeSnack = null;
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 = this.resolveSubmitErrorMessage(normalizedError, operation);
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.18",
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.18",
13
- "@praxisui/dynamic-fields": "^10.0.0-rc.18",
14
- "@praxisui/metadata-editor": "^10.0.0-rc.18",
15
- "@praxisui/rich-content": "^10.0.0-rc.18",
16
- "@praxisui/settings-panel": "^10.0.0-rc.18",
17
- "@praxisui/visual-builder": "^10.0.0-rc.18",
18
- "@praxisui/core": "^10.0.0-rc.18",
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 loadedEntityResourceVersion;
1020
- private entityVersionInvalidated;
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
- private resolveCurrentEntityResourceVersion;
1142
- private unwrapAndCaptureSubmitResponse;
1143
- private captureUpdatedEntityResourceVersion;
1144
- private handleVersionedSubmitError;
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;