@praxisui/dynamic-form 9.0.0-beta.73 → 9.0.0-beta.75

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.
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": "1.0.0",
3
- "generatedAt": "2026-07-12T01:34:40.567Z",
3
+ "generatedAt": "2026-07-16T21:37:28.607Z",
4
4
  "packageName": "@praxisui/dynamic-form",
5
- "packageVersion": "9.0.0-beta.73",
5
+ "packageVersion": "9.0.0-beta.75",
6
6
  "sourceRegistry": "praxis-component-registry-ingestion",
7
7
  "sourceRegistryVersion": "1.0.0",
8
8
  "componentCount": 2,
@@ -1,21 +1,22 @@
1
- import { firstValueFrom } from 'rxjs';
1
+ import { Observable } from 'rxjs';
2
+ import { withAuthoringScopePolicy } from '@praxisui/ai';
2
3
 
3
4
  class DynamicFormAgenticAuthoringTurnFlow {
4
5
  adapter;
5
- aiApi;
6
+ turnClient;
6
7
  mode = 'agentic-authoring';
7
- constructor(adapter, aiApi) {
8
+ constructor(adapter, turnClient) {
8
9
  this.adapter = adapter;
9
- this.aiApi = aiApi;
10
+ this.turnClient = turnClient;
10
11
  }
11
- async submit(request) {
12
+ submit(request) {
12
13
  const prompt = (request.prompt ?? '').trim();
13
14
  if (!prompt) {
14
- return {
15
+ return Promise.resolve({
15
16
  state: 'listening',
16
17
  phase: 'capture',
17
18
  statusText: '',
18
- };
19
+ });
19
20
  }
20
21
  const componentId = this.adapter.componentId || request.componentId || 'praxis-dynamic-form';
21
22
  const componentType = this.adapter.componentType || request.componentType || 'form';
@@ -23,25 +24,207 @@ class DynamicFormAgenticAuthoringTurnFlow {
23
24
  const runtimeState = this.optionalJsonObject(this.adapter.getRuntimeState?.());
24
25
  const contextHints = this.mergeJsonObjects(this.optionalJsonObject(this.adapter.getAuthoringContext?.()), this.optionalJsonObject(request.action?.contextHints));
25
26
  if (this.shouldRestrictToInlineHelp(contextHints)) {
26
- return this.toInlineHelpOnlyResult(prompt, request, contextHints);
27
+ return Promise.resolve(this.toInlineHelpOnlyResult(prompt, request, contextHints));
27
28
  }
28
- const response = await firstValueFrom(this.aiApi.getPatch({
29
- componentId,
30
- componentType,
31
- userPrompt: prompt,
32
- sessionId: request.sessionId,
33
- clientTurnId: request.clientTurnId,
34
- messages: this.toChatMessages(request.messages, prompt),
35
- currentState,
36
- currentStateDigest: this.buildCurrentStateDigest(currentState, runtimeState),
37
- uiContextRef: {
29
+ const buildRequest = async () => {
30
+ await this.adapter.prepareAuthoringContext?.();
31
+ const preparedRuntimeState = this.optionalJsonObject(this.adapter.getRuntimeState?.()) ?? runtimeState;
32
+ const preparedContextHints = this.mergeJsonObjects(this.optionalJsonObject(this.adapter.getAuthoringContext?.()), this.optionalJsonObject(request.action?.contextHints));
33
+ const dataProfile = this.optionalJsonObject(this.adapter.getDataProfile?.());
34
+ const schemaFields = this.adapter.getSchemaFields?.()
35
+ ?.map((field) => this.toAiJsonObject(field))
36
+ .filter((field) => Object.keys(field).length > 0);
37
+ const patchRequest = withAuthoringScopePolicy({
38
+ componentId,
39
+ componentType,
40
+ userPrompt: prompt,
41
+ sessionId: request.sessionId,
42
+ clientTurnId: request.clientTurnId,
43
+ messages: this.toChatMessages(request.messages, prompt),
44
+ currentState,
45
+ currentStateDigest: this.buildCurrentStateDigest(currentState, preparedRuntimeState),
46
+ uiContextRef: {
47
+ componentId,
48
+ componentType,
49
+ },
50
+ ...(dataProfile ? { dataProfile } : {}),
51
+ ...(preparedRuntimeState ? { runtimeState: preparedRuntimeState } : {}),
52
+ ...(schemaFields?.length ? { schemaFields } : {}),
53
+ ...(preparedContextHints ? { contextHints: preparedContextHints } : {}),
54
+ }, {
38
55
  componentId,
39
56
  componentType,
40
- },
41
- ...(runtimeState ? { runtimeState } : {}),
42
- ...(contextHints ? { contextHints } : {}),
57
+ componentLabel: this.adapter.componentName || 'formulário',
58
+ });
59
+ return {
60
+ ...patchRequest,
61
+ targetApp: 'praxis-ui-angular',
62
+ targetComponentId: componentId,
63
+ currentPage: currentState,
64
+ conversationMessages: this.toConversationMessages(request.messages),
65
+ pendingClarification: this.toPendingClarification(request),
66
+ attachmentSummaries: this.toAttachmentSummaries(request),
67
+ activeSemanticDecision: this.optionalJsonObject(request.activeSemanticDecision ?? request.action?.activeSemanticDecision),
68
+ contextHints: patchRequest.contextHints ?? null,
69
+ };
70
+ };
71
+ return this.submitViaAgenticStream(request, buildRequest);
72
+ }
73
+ submitViaAgenticStream(request, buildRequest) {
74
+ return new Observable((subscriber) => {
75
+ let closed = false;
76
+ let subscription = null;
77
+ const close = () => {
78
+ closed = true;
79
+ subscription?.unsubscribe();
80
+ subscription = null;
81
+ };
82
+ void buildRequest()
83
+ .then((turnRequest) => {
84
+ if (closed)
85
+ return;
86
+ subscription = this.turnClient.streamEvents(turnRequest, {
87
+ initialStatusText: 'Preparando o contexto seguro do formulário.',
88
+ resultTimeoutMs: 90_000,
89
+ streamTimeoutMs: 90_000,
90
+ }).subscribe({
91
+ next: (event) => {
92
+ if (closed)
93
+ return;
94
+ const result = this.toAgenticStreamTurnResult(event, request);
95
+ subscriber.next(result);
96
+ if (result.state !== 'processing') {
97
+ subscriber.complete();
98
+ close();
99
+ }
100
+ },
101
+ error: (error) => {
102
+ if (closed)
103
+ return;
104
+ subscriber.next(this.toAssistantErrorResult(error));
105
+ subscriber.complete();
106
+ close();
107
+ },
108
+ });
109
+ })
110
+ .catch((error) => {
111
+ if (closed)
112
+ return;
113
+ subscriber.next(this.toAssistantErrorResult(error));
114
+ subscriber.complete();
115
+ close();
116
+ });
117
+ return () => close();
118
+ });
119
+ }
120
+ toAgenticStreamTurnResult(event, request) {
121
+ if (event.kind === 'stream-started') {
122
+ return {
123
+ state: 'processing',
124
+ phase: 'contextualize',
125
+ statusText: 'Analisando o formulário e suas capacidades.',
126
+ sessionId: event.start.threadId,
127
+ clientTurnId: request.clientTurnId ?? event.start.turnId,
128
+ observationId: event.start.observationId ?? null,
129
+ canApply: false,
130
+ };
131
+ }
132
+ if (event.kind === 'stream-lifecycle') {
133
+ return {
134
+ state: 'processing',
135
+ phase: 'contextualize',
136
+ statusText: event.statusText,
137
+ sessionId: event.start.threadId,
138
+ clientTurnId: request.clientTurnId ?? event.start.turnId,
139
+ observationId: event.start.observationId ?? null,
140
+ canApply: false,
141
+ diagnostics: event.diagnostics
142
+ ? { streamLifecycle: { phase: event.phase, ...event.diagnostics } }
143
+ : undefined,
144
+ };
145
+ }
146
+ if (event.event.type === 'result') {
147
+ const payload = this.toRecord(event.event.payload) ?? {};
148
+ const rawResponse = this.toRecord(payload['response']) ?? payload;
149
+ const response = this.normalizeAgenticResponse(rawResponse);
150
+ return {
151
+ ...this.toTurnResult(this.compileAdapterResponse(response), request),
152
+ sessionId: event.event.threadId || request.sessionId,
153
+ clientTurnId: request.clientTurnId ?? event.event.turnId,
154
+ };
155
+ }
156
+ return this.turnClient.toTurnResult(event.event, request.clientTurnId);
157
+ }
158
+ normalizeAgenticResponse(response) {
159
+ const type = response['type'];
160
+ const normalizedType = type === 'patch'
161
+ || type === 'clarification'
162
+ || type === 'error'
163
+ || type === 'info'
164
+ ? type
165
+ : this.toRecord(response['patch']) || this.toRecord(response['compiledPatch'])
166
+ ? 'patch'
167
+ : 'info';
168
+ const patch = this.toRecord(response['patch']) ?? this.toRecord(response['compiledPatch']) ?? undefined;
169
+ const message = typeof response['message'] === 'string'
170
+ ? response['message']
171
+ : typeof response['assistantMessage'] === 'string'
172
+ ? response['assistantMessage']
173
+ : undefined;
174
+ return {
175
+ ...response,
176
+ type: normalizedType,
177
+ ...(patch ? { patch } : {}),
178
+ ...(message ? { message } : {}),
179
+ };
180
+ }
181
+ toConversationMessages(messages) {
182
+ return (messages ?? [])
183
+ .filter((message) => message.role === 'user' || message.role === 'assistant' || message.role === 'system')
184
+ .map((message) => ({
185
+ id: message.id,
186
+ role: message.role,
187
+ text: message.text,
188
+ }))
189
+ .filter((message) => message.text.trim().length > 0);
190
+ }
191
+ toPendingClarification(request) {
192
+ const pending = request.pendingClarification;
193
+ if (!pending?.sourcePrompt?.trim())
194
+ return null;
195
+ return {
196
+ sourcePrompt: pending.sourcePrompt,
197
+ questions: pending.questions.map((question) => question.label).filter(Boolean),
198
+ assistantMessage: pending.assistantMessage ?? null,
199
+ clientTurnId: pending.clientTurnId ?? null,
200
+ diagnostics: this.optionalJsonObject(pending.diagnostics) ?? null,
201
+ };
202
+ }
203
+ toAttachmentSummaries(request) {
204
+ return (request.attachments ?? []).map((attachment) => ({
205
+ id: attachment.id,
206
+ name: attachment.name,
207
+ kind: attachment.kind,
208
+ mimeType: attachment.mimeType ?? null,
209
+ sizeBytes: attachment.sizeBytes ?? null,
210
+ source: attachment.source ?? null,
211
+ hasPreview: !!attachment.previewUrl,
43
212
  }));
44
- return this.toTurnResult(this.compileAdapterResponse(response), request);
213
+ }
214
+ toAssistantErrorResult(error) {
215
+ const message = error instanceof Error
216
+ ? error.message
217
+ : typeof error === 'string' && error.trim()
218
+ ? error
219
+ : 'Não foi possível concluir o pedido do formulário.';
220
+ return {
221
+ state: 'error',
222
+ phase: 'capture',
223
+ assistantMessage: `Não consegui concluir este pedido. ${message}`,
224
+ errorText: message,
225
+ canApply: false,
226
+ pendingPatch: null,
227
+ };
45
228
  }
46
229
  async apply(request) {
47
230
  const contextHints = this.optionalJsonObject(this.adapter.getAuthoringContext?.());
@@ -26,7 +26,7 @@ import * as i1 from '@praxisui/core';
26
26
  import { serializeEntityLookupValueForPayload, PraxisI18nService, PraxisIconDirective, providePraxisI18nConfig, RULE_PROPERTY_SCHEMA, FIELD_METADATA_CAPABILITIES, migrateFormLayoutRule, PraxisJsonLogicService, normalizeFormLayoutItems, deepMerge, createDefaultFormConfig, normalizeFormConfig as normalizeFormConfig$1, ensureIds, createEmptyRichContentDocument, isPraxisRuntimeGlobalActionEffect, ASYNC_CONFIG_STORAGE, withFormConfigSections, createFieldLayoutItem, getFormLayoutFieldNames, LoggerService, createCorporateLoggerConfig, ConsoleLoggerSink, GlobalActionService, ResourceQuickConnectComponent, composeHeadersWithVersion, buildApiUrl, mapFieldDefinitionsToMetadata, reconcileFormConfig, materializeFormLayoutFromMetadata, syncWithServerMetadata, PRAXIS_LOADING_CTX, evaluateFieldAccess, normalizeControlTypeKey, normalizePraxisEffectPolicy, buildPraxisEffectDistinctKey, getFormColumnFieldNames, resolveSpan, resolveOffset, resolveOrder, resolveHidden, buildSchemaIdStorageKeySegment, buildSchemaId, fetchWithETag, resolveControlTypeAlias, getTextTransformer, MemoryCacheAdapter, LocalStorageCacheAdapter, SchemaMetadataClient, CONNECTION_STORAGE, API_URL, PRAXIS_LOADING_RENDERER, FORM_HOOKS_PRESETS, EmptyStateCardComponent, isValidFormConfig, ComponentMetadataRegistry, FieldControlType, isRequiredGlobalActionPayloadMissing, GLOBAL_ACTION_CATALOG, PRAXIS_GLOBAL_ACTION_CATALOG, getGlobalActionCatalog, SURFACE_OPEN_I18N_NAMESPACE, getGlobalActionUiSchema, getRequiredGlobalActionPayloadKeys, hasMeaningfulGlobalActionPayloadValue, SurfaceOpenActionEditorComponent, SURFACE_OPEN_I18N_CONFIG, validateGlobalActionRefs, normalizeFormMetadata } from '@praxisui/core';
27
27
  import * as i1$1 from '@praxisui/dynamic-fields';
28
28
  import { getControlTypeCatalog, ConfirmDialogComponent, DynamicFieldLoaderDirective } from '@praxisui/dynamic-fields';
29
- import { BaseAiAdapter, AiBackendApiService, PraxisAssistantSessionRegistryService, PraxisAssistantTurnOrchestratorService, createPraxisAssistantViewportLayout, PraxisAiAssistantShellComponent } from '@praxisui/ai';
29
+ import { BaseAiAdapter, AgenticAuthoringTurnClientService, PraxisAssistantSessionRegistryService, PraxisAssistantTurnOrchestratorService, createPraxisAssistantViewportLayout, PraxisAiAssistantShellComponent } from '@praxisui/ai';
30
30
  import { PraxisRichContent, PraxisRichContentConfigEditor } from '@praxisui/rich-content';
31
31
  import * as i6 from '@angular/material/menu';
32
32
  import { MatMenuModule } from '@angular/material/menu';
@@ -243,6 +243,12 @@ function normalizeSubmitPayload(data) {
243
243
  return normalizeTemporalStrings(normalizeDateArrays(data));
244
244
  }
245
245
  function normalizeTemporalStrings(value) {
246
+ // Date-only fields are converted by the field-aware submit pipeline. Keep
247
+ // native Date values intact here instead of treating them as generic records
248
+ // (which would otherwise turn them into an empty object).
249
+ if (value instanceof Date) {
250
+ return value;
251
+ }
246
252
  if (Array.isArray(value)) {
247
253
  return value.map((item) => normalizeTemporalStrings(item));
248
254
  }
@@ -373,9 +379,73 @@ function filterRecordForSubmit(value, fields, dirtyFields, scopePath) {
373
379
  return payload;
374
380
  }
375
381
  function normalizeFieldValueForSubmit(field, value, dirtyFields, fieldPath) {
376
- const normalizedEntityLookupValue = normalizeEntityLookupValueForSubmit(field, value);
382
+ const normalizedDateValue = normalizeDateOnlyFieldValueForSubmit(field, value);
383
+ const normalizedEntityLookupValue = normalizeEntityLookupValueForSubmit(field, normalizedDateValue);
377
384
  return filterNestedArrayForSubmit(field, normalizedEntityLookupValue, dirtyFields, fieldPath);
378
385
  }
386
+ /**
387
+ * Projects date-only controls into the ISO-8601 representation required by
388
+ * OpenAPI `format: date` / Java `LocalDate` contracts. A date field may hold
389
+ * a native Date (calendar selection) or its locale display value (manual
390
+ * masked input); neither presentation value should leak into the request.
391
+ */
392
+ function normalizeDateOnlyFieldValueForSubmit(field, value) {
393
+ if (!isDateOnlyField(field) || value === null || value === undefined || value === '') {
394
+ return value;
395
+ }
396
+ if (value instanceof Date) {
397
+ return isValidDate(value) ? formatLocalIsoDate(value) : value;
398
+ }
399
+ if (typeof value !== 'string') {
400
+ return value;
401
+ }
402
+ const trimmed = value.trim();
403
+ if (isIsoDate(trimmed)) {
404
+ return trimmed;
405
+ }
406
+ const parsed = parseDayFirstDate(trimmed, field);
407
+ return parsed ? formatLocalIsoDate(parsed) : value;
408
+ }
409
+ function isDateOnlyField(field) {
410
+ const dataType = String(field.dataType ?? '').trim().toLowerCase();
411
+ if (dataType === 'date') {
412
+ return true;
413
+ }
414
+ const controlType = String(field.controlType ?? '').trim().toLowerCase();
415
+ return controlType === 'date' || controlType === 'datepicker' || controlType === 'date_picker';
416
+ }
417
+ function isIsoDate(value) {
418
+ return /^\d{4}-\d{2}-\d{2}$/.test(value);
419
+ }
420
+ function parseDayFirstDate(value, field) {
421
+ const displayFormat = String(field.displayFormat ?? field.mask ?? '').toLowerCase();
422
+ if (!displayFormat.includes('dd') || !displayFormat.includes('mm') || !displayFormat.includes('yyyy')) {
423
+ return null;
424
+ }
425
+ const match = /^(\d{1,2})\/(\d{1,2})\/(\d{4})$/.exec(value);
426
+ if (!match) {
427
+ return null;
428
+ }
429
+ const day = Number(match[1]);
430
+ const month = Number(match[2]);
431
+ const year = Number(match[3]);
432
+ const parsed = new Date(year, month - 1, day);
433
+ return isValidDate(parsed) &&
434
+ parsed.getFullYear() === year &&
435
+ parsed.getMonth() === month - 1 &&
436
+ parsed.getDate() === day
437
+ ? parsed
438
+ : null;
439
+ }
440
+ function isValidDate(value) {
441
+ return !Number.isNaN(value.getTime());
442
+ }
443
+ function formatLocalIsoDate(value) {
444
+ const year = value.getFullYear();
445
+ const month = String(value.getMonth() + 1).padStart(2, '0');
446
+ const day = String(value.getDate()).padStart(2, '0');
447
+ return `${year}-${month}-${day}`;
448
+ }
379
449
  function normalizeEntityLookupValueForSubmit(field, value) {
380
450
  if (!isEntityLookupField(field)) {
381
451
  return value;
@@ -1795,10 +1865,14 @@ class PraxisFormActionsComponent {
1795
1865
  }
1796
1866
  const buttons = [];
1797
1867
  const submitBtn = {
1798
- id: 'submit',
1799
- type: 'submit',
1800
1868
  color: 'primary',
1801
1869
  ...actions.submit,
1870
+ // `actions.submit` is the reserved submit slot. Its visual configuration
1871
+ // may be customized, but its runtime identity must remain canonical so a
1872
+ // persisted editorial id cannot turn a submit button into a no-op custom
1873
+ // action while its native submit event is prevented below.
1874
+ id: 'submit',
1875
+ type: 'submit',
1802
1876
  visible: actions.submit?.visible ?? actions.showSaveButton !== false,
1803
1877
  label: actions.submitButtonLabel || actions.submit?.label || 'ENVIAR',
1804
1878
  };
@@ -2008,6 +2082,8 @@ class PraxisFormActionsComponent {
2008
2082
  });
2009
2083
  }
2010
2084
  resolveButtonActionId(button) {
2085
+ if (button.type === 'submit')
2086
+ return 'submit';
2011
2087
  const id = button.id?.trim();
2012
2088
  if (id === 'submit' || id === 'cancel' || id === 'reset')
2013
2089
  return id;
@@ -7168,6 +7244,36 @@ class FormAiAdapter extends BaseAiAdapter {
7168
7244
  filledFields: filledCount,
7169
7245
  };
7170
7246
  }
7247
+ getDataProfile() {
7248
+ const config = this.host.config;
7249
+ const actions = config?.actions;
7250
+ return {
7251
+ sectionCount: config?.sections?.length ?? 0,
7252
+ fieldCount: config?.fieldMetadata?.length ?? 0,
7253
+ ruleCount: config?.formRules?.length ?? 0,
7254
+ actionCount: [
7255
+ actions?.submit,
7256
+ actions?.cancel,
7257
+ actions?.reset,
7258
+ ...(actions?.custom ?? []),
7259
+ ].filter(Boolean).length,
7260
+ };
7261
+ }
7262
+ getSchemaFields() {
7263
+ return (this.host.config?.fieldMetadata ?? [])
7264
+ .map((field) => ({
7265
+ name: field.name,
7266
+ label: field.label,
7267
+ type: field.dataType ?? field.controlType,
7268
+ controlType: field.controlType,
7269
+ source: field.source ?? 'schema',
7270
+ required: field.required === true,
7271
+ readonly: field.readOnly === true,
7272
+ disabled: field.disabled === true,
7273
+ hidden: field.hidden === true,
7274
+ }))
7275
+ .filter((field) => typeof field.name === 'string' && !!field.name.trim());
7276
+ }
7171
7277
  getAuthoringContext() {
7172
7278
  const manifest = PRAXIS_DYNAMIC_FORM_AUTHORING_MANIFEST;
7173
7279
  return {
@@ -10442,7 +10548,7 @@ class PraxisDynamicForm {
10442
10548
  enterpriseRuntimeContext;
10443
10549
  i18n = inject(PraxisI18nService);
10444
10550
  injector = inject(Injector);
10445
- aiApi = inject(AiBackendApiService);
10551
+ agenticTurnClient = inject(AgenticAuthoringTurnClientService);
10446
10552
  assistantSessions = inject(PraxisAssistantSessionRegistryService);
10447
10553
  aiTurnOrchestrator = inject(PraxisAssistantTurnOrchestratorService);
10448
10554
  jsonLogic = inject(PraxisJsonLogicService);
@@ -10630,7 +10736,16 @@ class PraxisDynamicForm {
10630
10736
  : this.presentationModeGlobal === true;
10631
10737
  }
10632
10738
  getRenderableFieldsForLoader(fields) {
10633
- return fields.map((field) => this.materializeFieldPresentationPolicy(field));
10739
+ return fields.map((field) => {
10740
+ const materialized = this.materializeFieldPresentationPolicy(field);
10741
+ if (!this.apiUrlEntry || materialized.apiUrlEntry === this.apiUrlEntry) {
10742
+ return materialized;
10743
+ }
10744
+ // Dynamic field controls own their CRUD clients. Carry the resolved entry in the
10745
+ // runtime-only metadata so option sources opened in detached hosts (dialogs,
10746
+ // drawers, overlays) resolve against the same API as the form itself.
10747
+ return { ...materialized, apiUrlEntry: this.apiUrlEntry };
10748
+ });
10634
10749
  }
10635
10750
  materializeFieldPresentationPolicy(field) {
10636
10751
  let next = null;
@@ -10808,6 +10923,10 @@ class PraxisDynamicForm {
10808
10923
  entityHydrationPending = false;
10809
10924
  schemaCache = null;
10810
10925
  lastSchemaMeta;
10926
+ schemaMetaPersistenceQueues = new Map();
10927
+ formConfigPersistenceQueues = new Map();
10928
+ formConfigPersistenceGenerations = new Map();
10929
+ formConfigConflictBlockThrough = new Map();
10811
10930
  schemaRootHooks;
10812
10931
  destroy$ = new Subject();
10813
10932
  formValueChangesSubscription = null;
@@ -11999,11 +12118,12 @@ class PraxisDynamicForm {
11999
12118
  persistFormConfig(key, cfg) {
12000
12119
  if (!key) {
12001
12120
  this.warnMissingId();
12002
- return;
12121
+ return Promise.resolve();
12003
12122
  }
12004
12123
  const toSave = this.removeEmptyContainersOnSave
12005
12124
  ? this.dynamicLayout.pruneEmptyContainers(cfg).config
12006
12125
  : cfg;
12126
+ const snapshot = this.cloneFormConfigForPatch(toSave);
12007
12127
  try {
12008
12128
  const sections = Array.isArray(toSave.sections) ? toSave.sections.length : 0;
12009
12129
  const fields = Array.isArray(toSave.fieldMetadata) ? toSave.fieldMetadata.length : 0;
@@ -12013,11 +12133,50 @@ class PraxisDynamicForm {
12013
12133
  this.debugLog('[PDF] persistFormConfig: begin', { key, sections, fields, fieldNames });
12014
12134
  }
12015
12135
  catch { }
12016
- this.asyncConfigStorage.saveConfig(key, toSave).subscribe();
12017
- try {
12018
- this.debugLog('[PDF] persistFormConfig: done', { key });
12019
- }
12020
- catch { }
12136
+ const generation = (this.formConfigPersistenceGenerations.get(key) ?? 0) + 1;
12137
+ this.formConfigPersistenceGenerations.set(key, generation);
12138
+ const previous = this.formConfigPersistenceQueues.get(key) ?? Promise.resolve();
12139
+ const pending = previous
12140
+ .catch(() => undefined)
12141
+ .then(async () => {
12142
+ if (generation <= (this.formConfigConflictBlockThrough.get(key) ?? 0))
12143
+ return;
12144
+ try {
12145
+ await firstValueFrom(this.asyncConfigStorage.saveConfig(key, snapshot));
12146
+ try {
12147
+ this.debugLog('[PDF] persistFormConfig: done', { key });
12148
+ }
12149
+ catch { }
12150
+ }
12151
+ catch (error) {
12152
+ const status = Number(error?.status);
12153
+ if (status !== 409 && status !== 412) {
12154
+ this.warnLog('[PDF] persistFormConfig: not acknowledged', error);
12155
+ return;
12156
+ }
12157
+ this.formConfigConflictBlockThrough.set(key, this.formConfigPersistenceGenerations.get(key) ?? generation);
12158
+ const remote = await firstValueFrom(this.asyncConfigStorage.loadConfig(key))
12159
+ .catch(() => null);
12160
+ const source = String(snapshot.metadata?.source || '').trim().toLowerCase();
12161
+ if (remote && source === 'default') {
12162
+ this.config = normalizeFormConfig$1(remote);
12163
+ this.columnFieldsCache.clear();
12164
+ this.cdr.markForCheck();
12165
+ }
12166
+ this.warnLog('[PDF] persistFormConfig: concurrent version preserved', {
12167
+ key,
12168
+ status,
12169
+ resolution: remote ? 'remote-reloaded' : 'remote-unavailable',
12170
+ });
12171
+ }
12172
+ });
12173
+ this.formConfigPersistenceQueues.set(key, pending);
12174
+ void pending.finally(() => {
12175
+ if (this.formConfigPersistenceQueues.get(key) === pending) {
12176
+ this.formConfigPersistenceQueues.delete(key);
12177
+ }
12178
+ });
12179
+ return pending;
12021
12180
  }
12022
12181
  async createDefaultConfig() {
12023
12182
  try {
@@ -14562,6 +14721,11 @@ class PraxisDynamicForm {
14562
14721
  }
14563
14722
  }
14564
14723
  getFormActionEventId(button) {
14724
+ // Native submit buttons are reserved form semantics. Do not let an
14725
+ // editorial/persisted id reroute them to customAction after the browser
14726
+ // submit event has been intentionally prevented by praxis-form-actions.
14727
+ if (button.type === 'submit')
14728
+ return 'submit';
14565
14729
  const id = button.id?.trim();
14566
14730
  if (id === 'submit' || id === 'cancel' || id === 'reset')
14567
14731
  return id;
@@ -14933,11 +15097,11 @@ class PraxisDynamicForm {
14933
15097
  initializeAiAssistantController() {
14934
15098
  if (this.aiAssistantController)
14935
15099
  return;
14936
- import('./praxisui-dynamic-form-dynamic-form-agentic-authoring-turn-flow-CmFfx-VR.mjs')
15100
+ import('./praxisui-dynamic-form-dynamic-form-agentic-authoring-turn-flow-w9V0aQ9B.mjs')
14937
15101
  .then(({ DynamicFormAgenticAuthoringTurnFlow }) => {
14938
15102
  if (this.aiAssistantController)
14939
15103
  return;
14940
- const flow = new DynamicFormAgenticAuthoringTurnFlow(this.aiAdapter, this.aiApi);
15104
+ const flow = new DynamicFormAgenticAuthoringTurnFlow(this.aiAdapter, this.agenticTurnClient);
14941
15105
  const controller = this.aiTurnOrchestrator.createController(flow, {
14942
15106
  componentId: this.aiAdapter.componentId || 'praxis-dynamic-form',
14943
15107
  componentType: this.aiAdapter.componentType || 'form',
@@ -15375,28 +15539,61 @@ class PraxisDynamicForm {
15375
15539
  .then((value) => value ?? undefined)
15376
15540
  .catch(() => undefined);
15377
15541
  }
15542
+ persistSchemaMeta(key, candidate) {
15543
+ return this.enqueueSchemaMetaPersistence(key, candidate, (stored, pending) => {
15544
+ const current = stored && typeof stored === 'object' && !Array.isArray(stored)
15545
+ ? stored
15546
+ : {};
15547
+ const currentTimestamp = Date.parse(String(current['lastVerifiedAt'] || '')) || 0;
15548
+ const pendingTimestamp = Date.parse(String(pending['lastVerifiedAt'] || '')) || 0;
15549
+ return currentTimestamp > pendingTimestamp
15550
+ ? { ...pending, ...current }
15551
+ : { ...current, ...pending };
15552
+ });
15553
+ }
15554
+ enqueueSchemaMetaPersistence(key, candidate, merge) {
15555
+ const previous = this.schemaMetaPersistenceQueues.get(key) ?? Promise.resolve();
15556
+ const pending = previous
15557
+ .catch(() => undefined)
15558
+ .then(() => this.reconcileSchemaMetaPersistence(key, candidate, merge));
15559
+ this.schemaMetaPersistenceQueues.set(key, pending);
15560
+ void pending.finally(() => {
15561
+ if (this.schemaMetaPersistenceQueues.get(key) === pending) {
15562
+ this.schemaMetaPersistenceQueues.delete(key);
15563
+ }
15564
+ });
15565
+ return pending;
15566
+ }
15567
+ async reconcileSchemaMetaPersistence(key, candidate, merge) {
15568
+ for (let attempt = 0; attempt < 3; attempt += 1) {
15569
+ const stored = await firstValueFrom(this.asyncConfigStorage.loadConfig(key))
15570
+ .catch(() => null);
15571
+ const reconciled = merge(stored, candidate);
15572
+ if (JSON.stringify(stored) === JSON.stringify(reconciled))
15573
+ return;
15574
+ try {
15575
+ await firstValueFrom(this.asyncConfigStorage.saveConfig(key, reconciled));
15576
+ return;
15577
+ }
15578
+ catch (error) {
15579
+ const status = Number(error?.status);
15580
+ if ((status !== 409 && status !== 412) || attempt === 2)
15581
+ return;
15582
+ }
15583
+ }
15584
+ }
15378
15585
  rememberSchemaMetaContext(schemaId) {
15379
15586
  const id = String(schemaId || '').trim();
15380
15587
  if (!id)
15381
- return;
15588
+ return Promise.resolve();
15382
15589
  const indexKey = this.getSchemaMetaIndexKey();
15383
15590
  if (!indexKey)
15384
- return;
15385
- this.asyncConfigStorage
15386
- .loadConfig(indexKey)
15387
- .pipe(take(1))
15388
- .subscribe({
15389
- next: (stored) => {
15390
- const unique = new Set((Array.isArray(stored) ? stored : [])
15391
- .map((item) => String(item || '').trim())
15392
- .filter((item) => !!item));
15393
- unique.add(id);
15394
- this.asyncConfigStorage
15395
- .saveConfig(indexKey, Array.from(unique))
15396
- .pipe(take(1))
15397
- .subscribe({ error: () => { } });
15398
- },
15399
- error: () => { },
15591
+ return Promise.resolve();
15592
+ return this.enqueueSchemaMetaPersistence(indexKey, [id], (stored, pending) => {
15593
+ const unique = new Set([...(Array.isArray(stored) ? stored : []), ...pending]
15594
+ .map((item) => String(item || '').trim())
15595
+ .filter((item) => !!item));
15596
+ return Array.from(unique);
15400
15597
  });
15401
15598
  }
15402
15599
  isSameSchemaContext(left, right) {
@@ -15661,8 +15858,8 @@ class PraxisDynamicForm {
15661
15858
  metaToSave.serverHash = previousHash;
15662
15859
  }
15663
15860
  if (res.status === 304) {
15664
- this.asyncConfigStorage.saveConfig(scopedMetaKey, metaToSave).subscribe();
15665
- this.rememberSchemaMetaContext(currentSchemaId);
15861
+ await this.persistSchemaMeta(scopedMetaKey, metaToSave);
15862
+ await this.rememberSchemaMetaContext(currentSchemaId);
15666
15863
  // Mirror meta in memory (config.metadata) without persisting config
15667
15864
  try {
15668
15865
  const mem = { ...(this.config.metadata || {}) };
@@ -15685,8 +15882,8 @@ class PraxisDynamicForm {
15685
15882
  // status === 200: server hash changed (or first verification without hash). Do not apply schema here.
15686
15883
  const newHash = res.schemaHash;
15687
15884
  metaToSave.serverHash = newHash;
15688
- this.asyncConfigStorage.saveConfig(scopedMetaKey, metaToSave).subscribe();
15689
- this.rememberSchemaMetaContext(currentSchemaId);
15885
+ await this.persistSchemaMeta(scopedMetaKey, metaToSave);
15886
+ await this.rememberSchemaMetaContext(currentSchemaId);
15690
15887
  // Mirror meta in memory (config.metadata) without persisting config
15691
15888
  try {
15692
15889
  const mem = { ...(this.config.metadata || {}) };
@@ -18902,16 +19099,13 @@ class PraxisDynamicForm {
18902
19099
  this.getSchemaMetaKey();
18903
19100
  if (metaKey) {
18904
19101
  const nowIso = new Date().toISOString();
18905
- this.asyncConfigStorage
18906
- .saveConfig(metaKey, {
19102
+ await this.persistSchemaMeta(metaKey, {
18907
19103
  schemaId,
18908
19104
  schemaContext,
18909
19105
  serverHash: entry.schemaHash,
18910
19106
  lastVerifiedAt: nowIso,
18911
- })
18912
- .pipe(take(1))
18913
- .subscribe({ error: () => { } });
18914
- this.rememberSchemaMetaContext(schemaId);
19107
+ });
19108
+ await this.rememberSchemaMetaContext(schemaId);
18915
19109
  }
18916
19110
  }
18917
19111
  catch { }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@praxisui/dynamic-form",
3
- "version": "9.0.0-beta.73",
3
+ "version": "9.0.0-beta.75",
4
4
  "description": "Angular dynamic form engine for Praxis UI: metadata-driven forms, hooks, and services integrating @praxisui/* packages.",
5
5
  "peerDependencies": {
6
6
  "@angular/common": "^21.0.0",
@@ -9,13 +9,13 @@
9
9
  "@angular/forms": "^21.0.0",
10
10
  "@angular/material": "^21.0.0",
11
11
  "@angular/router": "^21.0.0",
12
- "@praxisui/ai": "^9.0.0-beta.73",
13
- "@praxisui/dynamic-fields": "^9.0.0-beta.73",
14
- "@praxisui/metadata-editor": "^9.0.0-beta.73",
15
- "@praxisui/rich-content": "^9.0.0-beta.73",
16
- "@praxisui/settings-panel": "^9.0.0-beta.73",
17
- "@praxisui/visual-builder": "^9.0.0-beta.73",
18
- "@praxisui/core": "^9.0.0-beta.73",
12
+ "@praxisui/ai": "^9.0.0-beta.75",
13
+ "@praxisui/dynamic-fields": "^9.0.0-beta.75",
14
+ "@praxisui/metadata-editor": "^9.0.0-beta.75",
15
+ "@praxisui/rich-content": "^9.0.0-beta.75",
16
+ "@praxisui/settings-panel": "^9.0.0-beta.75",
17
+ "@praxisui/visual-builder": "^9.0.0-beta.75",
18
+ "@praxisui/core": "^9.0.0-beta.75",
19
19
  "rxjs": "^7.8.0"
20
20
  },
21
21
  "dependencies": {
@@ -24,6 +24,7 @@ source_of_truth:
24
24
  - "projects/praxis-dynamic-form/src/lib/ai/form-ai.adapter.ts"
25
25
  - "projects/praxis-ai/src/lib/core/models/assistant-context.models.ts"
26
26
  - "projects/praxis-ai/src/lib/core/services/assistant-session-registry.service.ts"
27
+ - "projects/praxis-ai/src/lib/core/services/agentic-authoring-turn-client.service.ts"
27
28
  - "projects/praxis-dynamic-form/src/lib/config-editor/praxis-dynamic-form-config-editor.ts"
28
29
  - "projects/praxis-dynamic-form/src/lib/utils/prepare-submit-payload.ts"
29
30
  - "projects/praxis-dynamic-fields/src/lib/directives/dynamic-field-loader.directive.ts"
@@ -34,8 +35,8 @@ source_of_truth:
34
35
  - "projects/praxis-core/src/lib/services/dynamic-form.service.ts"
35
36
  - "projects/praxis-core/src/lib/models/form/rule-property.schema.ts"
36
37
  - "projects/praxis-dynamic-form/docs/layout-items-visual-blocks.md"
37
- source_of_truth_last_verified: "2026-05-02"
38
- last_updated: "2026-05-02"
38
+ source_of_truth_last_verified: "2026-07-16"
39
+ last_updated: "2026-07-16"
39
40
  toc: true
40
41
  sidebar: true
41
42
  tags:
@@ -82,6 +83,8 @@ Contrato operacional:
82
83
  - a identidade canonica da sessao segue `form:{routeKey}:{componentInstanceId || formId || 'form'}`;
83
84
  - o contexto assistivel publico e `PraxisAssistantContextSnapshot`, com `identity`, `target`, `contextItems`, `authoringManifestRef`, `resourcePath`, `schemaFields`, digests seguros e hints de governanca;
84
85
  - o snapshot pode apontar para `field`, `schemaBackedField`, `localField`, `section`, `row`, `column`, `visualBlock`, `formAction`, `message` ou `rule`, mas nao deve transportar `form.value`, `rawFormData`, config completa, notas privadas, estado runtime bruto, diagnostics arbitrarios, `pendingPatch`, `File`, `Blob`, bytes, base64 ou `previewUrl`;
86
+ - turnos do assistente usam `AgenticAuthoringTurnClientService` e `/api/praxis/config/ai/authoring/turn/stream/**`; indisponibilidade do transporte falha de forma fechada e nao reabre o caminho legado `getPatch`;
87
+ - o grounding do turno pode incluir `schemaFields` e `dataProfile` estruturais produzidos pelo adapter, mas nunca valores dos controles; anexos sao reduzidos a nome, tipo, MIME, tamanho, origem e indicador de preview;
85
88
  - edicao local so pode ser aplicada quando a resposta for compilada para `componentEditPlan` validado pelo manifesto autoral do componente;
86
89
  - patches livres retornados por backend/LLM sem plano compilado sao rejeitados pelo turn flow;
87
90
  - pedidos sobre regra de negocio compartilhada, elegibilidade, compliance, LGPD, politica, acesso, aprovacao, publicacao, materializacao ou enforcement devem virar handoff governado para `domain-rules`, nao patch local de formulario;
@@ -486,6 +486,8 @@ declare class FormAiAdapter extends BaseAiAdapter<FormConfig> {
486
486
  getCapabilities(): AiCapability[];
487
487
  getTaskPresets(): Record<string, string[]>;
488
488
  getRuntimeState(): Record<string, any>;
489
+ getDataProfile(): Record<string, any>;
490
+ getSchemaFields(): Record<string, any>[];
489
491
  getAuthoringContext(): Record<string, any>;
490
492
  compileAiResponse(response: Record<string, unknown>): AiResponseCompileResult | null;
491
493
  createSnapshot(): FormConfig;
@@ -692,7 +694,7 @@ declare class PraxisDynamicForm implements OnInit, OnChanges, OnDestroy {
692
694
  private enterpriseRuntimeContext?;
693
695
  private readonly i18n;
694
696
  private readonly injector;
695
- private readonly aiApi;
697
+ private readonly agenticTurnClient;
696
698
  private readonly assistantSessions;
697
699
  private readonly aiTurnOrchestrator;
698
700
  private readonly jsonLogic;
@@ -871,6 +873,10 @@ declare class PraxisDynamicForm implements OnInit, OnChanges, OnDestroy {
871
873
  protected entityHydrationPending: boolean;
872
874
  private schemaCache;
873
875
  private lastSchemaMeta?;
876
+ private readonly schemaMetaPersistenceQueues;
877
+ private readonly formConfigPersistenceQueues;
878
+ private readonly formConfigPersistenceGenerations;
879
+ private readonly formConfigConflictBlockThrough;
874
880
  private schemaRootHooks?;
875
881
  private destroy$;
876
882
  private formValueChangesSubscription;
@@ -1230,6 +1236,9 @@ declare class PraxisDynamicForm implements OnInit, OnChanges, OnDestroy {
1230
1236
  private getSchemaMetaKeyForSchemaId;
1231
1237
  private getSchemaMetaKeyForContext;
1232
1238
  private loadStoredSchemaMeta;
1239
+ private persistSchemaMeta;
1240
+ private enqueueSchemaMetaPersistence;
1241
+ private reconcileSchemaMetaPersistence;
1233
1242
  private rememberSchemaMetaContext;
1234
1243
  private isSameSchemaContext;
1235
1244
  private isStoredSchemaMetaForContext;