@praxisui/dynamic-form 8.0.0-beta.1 → 8.0.0-beta.11

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.
@@ -23,7 +23,7 @@ import { MatBadgeModule } from '@angular/material/badge';
23
23
  import { firstValueFrom, BehaviorSubject, Subject, throwError, debounceTime as debounceTime$1, takeUntil as takeUntil$1 } from 'rxjs';
24
24
  import { map, take, takeUntil, timeout, tap, debounceTime, finalize } from 'rxjs/operators';
25
25
  import * as i1$2 from '@praxisui/core';
26
- import { PraxisI18nService, PraxisIconDirective, providePraxisI18nConfig, RULE_PROPERTY_SCHEMA, FIELD_METADATA_CAPABILITIES, deepMerge, createDefaultFormConfig, normalizeFormConfig as normalizeFormConfig$1, ensureIds, createEmptyRichContentDocument, ASYNC_CONFIG_STORAGE, migrateFormLayoutRule, LoggerService, createCorporateLoggerConfig, ConsoleLoggerSink, ResourceQuickConnectComponent, composeHeadersWithVersion, buildApiUrl, mapFieldDefinitionsToMetadata, reconcileFormConfig, syncWithServerMetadata, PRAXIS_LOADING_CTX, normalizeControlTypeKey, resolveSpan, resolveOffset, resolveOrder, resolveHidden, buildSchemaId, fetchWithETag, resolveControlTypeAlias, getTextTransformer, MemoryCacheAdapter, LocalStorageCacheAdapter, SchemaMetadataClient, CONNECTION_STORAGE, API_URL, PRAXIS_LOADING_RENDERER, FORM_HOOKS_PRESETS, EmptyStateCardComponent, isValidFormConfig, ComponentMetadataRegistry, FieldControlType, GLOBAL_ACTION_CATALOG, GLOBAL_ACTION_SPEC_CATALOG, PRAXIS_GLOBAL_ACTION_CATALOG, getGlobalActionCatalog, SURFACE_OPEN_I18N_NAMESPACE, getGlobalActionUiSchema, SurfaceOpenActionEditorComponent, SURFACE_OPEN_I18N_CONFIG, PraxisJsonLogicService } from '@praxisui/core';
26
+ import { PraxisI18nService, PraxisIconDirective, providePraxisI18nConfig, RULE_PROPERTY_SCHEMA, FIELD_METADATA_CAPABILITIES, deepMerge, createDefaultFormConfig, normalizeFormConfig as normalizeFormConfig$1, ensureIds, createEmptyRichContentDocument, ASYNC_CONFIG_STORAGE, migrateFormLayoutRule, LoggerService, createCorporateLoggerConfig, ConsoleLoggerSink, ResourceQuickConnectComponent, composeHeadersWithVersion, buildApiUrl, mapFieldDefinitionsToMetadata, reconcileFormConfig, syncWithServerMetadata, PRAXIS_LOADING_CTX, normalizeControlTypeKey, resolveSpan, resolveOffset, resolveOrder, resolveHidden, 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, PraxisJsonLogicService } from '@praxisui/core';
27
27
  import * as i1 from '@praxisui/dynamic-fields';
28
28
  import { getControlTypeCatalog, ConfirmDialogComponent, DynamicFieldLoaderDirective } from '@praxisui/dynamic-fields';
29
29
  import { BaseAiAdapter, PraxisAiAssistantComponent } from '@praxisui/ai';
@@ -179,21 +179,11 @@ function prepareSubmitPayload(rawValue, fieldMetadata, options = {}) {
179
179
  if (!normalized || typeof normalized !== 'object' || Array.isArray(normalized)) {
180
180
  return normalized;
181
181
  }
182
- const payload = { ...normalized };
183
182
  const dirtyFields = new Set(options.dirtyFields || []);
184
- for (const field of fieldMetadata || []) {
185
- if (shouldOmitFieldFromSubmit(field, dirtyFields)) {
186
- delete payload[field.name];
187
- continue;
188
- }
189
- if (shouldOmitEmptyOptionalField(field, payload[field.name], dirtyFields)) {
190
- delete payload[field.name];
191
- }
192
- }
193
- return payload;
183
+ return filterRecordForSubmit(normalized, fieldMetadata || [], dirtyFields);
194
184
  }
195
- function shouldOmitEmptyOptionalField(field, value, dirtyFields) {
196
- if (dirtyFields.has(field.name) || isRequiredField(field)) {
185
+ function shouldOmitEmptyOptionalField(field, value, dirtyFields, scopePath) {
186
+ if (isFieldDirty(field.name, dirtyFields, scopePath) || isRequiredField(field)) {
197
187
  return false;
198
188
  }
199
189
  return value === null || value === undefined || value === '';
@@ -202,25 +192,71 @@ function isRequiredField(field) {
202
192
  const validators = field.validators;
203
193
  return field.required === true || validators?.required === true;
204
194
  }
205
- function shouldOmitFieldFromSubmit(field, dirtyFields = new Set()) {
195
+ function shouldOmitFieldFromSubmit(field, dirtyFields = new Set(), scopePath) {
206
196
  const policy = field.submitPolicy;
207
197
  if (policy) {
208
- return shouldOmitByPolicy(policy, field.name, dirtyFields);
198
+ return shouldOmitByPolicy(policy, field.name, dirtyFields, scopePath);
209
199
  }
210
200
  return field.source === 'local' || field.transient === true;
211
201
  }
212
- function shouldOmitByPolicy(policy, fieldName, dirtyFields) {
202
+ function shouldOmitByPolicy(policy, fieldName, dirtyFields, scopePath) {
213
203
  switch (policy) {
214
204
  case 'include':
215
205
  return false;
216
206
  case 'omit':
217
207
  return true;
218
208
  case 'includeWhenDirty':
219
- return !dirtyFields.has(fieldName);
209
+ return !isFieldDirty(fieldName, dirtyFields, scopePath);
220
210
  default:
221
211
  return false;
222
212
  }
223
213
  }
214
+ function filterRecordForSubmit(value, fields, dirtyFields, scopePath) {
215
+ const payload = { ...value };
216
+ for (const field of fields) {
217
+ if (shouldOmitFieldFromSubmit(field, dirtyFields, scopePath)) {
218
+ delete payload[field.name];
219
+ continue;
220
+ }
221
+ const fieldValue = payload[field.name];
222
+ const fieldPath = buildScopedFieldPath(field.name, scopePath);
223
+ payload[field.name] = filterNestedArrayForSubmit(field, fieldValue, dirtyFields, fieldPath);
224
+ if (shouldOmitEmptyOptionalField(field, payload[field.name], dirtyFields, scopePath)) {
225
+ delete payload[field.name];
226
+ }
227
+ }
228
+ return payload;
229
+ }
230
+ function filterNestedArrayForSubmit(field, value, dirtyFields, fieldPath) {
231
+ const itemFields = field.array?.itemSchema?.fields;
232
+ if (!Array.isArray(value) || !itemFields?.length) {
233
+ return value;
234
+ }
235
+ return value.map((item, index) => {
236
+ if (!item || typeof item !== 'object' || Array.isArray(item)) {
237
+ return item;
238
+ }
239
+ return filterRecordForSubmit(item, itemFields, dirtyFields, `${fieldPath}.${index}`);
240
+ });
241
+ }
242
+ function isFieldDirty(fieldName, dirtyFields, scopePath) {
243
+ if (dirtyFields.has(fieldName)) {
244
+ return true;
245
+ }
246
+ if (!scopePath) {
247
+ return false;
248
+ }
249
+ return (dirtyFields.has(scopePath) ||
250
+ dirtyFields.has(buildScopedFieldPath(fieldName, scopePath)) ||
251
+ dirtyFields.has(buildBracketScopedFieldPath(fieldName, scopePath)));
252
+ }
253
+ function buildScopedFieldPath(fieldName, scopePath) {
254
+ return scopePath ? `${scopePath}.${fieldName}` : fieldName;
255
+ }
256
+ function buildBracketScopedFieldPath(fieldName, scopePath) {
257
+ const bracketPath = scopePath.replace(/\.(\d+)(?=\.|$)/g, '[$1]');
258
+ return `${bracketPath}.${fieldName}`;
259
+ }
224
260
 
225
261
  const SECTION_HEADER_INITIALS_MIN = 1;
226
262
  const SECTION_HEADER_INITIALS_MAX = 4;
@@ -717,7 +753,7 @@ class PraxisFormActionsComponent {
717
753
  event.stopPropagation();
718
754
  return;
719
755
  }
720
- const actionId = button.id || button.action;
756
+ const actionId = this.resolveButtonActionId(button);
721
757
  if (!actionId)
722
758
  return;
723
759
  if (button.type === 'submit') {
@@ -784,7 +820,7 @@ class PraxisFormActionsComponent {
784
820
  return this.invalidSubmitHintCacheValue;
785
821
  }
786
822
  applyOverrides(button) {
787
- const actionId = button.id || button.action;
823
+ const actionId = this.resolveButtonActionId(button);
788
824
  const override = actionId ? this.actionOverrides?.[actionId] : undefined;
789
825
  if (!override)
790
826
  return button;
@@ -821,7 +857,7 @@ class PraxisFormActionsComponent {
821
857
  return;
822
858
  const dispose = this.shortcuts.registerShortcut(shortcut, {
823
859
  callback: () => {
824
- const actionId = btn.id || btn.action;
860
+ const actionId = this.resolveButtonActionId(btn);
825
861
  if (!actionId)
826
862
  return;
827
863
  this.action.emit({
@@ -836,6 +872,15 @@ class PraxisFormActionsComponent {
836
872
  this.shortcutDisposers.push(dispose);
837
873
  });
838
874
  }
875
+ resolveButtonActionId(button) {
876
+ const id = button.id?.trim();
877
+ if (id === 'submit' || id === 'cancel' || id === 'reset')
878
+ return id;
879
+ return (button.action?.trim() ||
880
+ id ||
881
+ button.globalAction?.actionId?.trim() ||
882
+ undefined);
883
+ }
839
884
  isSecondaryButton(button) {
840
885
  const actionId = (button.id || button.action || '').toLowerCase();
841
886
  return actionId === 'cancel' || actionId === 'reset';
@@ -6911,9 +6956,37 @@ class PraxisDynamicForm {
6911
6956
  };
6912
6957
  }
6913
6958
  getDirtyFieldNames() {
6914
- return Object.entries(this.form.controls)
6915
- .filter(([, control]) => control.dirty)
6916
- .map(([fieldName]) => fieldName);
6959
+ const dirtyFields = new Set();
6960
+ for (const [fieldName, control] of Object.entries(this.form.controls)) {
6961
+ if (control.dirty) {
6962
+ dirtyFields.add(fieldName);
6963
+ }
6964
+ this.collectDirtyFieldNames(control, fieldName, dirtyFields);
6965
+ }
6966
+ return Array.from(dirtyFields);
6967
+ }
6968
+ collectDirtyFieldNames(control, path, dirtyFields) {
6969
+ const childControls = control.controls;
6970
+ if (!childControls) {
6971
+ return;
6972
+ }
6973
+ if (Array.isArray(childControls)) {
6974
+ childControls.forEach((childControl, index) => {
6975
+ const childPath = `${path}.${index}`;
6976
+ if (childControl.dirty) {
6977
+ dirtyFields.add(childPath);
6978
+ }
6979
+ this.collectDirtyFieldNames(childControl, childPath, dirtyFields);
6980
+ });
6981
+ return;
6982
+ }
6983
+ for (const [childName, childControl] of Object.entries(childControls)) {
6984
+ const childPath = `${path}.${childName}`;
6985
+ if (childControl.dirty) {
6986
+ dirtyFields.add(childPath);
6987
+ }
6988
+ this.collectDirtyFieldNames(childControl, childPath, dirtyFields);
6989
+ }
6917
6990
  }
6918
6991
  buildCurrentFormRuntimeContext() {
6919
6992
  const resourcePath = (this.resourcePath || '').trim();
@@ -7357,6 +7430,7 @@ class PraxisDynamicForm {
7357
7430
  const emitAction = () => {
7358
7431
  this.customAction.emit({
7359
7432
  actionId,
7433
+ ...(action.globalAction ? { globalAction: action.globalAction } : {}),
7360
7434
  formData: this.form.value,
7361
7435
  isValid: this.form.valid,
7362
7436
  source: 'section-header',
@@ -7390,7 +7464,10 @@ class PraxisDynamicForm {
7390
7464
  };
7391
7465
  }
7392
7466
  getSectionHeaderActionEventId(action) {
7393
- return action.action?.trim() || action.id.trim();
7467
+ return (action.action?.trim() ||
7468
+ action.id.trim() ||
7469
+ action.globalAction?.actionId?.trim() ||
7470
+ '');
7394
7471
  }
7395
7472
  getSectionHeaderActionRuleKeys(section, action) {
7396
7473
  const logicalId = this.getSectionHeaderActionEventId(action);
@@ -8162,7 +8239,7 @@ class PraxisDynamicForm {
8162
8239
  }
8163
8240
  }
8164
8241
  _executeAction(button) {
8165
- const actionId = button.id || button.action;
8242
+ const actionId = this.getFormActionEventId(button);
8166
8243
  if (!actionId)
8167
8244
  return;
8168
8245
  switch (actionId) {
@@ -8198,7 +8275,8 @@ class PraxisDynamicForm {
8198
8275
  default:
8199
8276
  // This is a custom action
8200
8277
  this.customAction.emit({
8201
- actionId: actionId,
8278
+ actionId,
8279
+ ...(button.globalAction ? { globalAction: button.globalAction } : {}),
8202
8280
  formData: this.form.value,
8203
8281
  isValid: this.form.valid,
8204
8282
  source: 'button',
@@ -8206,6 +8284,15 @@ class PraxisDynamicForm {
8206
8284
  break;
8207
8285
  }
8208
8286
  }
8287
+ getFormActionEventId(button) {
8288
+ const id = button.id?.trim();
8289
+ if (id === 'submit' || id === 'cancel' || id === 'reset')
8290
+ return id;
8291
+ return (button.action?.trim() ||
8292
+ id ||
8293
+ button.globalAction?.actionId?.trim() ||
8294
+ undefined);
8295
+ }
8209
8296
  async onSubmit() {
8210
8297
  if (this.submitting)
8211
8298
  return;
@@ -9082,6 +9169,7 @@ class PraxisDynamicForm {
9082
9169
  materialDesign: fieldMeta.materialDesign,
9083
9170
  clearButton: fieldMeta.clearButton,
9084
9171
  extra: fieldMeta.extra,
9172
+ optionSource: fieldMeta.optionSource,
9085
9173
  controlType,
9086
9174
  };
9087
9175
  // Seed específicos para NUMBER (numericTextBox)
@@ -9843,6 +9931,22 @@ class PraxisDynamicForm {
9843
9931
  });
9844
9932
  return out;
9845
9933
  }
9934
+ mergeMetadataObjectPatch(current, patch) {
9935
+ if (!patch || typeof patch !== 'object' || Array.isArray(patch)) {
9936
+ return patch;
9937
+ }
9938
+ const base = current && typeof current === 'object' && !Array.isArray(current)
9939
+ ? { ...current }
9940
+ : {};
9941
+ Object.entries(patch).forEach(([key, value]) => {
9942
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
9943
+ base[key] = this.mergeMetadataObjectPatch(base[key], value);
9944
+ return;
9945
+ }
9946
+ base[key] = value;
9947
+ });
9948
+ return base;
9949
+ }
9846
9950
  deleteMetadataPath(target, path) {
9847
9951
  if (!target || typeof path !== 'string' || !path.trim())
9848
9952
  return;
@@ -10034,6 +10138,9 @@ class PraxisDynamicForm {
10034
10138
  fm.dataAttributes = patch.dataAttributes;
10035
10139
  }
10036
10140
  }
10141
+ if (patch.optionSource !== undefined) {
10142
+ fm.optionSource = this.mergeMetadataObjectPatch(fm.optionSource, patch.optionSource);
10143
+ }
10037
10144
  // Generic extras passthrough: merge into fm.extra to allow component-specific appearance/behavior
10038
10145
  if (patch.extra !== undefined) {
10039
10146
  const currExtra = (fm.extra && typeof fm.extra === 'object') ? fm.extra : {};
@@ -14878,6 +14985,7 @@ const ACTIONS_EDITOR_I18N_CONFIG = {
14878
14985
  'global.section.essential': 'Essencial',
14879
14986
  'global.section.dialogOptional': 'Diálogo (opcional)',
14880
14987
  'global.validation.validJson': 'Informe um JSON válido.',
14988
+ 'global.validation.requiredField': 'Campo obrigatório.',
14881
14989
  'tabs.defaultButtons': 'Botões padrão',
14882
14990
  'tabs.customButtons': 'Botões customizados',
14883
14991
  'tabs.layout': 'Layout',
@@ -14964,6 +15072,7 @@ const ACTIONS_EDITOR_I18N_CONFIG = {
14964
15072
  'global.section.essential': 'Essential',
14965
15073
  'global.section.dialogOptional': 'Dialog (optional)',
14966
15074
  'global.validation.validJson': 'Provide valid JSON.',
15075
+ 'global.validation.requiredField': 'Required field.',
14967
15076
  'tabs.defaultButtons': 'Default buttons',
14968
15077
  'tabs.customButtons': 'Custom buttons',
14969
15078
  'tabs.layout': 'Layout',
@@ -15051,204 +15160,32 @@ const ACTIONS_EDITOR_I18N_CONFIG = {
15051
15160
 
15052
15161
  function buildGlobalActionCatalog(params) {
15053
15162
  const merged = new Map();
15054
- for (const spec of params.legacySpecs) {
15055
- const mapped = params.mapLegacySpec
15056
- ? params.mapLegacySpec(spec)
15057
- : { ...spec };
15058
- merged.set(mapped.id, mapped);
15059
- }
15060
15163
  const source = params.injectedCatalog.length
15061
15164
  ? params.injectedCatalog
15062
15165
  : params.fallbackCatalog;
15063
15166
  for (const entry of source) {
15064
- if (merged.has(entry.id))
15065
- continue;
15066
15167
  const mapped = params.mapCatalogEntry
15067
15168
  ? params.mapCatalogEntry(entry)
15068
15169
  : {
15069
15170
  id: entry.id,
15070
15171
  label: entry.label,
15071
15172
  description: entry.description,
15173
+ payloadSchema: entry.payloadSchema,
15072
15174
  };
15073
15175
  merged.set(mapped.id, mapped);
15074
15176
  }
15075
15177
  return Array.from(merged.values());
15076
15178
  }
15077
- function parseActionValue(value, catalog, customActionValue = '__custom__') {
15078
- const raw = String(value || '').trim();
15079
- if (!raw) {
15080
- return { id: customActionValue, param: '', isGlobal: false, raw: '' };
15081
- }
15082
- const [prefix, ...rest] = raw.split(':');
15083
- const candidate = catalog.find((item) => item.id === prefix);
15084
- if (candidate) {
15085
- return {
15086
- id: candidate.id,
15087
- param: rest.join(':'),
15088
- isGlobal: true,
15089
- raw,
15090
- };
15091
- }
15092
- const exact = catalog.find((item) => item.id === raw);
15093
- if (exact) {
15094
- return { id: exact.id, param: '', isGlobal: true, raw };
15095
- }
15096
- return { id: customActionValue, param: '', isGlobal: false, raw };
15097
- }
15098
- function getActionParamInfo(value, catalog, customActionValue = '__custom__') {
15099
- const parsed = parseActionValue(value, catalog, customActionValue);
15100
- const param = parsed.param || '';
15101
- const trimmed = param.trim();
15102
- const looksJson = (trimmed.startsWith('{') && trimmed.endsWith('}')) ||
15103
- (trimmed.startsWith('[') && trimmed.endsWith(']'));
15104
- if (looksJson) {
15105
- try {
15106
- const json = JSON.parse(trimmed);
15107
- if (json && typeof json === 'object') {
15108
- return { ...parsed, param, isJson: true, json };
15109
- }
15110
- }
15111
- catch {
15112
- // ignore malformed legacy value
15113
- }
15114
- }
15115
- return { ...parsed, param, isJson: false, json: undefined };
15116
- }
15117
- function normalizeSurfaceOpenPayload(payload) {
15118
- return {
15119
- presentation: payload?.presentation === 'drawer' ? 'drawer' : 'modal',
15120
- title: payload?.title,
15121
- subtitle: payload?.subtitle,
15122
- icon: payload?.icon,
15123
- size: payload?.size && Object.keys(payload.size).length
15124
- ? { ...payload.size }
15125
- : undefined,
15126
- widget: {
15127
- id: String(payload?.widget?.id || ''),
15128
- inputs: { ...(payload?.widget?.inputs || {}) },
15129
- bindingOrder: payload?.widget?.bindingOrder?.length
15130
- ? [...payload.widget.bindingOrder]
15131
- : undefined,
15132
- },
15133
- bindings: payload?.bindings?.length
15134
- ? payload.bindings.map((binding) => ({
15135
- ...binding,
15136
- }))
15137
- : [],
15138
- context: payload?.context &&
15139
- typeof payload.context === 'object' &&
15140
- !Array.isArray(payload.context)
15141
- ? { ...payload.context }
15142
- : undefined,
15143
- };
15144
- }
15145
- function serializeGlobalActionParam(actionId, values, existingJson) {
15146
- const payload = buildGlobalActionPayload(values, existingJson);
15147
- const preferJson = !!(existingJson && Object.keys(existingJson).length);
15148
- const primary = buildPrimaryParam(actionId, values, payload, preferJson);
15149
- if (primary)
15150
- return primary;
15151
- if (!payload || Object.keys(payload).length === 0)
15152
- return '';
15153
- return JSON.stringify(payload);
15154
- }
15155
- function isRequiredParamMissing(value, catalog, customActionValue = '__custom__') {
15156
- const parsed = parseActionValue(value, catalog, customActionValue);
15157
- const spec = catalog.find((item) => item.id === parsed.id);
15158
- if (!parsed.isGlobal || !spec?.param?.required)
15159
- return false;
15160
- return !String(parsed.param || '').trim();
15179
+ function isGlobalActionId(value, catalog) {
15180
+ const id = String(value || '').trim();
15181
+ return !!id && catalog.some((item) => item.id === id);
15161
15182
  }
15162
- function isEquivalentActionFieldValue(actual, draft, fieldType) {
15163
- if (fieldType === 'toggle') {
15164
- return Boolean(actual) === Boolean(draft);
15165
- }
15166
- if (fieldType === 'number') {
15167
- return String(actual ?? '') === String(draft ?? '');
15168
- }
15169
- return String(actual ?? '') === String(draft ?? '');
15170
- }
15171
- function parseMethodAndUrl(raw) {
15172
- const text = String(raw || '').trim();
15173
- if (!text)
15174
- return {};
15175
- const idx = text.indexOf(':');
15176
- if (idx > 0) {
15177
- return {
15178
- method: text.slice(0, idx).toUpperCase(),
15179
- url: text.slice(idx + 1),
15180
- };
15181
- }
15182
- return { url: text };
15183
- }
15184
- function stringifyJson(value) {
15185
- if (value === undefined || value === null || value === '')
15186
- return '';
15187
- if (typeof value === 'string')
15188
- return value;
15189
- try {
15190
- return JSON.stringify(value, null, 2);
15191
- }
15192
- catch {
15193
- return '';
15194
- }
15195
- }
15196
- function buildPrimaryParam(actionId, values, payload, preferJson) {
15197
- if (preferJson)
15198
- return '';
15199
- const onlyPayload = payload ? Object.keys(payload).length : 0;
15200
- const text = (value) => String(value || '').trim();
15201
- if (actionId === 'navigate' || actionId === 'openUrl') {
15202
- const url = text(values.url);
15203
- if (url && onlyPayload === 1)
15204
- return url;
15205
- }
15206
- if (actionId === 'showAlert' ||
15207
- actionId === 'confirm' ||
15208
- actionId === 'alert' ||
15209
- actionId === 'prompt') {
15210
- const message = text(values.message);
15211
- if (message && onlyPayload === 1)
15212
- return message;
15213
- }
15214
- if (actionId === 'download') {
15215
- const url = text(values.url);
15216
- if (url && onlyPayload === 1)
15217
- return url;
15218
- }
15219
- if (actionId === 'copyToClipboard') {
15220
- const copyText = text(values.text);
15221
- if (copyText && onlyPayload === 1)
15222
- return copyText;
15223
- }
15224
- if (actionId === 'saveFormDraft' ||
15225
- actionId === 'loadFormDraft' ||
15226
- actionId === 'clearFormDraft') {
15227
- const key = text(values.key);
15228
- if (key && onlyPayload === 1)
15229
- return key;
15230
- }
15231
- if (actionId === 'refreshOptions' || actionId === 'loadDependentData') {
15232
- const field = text(values.field);
15233
- if (field && onlyPayload === 1)
15234
- return field;
15235
- }
15236
- if (actionId === 'apiCall') {
15237
- const method = text(values.method);
15238
- const url = text(values.url);
15239
- if (url && onlyPayload <= 2) {
15240
- return method ? `${method}:${url}` : url;
15241
- }
15242
- }
15243
- return '';
15244
- }
15245
- function buildGlobalActionPayload(values, existingJson) {
15183
+ function normalizeGlobalActionPayload(values, existingPayload) {
15246
15184
  const payload = {};
15247
15185
  const hasText = (value) => {
15248
15186
  if (value === undefined || value === null)
15249
15187
  return false;
15250
- const text = String(value).trim();
15251
- return text.length > 0;
15188
+ return String(value).trim().length > 0;
15252
15189
  };
15253
15190
  const toText = (value) => hasText(value) ? String(value).trim() : undefined;
15254
15191
  const toNumber = (value) => {
@@ -15277,8 +15214,8 @@ function buildGlobalActionPayload(values, existingJson) {
15277
15214
  payload[key] = true;
15278
15215
  }
15279
15216
  else if (value === false &&
15280
- existingJson &&
15281
- Object.prototype.hasOwnProperty.call(existingJson, key)) {
15217
+ existingPayload &&
15218
+ Object.prototype.hasOwnProperty.call(existingPayload, key)) {
15282
15219
  payload[key] = false;
15283
15220
  }
15284
15221
  };
@@ -15316,101 +15253,55 @@ function buildGlobalActionPayload(values, existingJson) {
15316
15253
  ...(animationDuration !== undefined ? { duration: animationDuration } : {}),
15317
15254
  };
15318
15255
  }
15319
- const width = toText(values.width);
15320
- const height = toText(values.height);
15321
- const minWidth = toText(values.minWidth);
15322
- const maxWidth = toText(values.maxWidth);
15323
- const minHeight = toText(values.minHeight);
15324
- const maxHeight = toText(values.maxHeight);
15325
- if (width)
15326
- payload.width = width;
15327
- if (height)
15328
- payload.height = height;
15329
- if (minWidth)
15330
- payload.minWidth = minWidth;
15331
- if (maxWidth)
15332
- payload.maxWidth = maxWidth;
15333
- if (minHeight)
15334
- payload.minHeight = minHeight;
15335
- if (maxHeight)
15336
- payload.maxHeight = maxHeight;
15256
+ for (const key of ['width', 'height', 'minWidth', 'maxWidth', 'minHeight', 'maxHeight']) {
15257
+ const value = toText(values[key]);
15258
+ if (value)
15259
+ payload[key] = value;
15260
+ }
15337
15261
  includeBool('disableClose', values.disableClose);
15338
15262
  includeBool('hasBackdrop', values.hasBackdrop);
15339
15263
  includeBool('closeOnBackdropClick', values.closeOnBackdropClick);
15340
15264
  includeBool('autoFocus', values.autoFocus);
15341
15265
  includeBool('restoreFocus', values.restoreFocus);
15342
- const backdropClass = toText(values.backdropClass);
15343
- const panelClass = toText(values.panelClass);
15344
- if (backdropClass)
15345
- payload.backdropClass = backdropClass;
15346
- if (panelClass)
15347
- payload.panelClass = panelClass;
15348
- const autoFocusedElement = toText(values.autoFocusedElement);
15349
- if (autoFocusedElement)
15350
- payload.autoFocusedElement = autoFocusedElement;
15351
- const ariaRole = toText(values.ariaRole);
15352
- const ariaLabel = toText(values.ariaLabel);
15353
- const ariaLabelledBy = toText(values.ariaLabelledBy);
15354
- const ariaDescribedBy = toText(values.ariaDescribedBy);
15355
- if (ariaRole)
15356
- payload.ariaRole = ariaRole;
15357
- if (ariaLabel)
15358
- payload.ariaLabel = ariaLabel;
15359
- if (ariaLabelledBy)
15360
- payload.ariaLabelledBy = ariaLabelledBy;
15361
- if (ariaDescribedBy)
15362
- payload.ariaDescribedBy = ariaDescribedBy;
15266
+ for (const key of [
15267
+ 'backdropClass',
15268
+ 'panelClass',
15269
+ 'autoFocusedElement',
15270
+ 'ariaRole',
15271
+ 'ariaLabel',
15272
+ 'ariaLabelledBy',
15273
+ 'ariaDescribedBy',
15274
+ 'confirmLabel',
15275
+ 'cancelLabel',
15276
+ 'okLabel',
15277
+ 'placeholder',
15278
+ 'defaultValue',
15279
+ 'url',
15280
+ 'target',
15281
+ 'level',
15282
+ 'text',
15283
+ ]) {
15284
+ const value = toText(values[key]);
15285
+ if (value)
15286
+ payload[key] = value;
15287
+ }
15363
15288
  const styles = toJson(values.styles);
15364
15289
  if (styles)
15365
15290
  payload.styles = styles;
15366
- const confirmLabel = toText(values.confirmLabel);
15367
- if (confirmLabel)
15368
- payload.confirmLabel = confirmLabel;
15369
- const cancelLabel = toText(values.cancelLabel);
15370
- if (cancelLabel)
15371
- payload.cancelLabel = cancelLabel;
15372
- const okLabel = toText(values.okLabel);
15373
- if (okLabel)
15374
- payload.okLabel = okLabel;
15375
- const placeholder = toText(values.placeholder);
15376
- if (placeholder)
15377
- payload.placeholder = placeholder;
15378
- const defaultValue = toText(values.defaultValue);
15379
- if (defaultValue)
15380
- payload.defaultValue = defaultValue;
15381
- if (values.mode === true) {
15291
+ if (values.mode === true)
15382
15292
  payload.mode = 'dialog';
15383
- }
15384
- const url = toText(values.url);
15385
- if (url)
15386
- payload.url = url;
15387
- const target = toText(values.target);
15388
- if (target)
15389
- payload.target = target;
15390
15293
  includeBool('newTab', values.newTab);
15391
15294
  includeBool('replaceUrl', values.replaceUrl);
15392
- const method = toText(values.method);
15393
- if (method)
15394
- payload.method = method;
15395
- const bodySource = toText(values.bodySource);
15396
- if (bodySource)
15397
- payload.bodySource = bodySource;
15295
+ includeBool('useFieldValue', values.useFieldValue);
15296
+ const params = toJson(values.params);
15297
+ if (params !== undefined)
15298
+ payload.params = params;
15398
15299
  const headers = toJson(values.headers);
15399
- if (headers)
15300
+ if (headers !== undefined)
15400
15301
  payload.headers = headers;
15401
15302
  const body = toJson(values.body);
15402
- if (body)
15303
+ if (body !== undefined)
15403
15304
  payload.body = body;
15404
- const key = toText(values.key);
15405
- if (key)
15406
- payload.key = key;
15407
- const field = toText(values.field);
15408
- if (field)
15409
- payload.field = field;
15410
- const copyText = toText(values.text);
15411
- if (copyText)
15412
- payload.text = copyText;
15413
- includeBool('useFieldValue', values.useFieldValue);
15414
15305
  const contentType = toText(values.contentType);
15415
15306
  if (contentType)
15416
15307
  payload.contentType = contentType;
@@ -15429,6 +15320,65 @@ function buildGlobalActionPayload(values, existingJson) {
15429
15320
  }
15430
15321
  return payload;
15431
15322
  }
15323
+ function buildGlobalActionRef(actionId, values, existingPayload) {
15324
+ const payload = normalizeGlobalActionPayload(values, existingPayload);
15325
+ return Object.keys(payload).length
15326
+ ? { actionId, payload }
15327
+ : { actionId };
15328
+ }
15329
+ function isRequiredParamMissing(ref, catalog) {
15330
+ const spec = catalog.find((item) => item.id === ref?.actionId);
15331
+ return isRequiredGlobalActionPayloadMissing(ref, spec);
15332
+ }
15333
+ function isEquivalentActionFieldValue(actual, draft, fieldType) {
15334
+ if (fieldType === 'toggle') {
15335
+ return Boolean(actual) === Boolean(draft);
15336
+ }
15337
+ if (fieldType === 'number') {
15338
+ return String(actual ?? '') === String(draft ?? '');
15339
+ }
15340
+ return String(actual ?? '') === String(draft ?? '');
15341
+ }
15342
+ function stringifyJson(value) {
15343
+ if (value === undefined || value === null || value === '')
15344
+ return '';
15345
+ if (typeof value === 'string')
15346
+ return value;
15347
+ try {
15348
+ return JSON.stringify(value, null, 2);
15349
+ }
15350
+ catch {
15351
+ return '';
15352
+ }
15353
+ }
15354
+ function normalizeSurfaceOpenPayload(payload) {
15355
+ return {
15356
+ presentation: payload?.presentation === 'drawer' ? 'drawer' : 'modal',
15357
+ title: payload?.title,
15358
+ subtitle: payload?.subtitle,
15359
+ icon: payload?.icon,
15360
+ size: payload?.size && Object.keys(payload.size).length
15361
+ ? { ...payload.size }
15362
+ : undefined,
15363
+ widget: {
15364
+ id: String(payload?.widget?.id || ''),
15365
+ inputs: { ...(payload?.widget?.inputs || {}) },
15366
+ bindingOrder: payload?.widget?.bindingOrder?.length
15367
+ ? [...payload.widget.bindingOrder]
15368
+ : undefined,
15369
+ },
15370
+ bindings: payload?.bindings?.length
15371
+ ? payload.bindings.map((binding) => ({
15372
+ ...binding,
15373
+ }))
15374
+ : [],
15375
+ context: payload?.context &&
15376
+ typeof payload.context === 'object' &&
15377
+ !Array.isArray(payload.context)
15378
+ ? { ...payload.context }
15379
+ : undefined,
15380
+ };
15381
+ }
15432
15382
 
15433
15383
  let ActionsEditorComponent$1 = class ActionsEditorComponent {
15434
15384
  config;
@@ -15438,9 +15388,7 @@ let ActionsEditorComponent$1 = class ActionsEditorComponent {
15438
15388
  customActionStyleErrors = new Map();
15439
15389
  globalActionCatalogSource = inject(GLOBAL_ACTION_CATALOG, { optional: true }) ?? [];
15440
15390
  i18n = inject(PraxisI18nService);
15441
- legacyActionSpecs = GLOBAL_ACTION_SPEC_CATALOG;
15442
15391
  globalActionCatalog = buildGlobalActionCatalog({
15443
- legacySpecs: this.legacyActionSpecs,
15444
15392
  injectedCatalog: getGlobalActionCatalog(this.globalActionCatalogSource),
15445
15393
  fallbackCatalog: PRAXIS_GLOBAL_ACTION_CATALOG,
15446
15394
  mapCatalogEntry: (entry) => this.mapCatalogEntryToActionSpec(entry),
@@ -15529,17 +15477,17 @@ let ActionsEditorComponent$1 = class ActionsEditorComponent {
15529
15477
  getActionSpecById(id) {
15530
15478
  return this.globalActionCatalog.find((item) => item.id === id);
15531
15479
  }
15532
- getCustomActionSelectValue(value) {
15533
- const parsed = parseActionValue(value, this.globalActionCatalog, this.customActionValue);
15534
- return parsed.isGlobal ? parsed.id : this.customActionValue;
15480
+ getCustomActionSelectValue(action) {
15481
+ return action?.globalAction?.actionId || this.customActionValue;
15535
15482
  }
15536
- getCustomActionParam(value) {
15537
- const parsed = parseActionValue(value, this.globalActionCatalog, this.customActionValue);
15538
- return parsed.isGlobal ? parsed.param : '';
15483
+ getCustomActionParam(action) {
15484
+ const payload = action?.globalAction?.payload;
15485
+ if (payload === undefined || payload === null)
15486
+ return '';
15487
+ return typeof payload === 'string' ? payload : stringifyJson(payload);
15539
15488
  }
15540
- getCustomActionCustomValue(value) {
15541
- const parsed = parseActionValue(value, this.globalActionCatalog, this.customActionValue);
15542
- return parsed.isGlobal ? '' : parsed.raw;
15489
+ getCustomActionCustomValue(action) {
15490
+ return action?.globalAction ? '' : action?.action || '';
15543
15491
  }
15544
15492
  onCustomActionSelectChange(index, value) {
15545
15493
  const current = this.actions.custom?.[index];
@@ -15547,31 +15495,36 @@ let ActionsEditorComponent$1 = class ActionsEditorComponent {
15547
15495
  return;
15548
15496
  this.clearActionFieldState(current);
15549
15497
  if (value === this.customActionValue) {
15550
- const parsed = parseActionValue(current.action, this.globalActionCatalog, this.customActionValue);
15551
- if (parsed.isGlobal) {
15552
- this.updateCustomAction(index, 'action', '');
15553
- }
15498
+ this.updateCustomActionRef(index, undefined);
15554
15499
  return;
15555
15500
  }
15556
- const parsed = parseActionValue(current.action, this.globalActionCatalog, this.customActionValue);
15557
- const param = parsed.isGlobal && parsed.id === value ? parsed.param : '';
15558
- this.updateCustomAction(index, 'action', param ? `${value}:${param}` : value);
15501
+ const payload = current.globalAction?.actionId === value ? current.globalAction.payload : undefined;
15502
+ this.updateCustomActionRef(index, payload !== undefined ? { actionId: value, payload } : { actionId: value });
15559
15503
  }
15560
15504
  onCustomActionParamChange(index, value) {
15561
15505
  const current = this.actions.custom?.[index];
15562
15506
  if (!current)
15563
15507
  return;
15564
- const parsed = parseActionValue(current.action, this.globalActionCatalog, this.customActionValue);
15565
- const id = parsed.isGlobal ? parsed.id : '';
15566
- this.updateCustomAction(index, 'action', id ? (value ? `${id}:${value}` : id) : value);
15508
+ const id = current.globalAction?.actionId;
15509
+ if (!id)
15510
+ return;
15511
+ const trimmed = String(value || '').trim();
15512
+ if (!trimmed) {
15513
+ this.updateCustomActionRef(index, { actionId: id });
15514
+ return;
15515
+ }
15516
+ try {
15517
+ this.updateCustomActionRef(index, { actionId: id, payload: JSON.parse(trimmed) });
15518
+ }
15519
+ catch {
15520
+ this.updateCustomActionRef(index, { actionId: id, payload: trimmed });
15521
+ }
15567
15522
  }
15568
15523
  onCustomActionCustomChange(index, value) {
15569
15524
  this.updateCustomAction(index, 'action', value);
15570
15525
  }
15571
- isGlobalActionId(id) {
15572
- if (!id)
15573
- return false;
15574
- return this.globalActionCatalog.some((item) => item.id === id);
15526
+ isCanonicalGlobalActionId(id) {
15527
+ return isGlobalActionId(id, this.globalActionCatalog);
15575
15528
  }
15576
15529
  getActionCatalogLabel(spec) {
15577
15530
  if (spec?.id === 'surface.open') {
@@ -15607,6 +15560,7 @@ let ActionsEditorComponent$1 = class ActionsEditorComponent {
15607
15560
  id: entry.id,
15608
15561
  label: this.getActionCatalogLabel(entry),
15609
15562
  description: this.getActionCatalogDescription(entry),
15563
+ payloadSchema: entry.payloadSchema,
15610
15564
  param: hasPayloadSchema
15611
15565
  ? {
15612
15566
  label: this.tx('globalAction.payload.label', 'Payload (optional JSON)'),
@@ -15620,20 +15574,16 @@ let ActionsEditorComponent$1 = class ActionsEditorComponent {
15620
15574
  };
15621
15575
  }
15622
15576
  getGlobalActionSchema(action) {
15623
- const parsed = parseActionValue(action.action, this.globalActionCatalog, this.customActionValue);
15624
- if (!parsed.isGlobal)
15625
- return undefined;
15626
- return getGlobalActionUiSchema(parsed.id);
15577
+ return getGlobalActionUiSchema(action.globalAction?.actionId);
15627
15578
  }
15628
15579
  getSurfaceOpenActionPayload(action) {
15629
- const info = getActionParamInfo(action.action, this.globalActionCatalog, this.customActionValue);
15630
- const payload = info.isJson && info.json && typeof info.json === 'object' && !Array.isArray(info.json)
15631
- ? info.json
15632
- : undefined;
15633
- return normalizeSurfaceOpenPayload(payload);
15580
+ return normalizeSurfaceOpenPayload(action.globalAction?.payload);
15634
15581
  }
15635
15582
  onSurfaceOpenActionPayloadChange(action, payload, index) {
15636
- this.updateCustomAction(index, 'action', `surface.open:${JSON.stringify(normalizeSurfaceOpenPayload(payload))}`);
15583
+ this.updateCustomActionRef(index, {
15584
+ actionId: 'surface.open',
15585
+ payload: normalizeSurfaceOpenPayload(payload),
15586
+ });
15637
15587
  }
15638
15588
  shouldShowGlobalActionField(action, field) {
15639
15589
  if (!field.dependsOnKey)
@@ -15644,6 +15594,18 @@ let ActionsEditorComponent$1 = class ActionsEditorComponent {
15644
15594
  const values = this.collectGlobalActionFieldValues(action, schema.fields);
15645
15595
  return String(values[field.dependsOnKey] ?? '') === String(field.dependsOnValue ?? '');
15646
15596
  }
15597
+ isGlobalActionFieldRequired(action, field) {
15598
+ const actionId = action.globalAction?.actionId;
15599
+ const spec = this.globalActionCatalog.find((item) => item.id === actionId);
15600
+ return getRequiredGlobalActionPayloadKeys(actionId, spec).includes(field.key);
15601
+ }
15602
+ isGlobalActionFieldMissing(action, field) {
15603
+ if (!this.isGlobalActionFieldRequired(action, field))
15604
+ return false;
15605
+ if (!this.shouldShowGlobalActionField(action, field))
15606
+ return false;
15607
+ return !hasMeaningfulGlobalActionPayloadValue(this.getGlobalActionFieldValue(action, field));
15608
+ }
15647
15609
  hasActionFieldError(action, key) {
15648
15610
  const errors = this.actionFieldErrors.get(this.getActionDraftKey(action));
15649
15611
  return !!errors?.[key];
@@ -15655,48 +15617,19 @@ let ActionsEditorComponent$1 = class ActionsEditorComponent {
15655
15617
  getGlobalActionFieldValue(action, field) {
15656
15618
  const draftKey = this.getActionDraftKey(action);
15657
15619
  const draft = this.actionFieldDrafts.get(draftKey)?.[field.key];
15658
- const info = getActionParamInfo(action.action, this.globalActionCatalog, this.customActionValue);
15659
- const raw = info.param || '';
15660
- const json = info.json || {};
15661
- const hasJson = info.isJson;
15620
+ const json = action.globalAction?.payload || {};
15662
15621
  let value;
15663
15622
  switch (field.key) {
15664
15623
  case 'message':
15665
- value = json.message ?? json.text ?? (hasJson ? '' : raw);
15624
+ value = json.message ?? json.text ?? '';
15666
15625
  break;
15667
15626
  case 'url':
15668
- if (json.url) {
15669
- value = json.url;
15670
- break;
15671
- }
15672
- if (!hasJson && info.id === 'apiCall') {
15673
- const parsed = parseMethodAndUrl(raw);
15674
- value = parsed.url || '';
15675
- break;
15676
- }
15677
- value = hasJson ? '' : raw;
15678
- break;
15679
- case 'method': {
15680
- if (json.method) {
15681
- value = String(json.method);
15682
- break;
15683
- }
15684
- if (!hasJson) {
15685
- const parsed = parseMethodAndUrl(raw);
15686
- value = parsed.method || '';
15687
- break;
15688
- }
15689
- value = '';
15690
- break;
15691
- }
15692
- case 'bodySource':
15693
- value = json.bodySource ?? '';
15627
+ value = json.url ?? '';
15694
15628
  break;
15629
+ case 'params':
15695
15630
  case 'headers':
15696
- value = stringifyJson(json.headers);
15697
- break;
15698
15631
  case 'body':
15699
- value = stringifyJson(json.body);
15632
+ value = stringifyJson(json[field.key]);
15700
15633
  break;
15701
15634
  case 'contentData':
15702
15635
  value = stringifyJson(json.content?.data);
@@ -15750,11 +15683,7 @@ let ActionsEditorComponent$1 = class ActionsEditorComponent {
15750
15683
  value = json.target ?? '';
15751
15684
  break;
15752
15685
  case 'text':
15753
- value = json.text ?? (hasJson ? '' : raw);
15754
- break;
15755
- case 'key':
15756
- case 'field':
15757
- value = json[field.key] ?? (hasJson ? '' : raw);
15686
+ value = json.text ?? '';
15758
15687
  break;
15759
15688
  default:
15760
15689
  value = json[field.key] ?? '';
@@ -15771,8 +15700,8 @@ let ActionsEditorComponent$1 = class ActionsEditorComponent {
15771
15700
  return value;
15772
15701
  }
15773
15702
  onGlobalActionFieldChange(action, field, value, index) {
15774
- const selectedId = this.getCustomActionSelectValue(action.action);
15775
- if (!this.isGlobalActionId(selectedId))
15703
+ const selectedId = this.getCustomActionSelectValue(action);
15704
+ if (!this.isCanonicalGlobalActionId(selectedId))
15776
15705
  return;
15777
15706
  const schema = getGlobalActionUiSchema(selectedId);
15778
15707
  if (!schema)
@@ -15796,9 +15725,7 @@ let ActionsEditorComponent$1 = class ActionsEditorComponent {
15796
15725
  }
15797
15726
  const values = this.collectGlobalActionFieldValues(action, schema.fields);
15798
15727
  values[field.key] = value;
15799
- const info = getActionParamInfo(action.action, this.globalActionCatalog, this.customActionValue);
15800
- const param = serializeGlobalActionParam(selectedId, values, info.json);
15801
- this.updateCustomAction(index, 'action', param ? `${selectedId}:${param}` : selectedId);
15728
+ this.updateCustomActionRef(index, buildGlobalActionRef(selectedId, values, action.globalAction?.payload));
15802
15729
  }
15803
15730
  setActionFieldError(action, key, message) {
15804
15731
  const draftKey = this.getActionDraftKey(action);
@@ -15820,7 +15747,7 @@ let ActionsEditorComponent$1 = class ActionsEditorComponent {
15820
15747
  }
15821
15748
  }
15822
15749
  getActionDraftKey(action) {
15823
- return String(action.id || action.action || action.label || 'action');
15750
+ return String(action.id || action.action || action.label || action.globalAction?.actionId || 'action');
15824
15751
  }
15825
15752
  setActionFieldDraft(action, key, value) {
15826
15753
  const draftKey = this.getActionDraftKey(action);
@@ -15884,6 +15811,25 @@ let ActionsEditorComponent$1 = class ActionsEditorComponent {
15884
15811
  this.syncCustomActionStyleTexts();
15885
15812
  }
15886
15813
  }
15814
+ updateCustomActionRef(index, globalAction) {
15815
+ const customActions = [...(this.actions.custom || [])];
15816
+ const current = { ...customActions[index] };
15817
+ if (globalAction) {
15818
+ current.globalAction = globalAction;
15819
+ delete current.action;
15820
+ }
15821
+ else {
15822
+ delete current.globalAction;
15823
+ }
15824
+ customActions[index] = current;
15825
+ this.configChange.emit({
15826
+ ...this.config,
15827
+ actions: {
15828
+ ...this.actions,
15829
+ custom: customActions,
15830
+ },
15831
+ });
15832
+ }
15887
15833
  addCustomButton() {
15888
15834
  const newButton = {
15889
15835
  id: `custom_${Date.now()}`,
@@ -16052,10 +15998,10 @@ let ActionsEditorComponent$1 = class ActionsEditorComponent {
16052
15998
  <div class="action-global-grid">
16053
15999
  @for (field of schema.fields; track field.key) {
16054
16000
  @if (shouldShowGlobalActionField(action, field)) {
16055
- @if (schema.id === 'showAlert' && field.key === 'message') {
16001
+ @if (schema.id === 'dialog.alert' && field.key === 'message') {
16056
16002
  <div class="action-section-title">{{ tx('global.section.essential', 'Essential') }}</div>
16057
16003
  }
16058
- @if (schema.id === 'showAlert' && field.key === 'title') {
16004
+ @if (schema.id === 'dialog.alert' && field.key === 'title') {
16059
16005
  <div class="action-section-title">{{ tx('global.section.dialogOptional', 'Dialog (optional)') }}</div>
16060
16006
  }
16061
16007
  @if (field.type === 'toggle') {
@@ -16086,6 +16032,7 @@ let ActionsEditorComponent$1 = class ActionsEditorComponent {
16086
16032
  (ngModelChange)="onGlobalActionFieldChange(action, field, $event, index)"
16087
16033
  [ngModelOptions]="{ standalone: true }"
16088
16034
  [placeholder]="field.placeholder || ''"
16035
+ [required]="isGlobalActionFieldRequired(action, field)"
16089
16036
  ></textarea>
16090
16037
  @if (field.hint) {
16091
16038
  <button
@@ -16098,6 +16045,9 @@ let ActionsEditorComponent$1 = class ActionsEditorComponent {
16098
16045
  <mat-icon>help_outline</mat-icon>
16099
16046
  </button>
16100
16047
  }
16048
+ @if (isGlobalActionFieldMissing(action, field)) {
16049
+ <mat-error aria-live="polite">{{ tx('global.validation.requiredField', 'Campo obrigatório.') }}</mat-error>
16050
+ }
16101
16051
  </mat-form-field>
16102
16052
  } @else if (field.type === 'json') {
16103
16053
  <mat-form-field>
@@ -16109,6 +16059,7 @@ let ActionsEditorComponent$1 = class ActionsEditorComponent {
16109
16059
  (ngModelChange)="onGlobalActionFieldChange(action, field, $event, index)"
16110
16060
  [ngModelOptions]="{ standalone: true }"
16111
16061
  [placeholder]="field.placeholder || '{ }'"
16062
+ [required]="isGlobalActionFieldRequired(action, field)"
16112
16063
  ></textarea>
16113
16064
  <button
16114
16065
  mat-icon-button
@@ -16121,6 +16072,8 @@ let ActionsEditorComponent$1 = class ActionsEditorComponent {
16121
16072
  </button>
16122
16073
  @if (hasActionFieldError(action, field.key)) {
16123
16074
  <mat-error>{{ getActionFieldError(action, field.key) }}</mat-error>
16075
+ } @else if (isGlobalActionFieldMissing(action, field)) {
16076
+ <mat-error aria-live="polite">{{ tx('global.validation.requiredField', 'Campo obrigatório.') }}</mat-error>
16124
16077
  }
16125
16078
  </mat-form-field>
16126
16079
  } @else if (field.type === 'select') {
@@ -16130,6 +16083,7 @@ let ActionsEditorComponent$1 = class ActionsEditorComponent {
16130
16083
  [ngModel]="getGlobalActionFieldValue(action, field)"
16131
16084
  (ngModelChange)="onGlobalActionFieldChange(action, field, $event, index)"
16132
16085
  [ngModelOptions]="{ standalone: true }"
16086
+ [required]="isGlobalActionFieldRequired(action, field)"
16133
16087
  >
16134
16088
  @for (opt of field.options || []; track opt.value) {
16135
16089
  <mat-option [value]="opt.value">{{ opt.label }}</mat-option>
@@ -16146,6 +16100,9 @@ let ActionsEditorComponent$1 = class ActionsEditorComponent {
16146
16100
  <mat-icon>help_outline</mat-icon>
16147
16101
  </button>
16148
16102
  }
16103
+ @if (isGlobalActionFieldMissing(action, field)) {
16104
+ <mat-error aria-live="polite">{{ tx('global.validation.requiredField', 'Campo obrigatório.') }}</mat-error>
16105
+ }
16149
16106
  </mat-form-field>
16150
16107
  } @else {
16151
16108
  <mat-form-field>
@@ -16157,6 +16114,7 @@ let ActionsEditorComponent$1 = class ActionsEditorComponent {
16157
16114
  (ngModelChange)="onGlobalActionFieldChange(action, field, $event, index)"
16158
16115
  [ngModelOptions]="{ standalone: true }"
16159
16116
  [placeholder]="field.placeholder || ''"
16117
+ [required]="isGlobalActionFieldRequired(action, field)"
16160
16118
  />
16161
16119
  @if (field.hint) {
16162
16120
  <button
@@ -16169,6 +16127,9 @@ let ActionsEditorComponent$1 = class ActionsEditorComponent {
16169
16127
  <mat-icon>help_outline</mat-icon>
16170
16128
  </button>
16171
16129
  }
16130
+ @if (isGlobalActionFieldMissing(action, field)) {
16131
+ <mat-error aria-live="polite">{{ tx('global.validation.requiredField', 'Campo obrigatório.') }}</mat-error>
16132
+ }
16172
16133
  </mat-form-field>
16173
16134
  }
16174
16135
  }
@@ -16443,7 +16404,7 @@ let ActionsEditorComponent$1 = class ActionsEditorComponent {
16443
16404
  <mat-accordion multi>
16444
16405
  @for (
16445
16406
  customAction of actions.custom;
16446
- track customAction.id || customAction.action || $index;
16407
+ track customAction.id || customAction.globalAction?.actionId || customAction.action || $index;
16447
16408
  let i = $index
16448
16409
  ) {
16449
16410
  <div class="custom-action-panel-shell">
@@ -16500,7 +16461,7 @@ let ActionsEditorComponent$1 = class ActionsEditorComponent {
16500
16461
  <mat-form-field>
16501
16462
  <mat-label>{{ tx('custom.action', 'Action') }}</mat-label>
16502
16463
  <mat-select
16503
- [value]="getCustomActionSelectValue(customAction.action)"
16464
+ [value]="getCustomActionSelectValue(customAction)"
16504
16465
  (selectionChange)="onCustomActionSelectChange(i, $event.value)"
16505
16466
  >
16506
16467
  <mat-option [value]="customActionValue">{{ tx('custom.action.custom', 'Custom') }}</mat-option>
@@ -16520,34 +16481,34 @@ let ActionsEditorComponent$1 = class ActionsEditorComponent {
16520
16481
  <mat-icon>help_outline</mat-icon>
16521
16482
  </button>
16522
16483
  </mat-form-field>
16523
- @if (getCustomActionSelectValue(customAction.action) === customActionValue) {
16484
+ @if (getCustomActionSelectValue(customAction) === customActionValue) {
16524
16485
  <mat-form-field>
16525
16486
  <mat-label>{{ tx('custom.action', 'Action') }}</mat-label>
16526
16487
  <input
16527
16488
  matInput
16528
- [value]="getCustomActionCustomValue(customAction.action)"
16489
+ [value]="getCustomActionCustomValue(customAction)"
16529
16490
  (input)="onCustomActionCustomChange(i, $any($event.target).value)"
16530
16491
  [placeholder]="tx('custom.action.customPlaceholder', 'Example: custom_action, navigate, delete')"
16531
16492
  />
16532
16493
  </mat-form-field>
16533
16494
  } @else {
16534
- @if (isGlobalActionId(getCustomActionSelectValue(customAction.action))) {
16495
+ @if (isCanonicalGlobalActionId(getCustomActionSelectValue(customAction))) {
16535
16496
  <ng-container
16536
16497
  [ngTemplateOutlet]="globalActionFields"
16537
16498
  [ngTemplateOutletContext]="{ $implicit: customAction, index: i }"
16538
16499
  ></ng-container>
16539
16500
  } @else {
16540
- @if (getActionSpecById(getCustomActionSelectValue(customAction.action)); as actionSpec) {
16501
+ @if (getActionSpecById(getCustomActionSelectValue(customAction)); as actionSpec) {
16541
16502
  @if (actionSpec.param) {
16542
16503
  <mat-form-field>
16543
16504
  <mat-label>{{ actionSpec.param.label || tx('custom.parameter', 'Parameter') }}</mat-label>
16544
16505
  <input
16545
16506
  matInput
16546
- [value]="getCustomActionParam(customAction.action)"
16507
+ [value]="getCustomActionParam(customAction)"
16547
16508
  (input)="onCustomActionParamChange(i, $any($event.target).value)"
16548
16509
  [placeholder]="actionSpec.param.placeholder || ''"
16549
16510
  />
16550
- @if (actionSpec.param.required && !getCustomActionParam(customAction.action)) {
16511
+ @if (actionSpec.param.required && !getCustomActionParam(customAction)) {
16551
16512
  <mat-error>{{ tx('custom.parameterRequired', 'Required parameter.') }}</mat-error>
16552
16513
  }
16553
16514
  </mat-form-field>
@@ -16825,7 +16786,7 @@ let ActionsEditorComponent$1 = class ActionsEditorComponent {
16825
16786
  </div>
16826
16787
  </mat-tab>
16827
16788
  </mat-tab-group>
16828
- `, isInline: true, styles: [".editor-container{padding:16px;display:flex;flex-direction:column;gap:16px}.action-fields{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:16px;align-items:center}.action-global-fields{grid-column:1 / -1;padding:12px 14px;border:1px dashed var(--md-sys-color-outline-variant, rgba(255, 255, 255, .12));border-radius:10px;background:var(--md-sys-color-surface-container-low)}.action-global-title{font-size:12px;font-weight:600;letter-spacing:.02em;text-transform:uppercase;color:var(--md-sys-color-on-surface-variant, #9aa0a6);margin-bottom:10px}.action-global-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:12px 16px}.help-icon-button{--mdc-icon-button-state-layer-size: 28px;--mdc-icon-button-icon-size: 18px;width:28px;height:28px;padding:0;display:inline-flex;align-items:center;justify-content:center;vertical-align:middle}.help-icon-button mat-icon{font-size:18px;line-height:18px;width:18px;height:18px}.action-global-fields .mat-mdc-form-field-icon-suffix{align-self:center}.action-section-title{width:100%;margin:10px 0 2px;font-size:12px;font-weight:600;letter-spacing:.04em;text-transform:uppercase;color:var(--md-sys-color-on-surface-variant, #9aa0a6)}.add-button{margin-bottom:16px;align-self:flex-start}.custom-action-panel-shell{display:grid;gap:8px}.custom-action-panel-toolbar{display:flex;justify-content:flex-end}.custom-action-order-controls{display:inline-flex;align-items:center;gap:4px;justify-content:flex-end}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$3.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$3.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i4$1.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i4$1.MatLabel, selector: "mat-label" }, { kind: "directive", type: i4$1.MatError, selector: "mat-error, [matError]", inputs: ["id"] }, { kind: "directive", type: i4$1.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i7$1.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatSlideToggleModule }, { kind: "component", type: i6$2.MatSlideToggle, selector: "mat-slide-toggle", inputs: ["name", "id", "labelPosition", "aria-label", "aria-labelledby", "aria-describedby", "required", "color", "disabled", "disableRipple", "tabIndex", "checked", "hideIcon", "disabledInteractive"], outputs: ["change", "toggleChange"], exportAs: ["matSlideToggle"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i6$3.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i6$3.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "component", type: i6$3.MatOptgroup, selector: "mat-optgroup", inputs: ["label", "disabled"], exportAs: ["matOptgroup"] }, { kind: "ngmodule", type: MatTabsModule }, { kind: "component", type: i7$3.MatTab, selector: "mat-tab", inputs: ["disabled", "label", "aria-label", "aria-labelledby", "labelClass", "bodyClass", "id"], exportAs: ["matTab"] }, { kind: "component", type: i7$3.MatTabGroup, selector: "mat-tab-group", inputs: ["color", "fitInkBarToContent", "mat-stretch-tabs", "mat-align-tabs", "dynamicHeight", "selectedIndex", "headerPosition", "animationDuration", "contentTabIndex", "disablePagination", "disableRipple", "preserveContent", "backgroundColor", "aria-label", "aria-labelledby"], outputs: ["selectedIndexChange", "focusChange", "animationDone", "selectedTabChange"], exportAs: ["matTabGroup"] }, { kind: "ngmodule", type: MatExpansionModule }, { kind: "directive", type: i3.MatAccordion, selector: "mat-accordion", inputs: ["hideToggle", "displayMode", "togglePosition"], exportAs: ["matAccordion"] }, { kind: "component", type: i3.MatExpansionPanel, selector: "mat-expansion-panel", inputs: ["hideToggle", "togglePosition"], outputs: ["afterExpand", "afterCollapse"], exportAs: ["matExpansionPanel"] }, { kind: "component", type: i3.MatExpansionPanelHeader, selector: "mat-expansion-panel-header", inputs: ["expandedHeight", "collapsedHeight", "tabIndex"] }, { kind: "directive", type: i3.MatExpansionPanelTitle, selector: "mat-panel-title" }, { kind: "directive", type: i3.MatExpansionPanelContent, selector: "ng-template[matExpansionPanelContent]" }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i5.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i4.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i4.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i9.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: SurfaceOpenActionEditorComponent, selector: "praxis-surface-open-action-editor", inputs: ["value", "hostKind"], outputs: ["valueChange"] }] });
16789
+ `, isInline: true, styles: [".editor-container{padding:16px;display:flex;flex-direction:column;gap:16px}.action-fields{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:16px;align-items:center}.action-global-fields{grid-column:1 / -1;padding:12px 14px;border:1px dashed var(--md-sys-color-outline-variant, rgba(255, 255, 255, .12));border-radius:10px;background:var(--md-sys-color-surface-container-low)}.action-global-title{font-size:12px;font-weight:600;letter-spacing:.02em;text-transform:uppercase;color:var(--md-sys-color-on-surface-variant, #9aa0a6);margin-bottom:10px}.action-global-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:12px 16px}.help-icon-button{--mdc-icon-button-state-layer-size: 28px;--mdc-icon-button-icon-size: 18px;width:28px;height:28px;padding:0;display:inline-flex;align-items:center;justify-content:center;vertical-align:middle}.help-icon-button mat-icon{font-size:18px;line-height:18px;width:18px;height:18px}.action-field-error{color:var(--md-sys-color-error, #ba1a1a);font-weight:500}.action-global-fields .mat-mdc-form-field-icon-suffix{align-self:center}.action-section-title{width:100%;margin:10px 0 2px;font-size:12px;font-weight:600;letter-spacing:.04em;text-transform:uppercase;color:var(--md-sys-color-on-surface-variant, #9aa0a6)}.add-button{margin-bottom:16px;align-self:flex-start}.custom-action-panel-shell{display:grid;gap:8px}.custom-action-panel-toolbar{display:flex;justify-content:flex-end}.custom-action-order-controls{display:inline-flex;align-items:center;gap:4px;justify-content:flex-end}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$3.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$3.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i1$3.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i4$1.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i4$1.MatLabel, selector: "mat-label" }, { kind: "directive", type: i4$1.MatError, selector: "mat-error, [matError]", inputs: ["id"] }, { kind: "directive", type: i4$1.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i7$1.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatSlideToggleModule }, { kind: "component", type: i6$2.MatSlideToggle, selector: "mat-slide-toggle", inputs: ["name", "id", "labelPosition", "aria-label", "aria-labelledby", "aria-describedby", "required", "color", "disabled", "disableRipple", "tabIndex", "checked", "hideIcon", "disabledInteractive"], outputs: ["change", "toggleChange"], exportAs: ["matSlideToggle"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i6$3.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i6$3.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "component", type: i6$3.MatOptgroup, selector: "mat-optgroup", inputs: ["label", "disabled"], exportAs: ["matOptgroup"] }, { kind: "ngmodule", type: MatTabsModule }, { kind: "component", type: i7$3.MatTab, selector: "mat-tab", inputs: ["disabled", "label", "aria-label", "aria-labelledby", "labelClass", "bodyClass", "id"], exportAs: ["matTab"] }, { kind: "component", type: i7$3.MatTabGroup, selector: "mat-tab-group", inputs: ["color", "fitInkBarToContent", "mat-stretch-tabs", "mat-align-tabs", "dynamicHeight", "selectedIndex", "headerPosition", "animationDuration", "contentTabIndex", "disablePagination", "disableRipple", "preserveContent", "backgroundColor", "aria-label", "aria-labelledby"], outputs: ["selectedIndexChange", "focusChange", "animationDone", "selectedTabChange"], exportAs: ["matTabGroup"] }, { kind: "ngmodule", type: MatExpansionModule }, { kind: "directive", type: i3.MatAccordion, selector: "mat-accordion", inputs: ["hideToggle", "displayMode", "togglePosition"], exportAs: ["matAccordion"] }, { kind: "component", type: i3.MatExpansionPanel, selector: "mat-expansion-panel", inputs: ["hideToggle", "togglePosition"], outputs: ["afterExpand", "afterCollapse"], exportAs: ["matExpansionPanel"] }, { kind: "component", type: i3.MatExpansionPanelHeader, selector: "mat-expansion-panel-header", inputs: ["expandedHeight", "collapsedHeight", "tabIndex"] }, { kind: "directive", type: i3.MatExpansionPanelTitle, selector: "mat-panel-title" }, { kind: "directive", type: i3.MatExpansionPanelContent, selector: "ng-template[matExpansionPanelContent]" }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i5.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i4.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i4.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i9.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: SurfaceOpenActionEditorComponent, selector: "praxis-surface-open-action-editor", inputs: ["value", "hostKind"], outputs: ["valueChange"] }] });
16829
16790
  };
16830
16791
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: ActionsEditorComponent$1, decorators: [{
16831
16792
  type: Component,
@@ -16860,10 +16821,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
16860
16821
  <div class="action-global-grid">
16861
16822
  @for (field of schema.fields; track field.key) {
16862
16823
  @if (shouldShowGlobalActionField(action, field)) {
16863
- @if (schema.id === 'showAlert' && field.key === 'message') {
16824
+ @if (schema.id === 'dialog.alert' && field.key === 'message') {
16864
16825
  <div class="action-section-title">{{ tx('global.section.essential', 'Essential') }}</div>
16865
16826
  }
16866
- @if (schema.id === 'showAlert' && field.key === 'title') {
16827
+ @if (schema.id === 'dialog.alert' && field.key === 'title') {
16867
16828
  <div class="action-section-title">{{ tx('global.section.dialogOptional', 'Dialog (optional)') }}</div>
16868
16829
  }
16869
16830
  @if (field.type === 'toggle') {
@@ -16894,6 +16855,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
16894
16855
  (ngModelChange)="onGlobalActionFieldChange(action, field, $event, index)"
16895
16856
  [ngModelOptions]="{ standalone: true }"
16896
16857
  [placeholder]="field.placeholder || ''"
16858
+ [required]="isGlobalActionFieldRequired(action, field)"
16897
16859
  ></textarea>
16898
16860
  @if (field.hint) {
16899
16861
  <button
@@ -16906,6 +16868,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
16906
16868
  <mat-icon>help_outline</mat-icon>
16907
16869
  </button>
16908
16870
  }
16871
+ @if (isGlobalActionFieldMissing(action, field)) {
16872
+ <mat-error aria-live="polite">{{ tx('global.validation.requiredField', 'Campo obrigatório.') }}</mat-error>
16873
+ }
16909
16874
  </mat-form-field>
16910
16875
  } @else if (field.type === 'json') {
16911
16876
  <mat-form-field>
@@ -16917,6 +16882,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
16917
16882
  (ngModelChange)="onGlobalActionFieldChange(action, field, $event, index)"
16918
16883
  [ngModelOptions]="{ standalone: true }"
16919
16884
  [placeholder]="field.placeholder || '{ }'"
16885
+ [required]="isGlobalActionFieldRequired(action, field)"
16920
16886
  ></textarea>
16921
16887
  <button
16922
16888
  mat-icon-button
@@ -16929,6 +16895,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
16929
16895
  </button>
16930
16896
  @if (hasActionFieldError(action, field.key)) {
16931
16897
  <mat-error>{{ getActionFieldError(action, field.key) }}</mat-error>
16898
+ } @else if (isGlobalActionFieldMissing(action, field)) {
16899
+ <mat-error aria-live="polite">{{ tx('global.validation.requiredField', 'Campo obrigatório.') }}</mat-error>
16932
16900
  }
16933
16901
  </mat-form-field>
16934
16902
  } @else if (field.type === 'select') {
@@ -16938,6 +16906,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
16938
16906
  [ngModel]="getGlobalActionFieldValue(action, field)"
16939
16907
  (ngModelChange)="onGlobalActionFieldChange(action, field, $event, index)"
16940
16908
  [ngModelOptions]="{ standalone: true }"
16909
+ [required]="isGlobalActionFieldRequired(action, field)"
16941
16910
  >
16942
16911
  @for (opt of field.options || []; track opt.value) {
16943
16912
  <mat-option [value]="opt.value">{{ opt.label }}</mat-option>
@@ -16954,6 +16923,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
16954
16923
  <mat-icon>help_outline</mat-icon>
16955
16924
  </button>
16956
16925
  }
16926
+ @if (isGlobalActionFieldMissing(action, field)) {
16927
+ <mat-error aria-live="polite">{{ tx('global.validation.requiredField', 'Campo obrigatório.') }}</mat-error>
16928
+ }
16957
16929
  </mat-form-field>
16958
16930
  } @else {
16959
16931
  <mat-form-field>
@@ -16965,6 +16937,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
16965
16937
  (ngModelChange)="onGlobalActionFieldChange(action, field, $event, index)"
16966
16938
  [ngModelOptions]="{ standalone: true }"
16967
16939
  [placeholder]="field.placeholder || ''"
16940
+ [required]="isGlobalActionFieldRequired(action, field)"
16968
16941
  />
16969
16942
  @if (field.hint) {
16970
16943
  <button
@@ -16977,6 +16950,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
16977
16950
  <mat-icon>help_outline</mat-icon>
16978
16951
  </button>
16979
16952
  }
16953
+ @if (isGlobalActionFieldMissing(action, field)) {
16954
+ <mat-error aria-live="polite">{{ tx('global.validation.requiredField', 'Campo obrigatório.') }}</mat-error>
16955
+ }
16980
16956
  </mat-form-field>
16981
16957
  }
16982
16958
  }
@@ -17251,7 +17227,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
17251
17227
  <mat-accordion multi>
17252
17228
  @for (
17253
17229
  customAction of actions.custom;
17254
- track customAction.id || customAction.action || $index;
17230
+ track customAction.id || customAction.globalAction?.actionId || customAction.action || $index;
17255
17231
  let i = $index
17256
17232
  ) {
17257
17233
  <div class="custom-action-panel-shell">
@@ -17308,7 +17284,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
17308
17284
  <mat-form-field>
17309
17285
  <mat-label>{{ tx('custom.action', 'Action') }}</mat-label>
17310
17286
  <mat-select
17311
- [value]="getCustomActionSelectValue(customAction.action)"
17287
+ [value]="getCustomActionSelectValue(customAction)"
17312
17288
  (selectionChange)="onCustomActionSelectChange(i, $event.value)"
17313
17289
  >
17314
17290
  <mat-option [value]="customActionValue">{{ tx('custom.action.custom', 'Custom') }}</mat-option>
@@ -17328,34 +17304,34 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
17328
17304
  <mat-icon>help_outline</mat-icon>
17329
17305
  </button>
17330
17306
  </mat-form-field>
17331
- @if (getCustomActionSelectValue(customAction.action) === customActionValue) {
17307
+ @if (getCustomActionSelectValue(customAction) === customActionValue) {
17332
17308
  <mat-form-field>
17333
17309
  <mat-label>{{ tx('custom.action', 'Action') }}</mat-label>
17334
17310
  <input
17335
17311
  matInput
17336
- [value]="getCustomActionCustomValue(customAction.action)"
17312
+ [value]="getCustomActionCustomValue(customAction)"
17337
17313
  (input)="onCustomActionCustomChange(i, $any($event.target).value)"
17338
17314
  [placeholder]="tx('custom.action.customPlaceholder', 'Example: custom_action, navigate, delete')"
17339
17315
  />
17340
17316
  </mat-form-field>
17341
17317
  } @else {
17342
- @if (isGlobalActionId(getCustomActionSelectValue(customAction.action))) {
17318
+ @if (isCanonicalGlobalActionId(getCustomActionSelectValue(customAction))) {
17343
17319
  <ng-container
17344
17320
  [ngTemplateOutlet]="globalActionFields"
17345
17321
  [ngTemplateOutletContext]="{ $implicit: customAction, index: i }"
17346
17322
  ></ng-container>
17347
17323
  } @else {
17348
- @if (getActionSpecById(getCustomActionSelectValue(customAction.action)); as actionSpec) {
17324
+ @if (getActionSpecById(getCustomActionSelectValue(customAction)); as actionSpec) {
17349
17325
  @if (actionSpec.param) {
17350
17326
  <mat-form-field>
17351
17327
  <mat-label>{{ actionSpec.param.label || tx('custom.parameter', 'Parameter') }}</mat-label>
17352
17328
  <input
17353
17329
  matInput
17354
- [value]="getCustomActionParam(customAction.action)"
17330
+ [value]="getCustomActionParam(customAction)"
17355
17331
  (input)="onCustomActionParamChange(i, $any($event.target).value)"
17356
17332
  [placeholder]="actionSpec.param.placeholder || ''"
17357
17333
  />
17358
- @if (actionSpec.param.required && !getCustomActionParam(customAction.action)) {
17334
+ @if (actionSpec.param.required && !getCustomActionParam(customAction)) {
17359
17335
  <mat-error>{{ tx('custom.parameterRequired', 'Required parameter.') }}</mat-error>
17360
17336
  }
17361
17337
  </mat-form-field>
@@ -17633,7 +17609,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
17633
17609
  </div>
17634
17610
  </mat-tab>
17635
17611
  </mat-tab-group>
17636
- `, styles: [".editor-container{padding:16px;display:flex;flex-direction:column;gap:16px}.action-fields{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:16px;align-items:center}.action-global-fields{grid-column:1 / -1;padding:12px 14px;border:1px dashed var(--md-sys-color-outline-variant, rgba(255, 255, 255, .12));border-radius:10px;background:var(--md-sys-color-surface-container-low)}.action-global-title{font-size:12px;font-weight:600;letter-spacing:.02em;text-transform:uppercase;color:var(--md-sys-color-on-surface-variant, #9aa0a6);margin-bottom:10px}.action-global-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:12px 16px}.help-icon-button{--mdc-icon-button-state-layer-size: 28px;--mdc-icon-button-icon-size: 18px;width:28px;height:28px;padding:0;display:inline-flex;align-items:center;justify-content:center;vertical-align:middle}.help-icon-button mat-icon{font-size:18px;line-height:18px;width:18px;height:18px}.action-global-fields .mat-mdc-form-field-icon-suffix{align-self:center}.action-section-title{width:100%;margin:10px 0 2px;font-size:12px;font-weight:600;letter-spacing:.04em;text-transform:uppercase;color:var(--md-sys-color-on-surface-variant, #9aa0a6)}.add-button{margin-bottom:16px;align-self:flex-start}.custom-action-panel-shell{display:grid;gap:8px}.custom-action-panel-toolbar{display:flex;justify-content:flex-end}.custom-action-order-controls{display:inline-flex;align-items:center;gap:4px;justify-content:flex-end}\n"] }]
17612
+ `, styles: [".editor-container{padding:16px;display:flex;flex-direction:column;gap:16px}.action-fields{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:16px;align-items:center}.action-global-fields{grid-column:1 / -1;padding:12px 14px;border:1px dashed var(--md-sys-color-outline-variant, rgba(255, 255, 255, .12));border-radius:10px;background:var(--md-sys-color-surface-container-low)}.action-global-title{font-size:12px;font-weight:600;letter-spacing:.02em;text-transform:uppercase;color:var(--md-sys-color-on-surface-variant, #9aa0a6);margin-bottom:10px}.action-global-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:12px 16px}.help-icon-button{--mdc-icon-button-state-layer-size: 28px;--mdc-icon-button-icon-size: 18px;width:28px;height:28px;padding:0;display:inline-flex;align-items:center;justify-content:center;vertical-align:middle}.help-icon-button mat-icon{font-size:18px;line-height:18px;width:18px;height:18px}.action-field-error{color:var(--md-sys-color-error, #ba1a1a);font-weight:500}.action-global-fields .mat-mdc-form-field-icon-suffix{align-self:center}.action-section-title{width:100%;margin:10px 0 2px;font-size:12px;font-weight:600;letter-spacing:.04em;text-transform:uppercase;color:var(--md-sys-color-on-surface-variant, #9aa0a6)}.add-button{margin-bottom:16px;align-self:flex-start}.custom-action-panel-shell{display:grid;gap:8px}.custom-action-panel-toolbar{display:flex;justify-content:flex-end}.custom-action-order-controls{display:inline-flex;align-items:center;gap:4px;justify-content:flex-end}\n"] }]
17637
17613
  }], propDecorators: { config: [{
17638
17614
  type: Input
17639
17615
  }], configChange: [{
@@ -18995,6 +18971,9 @@ class PraxisDynamicFormConfigEditor {
18995
18971
  serverMeta;
18996
18972
  destroy$ = new Subject();
18997
18973
  hasProcessedInitialRuleState = false;
18974
+ jsonEditorIsValid = true;
18975
+ globalActionValidationIssues = [];
18976
+ globalActionCatalog;
18998
18977
  // Observables obrigatórios da interface SettingsValueProvider
18999
18978
  isDirty$ = new BehaviorSubject(false);
19000
18979
  isValid$ = new BehaviorSubject(true);
@@ -19011,11 +18990,12 @@ class PraxisDynamicFormConfigEditor {
19011
18990
  }
19012
18991
  console.debug(message);
19013
18992
  }
19014
- constructor(storage, configService, settingsPanel, cdr, injectedData) {
18993
+ constructor(storage, configService, settingsPanel, cdr, globalActionCatalogSource, injectedData) {
19015
18994
  this.storage = storage;
19016
18995
  this.configService = configService;
19017
18996
  this.settingsPanel = settingsPanel;
19018
18997
  this.cdr = cdr;
18998
+ this.globalActionCatalog = this.buildGlobalActionCatalog(globalActionCatalogSource);
19019
18999
  const data = injectedData;
19020
19000
  this.openedWithCanonicalDocument =
19021
19001
  data?.document?.kind === 'praxis.dynamic-form.editor' ||
@@ -19113,6 +19093,7 @@ class PraxisDynamicFormConfigEditor {
19113
19093
  }
19114
19094
  }
19115
19095
  catch { }
19096
+ this.updateValidityState();
19116
19097
  // Nota: isValid$ é atualizado em onJsonValidationChange quando necessário
19117
19098
  // Para outras validações futuras, verificar se não há validação JSON em andamento
19118
19099
  }
@@ -19179,7 +19160,8 @@ class PraxisDynamicFormConfigEditor {
19179
19160
  }
19180
19161
  onJsonValidationChange(result) {
19181
19162
  // Atualizar estado de validação baseado no resultado do JSON
19182
- this.isValid$.next(result.isValid);
19163
+ this.jsonEditorIsValid = result.isValid;
19164
+ this.updateValidityState();
19183
19165
  // Evitar marcar dirty apenas por validação (não há mudança real de config)
19184
19166
  if (this.isDirty$.value) {
19185
19167
  this.updateDirtyState('json-validate');
@@ -19203,6 +19185,41 @@ class PraxisDynamicFormConfigEditor {
19203
19185
  this.lastRulesSignature = this.computeRulesSignature(this.ruleBuilderState);
19204
19186
  this.updateDirtyState('layout-change');
19205
19187
  }
19188
+ updateValidityState() {
19189
+ this.globalActionValidationIssues = this.collectGlobalActionValidationIssues(this.editedConfig);
19190
+ this.isValid$.next(this.jsonEditorIsValid && this.globalActionValidationIssues.length === 0);
19191
+ }
19192
+ collectGlobalActionValidationIssues(config) {
19193
+ return validateGlobalActionRefs(this.collectGlobalActionValidationTargets(config));
19194
+ }
19195
+ collectGlobalActionValidationTargets(config) {
19196
+ const targets = [];
19197
+ const add = (ref, path) => {
19198
+ if (!ref)
19199
+ return;
19200
+ targets.push({
19201
+ ref,
19202
+ path,
19203
+ catalogEntry: this.findGlobalActionCatalogEntry(ref.actionId),
19204
+ });
19205
+ };
19206
+ const actions = config.actions;
19207
+ add(actions?.submit?.globalAction, 'actions.submit.globalAction');
19208
+ add(actions?.cancel?.globalAction, 'actions.cancel.globalAction');
19209
+ add(actions?.reset?.globalAction, 'actions.reset.globalAction');
19210
+ actions?.custom?.forEach((action, index) => add(action?.globalAction, `actions.custom[${index}].globalAction`));
19211
+ config.sections?.forEach((section, sectionIndex) => {
19212
+ section?.headerActions?.forEach((action, actionIndex) => add(action?.globalAction, `sections[${sectionIndex}].headerActions[${actionIndex}].globalAction`));
19213
+ });
19214
+ return targets;
19215
+ }
19216
+ findGlobalActionCatalogEntry(actionId) {
19217
+ return this.globalActionCatalog.find((entry) => entry.id === actionId);
19218
+ }
19219
+ buildGlobalActionCatalog(source) {
19220
+ const injected = getGlobalActionCatalog(source);
19221
+ return injected.length ? injected : PRAXIS_GLOBAL_ACTION_CATALOG;
19222
+ }
19206
19223
  onRulesChanged(state) {
19207
19224
  if (!this.hasProcessedInitialRuleState) {
19208
19225
  const hadInitialRules = this.initialConfig?.formRules?.length ||
@@ -19432,6 +19449,7 @@ class PraxisDynamicFormConfigEditor {
19432
19449
  file: FieldType.STRING,
19433
19450
  url: FieldType.URL,
19434
19451
  boolean: FieldType.BOOLEAN,
19452
+ array: FieldType.JSON,
19435
19453
  json: FieldType.JSON,
19436
19454
  };
19437
19455
  return mapping[dataType ?? 'text'];
@@ -19686,7 +19704,7 @@ class PraxisDynamicFormConfigEditor {
19686
19704
  toBackConfigSnapshot() {
19687
19705
  return structuredClone(this.backConfig || { returnTo: '', confirmOnDirty: true });
19688
19706
  }
19689
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisDynamicFormConfigEditor, deps: [{ token: ASYNC_CONFIG_STORAGE }, { token: FormConfigService }, { token: i7.SettingsPanelService }, { token: i0.ChangeDetectorRef }, { token: SETTINGS_PANEL_DATA, optional: true }], target: i0.ɵɵFactoryTarget.Component });
19707
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisDynamicFormConfigEditor, deps: [{ token: ASYNC_CONFIG_STORAGE }, { token: FormConfigService }, { token: i7.SettingsPanelService }, { token: i0.ChangeDetectorRef }, { token: GLOBAL_ACTION_CATALOG, optional: true }, { token: SETTINGS_PANEL_DATA, optional: true }], target: i0.ɵɵFactoryTarget.Component });
19690
19708
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.17", type: PraxisDynamicFormConfigEditor, isStandalone: true, selector: "praxis-dynamic-form-config-editor", providers: [FormConfigService], viewQueries: [{ propertyName: "jsonEditor", first: true, predicate: JsonConfigEditorComponent, descendants: true }], ngImport: i0, template: "<mat-tab-group class=\"config-tabs\" data-testid=\"dynamic-form-config-editor-tabs\">\n <mat-tab label=\"Geral\">\n <div class=\"tab-content\" data-testid=\"config-tab-panel-geral\">\n <mat-card class=\"section-card\">\n <mat-card-content>\n <div class=\"section-header\">\n <div>\n <h3>Dados do formul\u00E1rio</h3>\n <p class=\"text-caption\">Defina o modo de dados e o contexto de edi\u00E7\u00E3o.</p>\n </div>\n </div>\n <div class=\"form-grid\">\n <div class=\"form-row\">\n <mat-form-field appearance=\"fill\" class=\"w-100\">\n <mat-label>Modo de dados</mat-label>\n <mat-select [(ngModel)]=\"inputMode\" (ngModelChange)=\"onInputModeChange()\">\n <mat-option value=\"create\">create</mat-option>\n <mat-option value=\"edit\">edit</mat-option>\n <mat-option value=\"view\">view</mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n <div class=\"block-status\" [class.block-status--implicit]=\"!isBindingsBlockPersisted()\">\n <strong>Bindings</strong>\n <span *ngIf=\"isBindingsBlockPersisted(); else bindingsMissing\">`bindings.mode` ser\u00E1 persistido no documento can\u00F4nico.</span>\n <ng-template #bindingsMissing>Bloco ausente no documento can\u00F4nico. O valor exibido \u00E9 fallback visual e n\u00E3o ser\u00E1 salvo at\u00E9 voc\u00EA editar este campo.</ng-template>\n </div>\n <div class=\"section-help\">\n <span class=\"section-help__label\">Entenda:</span>\n <button\n mat-icon-button\n class=\"help-icon-button\"\n type=\"button\"\n [matTooltip]=\"'O mode controla o fluxo (create/edit/view). enableCustomization s\u00F3 libera edi\u00E7\u00E3o do layout e n\u00E3o altera o mode.'\"\n matTooltipPosition=\"above\"\n >\n <mat-icon>help_outline</mat-icon>\n </button>\n </div>\n </div>\n </mat-card-content>\n </mat-card>\n\n <mat-card\n class=\"section-card\"\n *ngIf=\"hasResolvedRuntimeContract()\"\n data-testid=\"config-tab-runtime-contract\"\n >\n <mat-card-content>\n <div class=\"section-header\">\n <div>\n <h3>Contrato Runtime Resolvido</h3>\n <p class=\"text-caption\">\n Visualiza\u00E7\u00E3o somente leitura do contrato backend resolvido pelo host atual.\n </p>\n </div>\n </div>\n <div class=\"block-status block-status--readonly\">\n <strong>Runtime</strong>\n <span>\n Esses valores v\u00EAm do host/runtime atual e n\u00E3o s\u00E3o persistidos em\n <code>bindings</code> nem em <code>contextSnapshot</code>.\n </span>\n </div>\n <div class=\"runtime-contract-grid\">\n <div class=\"runtime-contract-field\">\n <span class=\"runtime-contract-label\">Schema URL</span>\n <code class=\"runtime-contract-code\">{{ runtimeContract?.schemaUrl || '\u2014' }}</code>\n </div>\n <div class=\"runtime-contract-field\">\n <span class=\"runtime-contract-label\">Origem do schema</span>\n <strong>{{ describeRuntimeSource(runtimeContract?.schemaSource) }}</strong>\n </div>\n <div class=\"runtime-contract-field\">\n <span class=\"runtime-contract-label\">Submit method</span>\n <strong>{{ runtimeContract?.submitMethod || '\u2014' }}</strong>\n </div>\n <div class=\"runtime-contract-field\">\n <span class=\"runtime-contract-label\">Submit URL</span>\n <code class=\"runtime-contract-code\">{{ runtimeContract?.submitUrl || '\u2014' }}</code>\n </div>\n <div class=\"runtime-contract-field\">\n <span class=\"runtime-contract-label\">Origem do submit</span>\n <strong>{{ describeRuntimeSource(runtimeContract?.submitSource) }}</strong>\n </div>\n </div>\n </mat-card-content>\n </mat-card>\n </div>\n </mat-tab>\n <mat-tab label=\"Verifica\u00E7\u00E3o de Schema\">\n <div class=\"tab-content\" data-testid=\"config-tab-panel-schema\">\n <mat-card class=\"section-card\">\n <mat-card-content>\n <div class=\"section-header\">\n <div>\n <h3>Valida\u00E7\u00E3o de schema</h3>\n <p class=\"text-caption\">Controle avisos quando o schema do servidor mudar.</p>\n </div>\n </div>\n <div class=\"form-grid\">\n <div class=\"form-row\">\n <mat-form-field appearance=\"fill\" class=\"w-100\">\n <mat-label>Notificar quando desatualizado</mat-label>\n <mat-select [(ngModel)]=\"schemaPrefs.notifyIfOutdated\" (ngModelChange)=\"onSchemaPrefsChange('schema-prefs-notify')\">\n <mat-option value=\"both\">Banner + Snackbar</mat-option>\n <mat-option value=\"inline\">Somente Banner</mat-option>\n <mat-option value=\"snackbar\">Somente Snackbar</mat-option>\n <mat-option value=\"none\">Nenhum</mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n <div class=\"form-row\">\n <mat-form-field appearance=\"fill\" class=\"w-100\">\n <mat-label>Intervalo de sil\u00EAncio (ms)</mat-label>\n <input matInput type=\"number\" [(ngModel)]=\"schemaPrefs.snoozeMs\" (ngModelChange)=\"onSchemaPrefsChange('schema-prefs-snooze')\" />\n </mat-form-field>\n </div>\n <div class=\"form-row\">\n <mat-slide-toggle [(ngModel)]=\"schemaPrefs.autoOpenSettingsOnOutdated\" (ngModelChange)=\"onSchemaPrefsChange('schema-prefs-auto-open')\">Abrir configura\u00E7\u00F5es\n automaticamente</mat-slide-toggle>\n </div>\n <div class=\"form-row\" *ngIf=\"serverMeta\">\n <div class=\"meta-card\">\n <div class=\"meta-row\">\n <span>Server hash</span>\n <strong>{{ serverMeta.serverHash || '\u2014' }}</strong>\n </div>\n <div class=\"meta-row\">\n <span>\u00DAltima verifica\u00E7\u00E3o</span>\n <strong>{{ serverMeta.lastVerifiedAt || '\u2014' }}</strong>\n </div>\n </div>\n </div>\n <div class=\"block-status\" [class.block-status--implicit]=\"!isSchemaPrefsBlockPersisted()\">\n <strong>Schema Prefs</strong>\n <span *ngIf=\"isSchemaPrefsBlockPersisted(); else schemaPrefsMissing\">`contextSnapshot.schemaPrefs` ser\u00E1 persistido no documento can\u00F4nico.</span>\n <ng-template #schemaPrefsMissing>Bloco ausente no documento can\u00F4nico. Os controles exibem fallback visual e n\u00E3o ser\u00E3o salvos at\u00E9 voc\u00EA editar.</ng-template>\n </div>\n </div>\n </mat-card-content>\n </mat-card>\n </div>\n </mat-tab>\n <mat-tab label=\"Layout\">\n <div class=\"tab-content tab-content--full\" data-testid=\"config-tab-panel-layout\">\n <div class=\"section-header section-header--compact\">\n <div>\n <h3>Layout do formul\u00E1rio</h3>\n <p class=\"text-caption\">Organize se\u00E7\u00F5es, linhas e campos com drag & drop.</p>\n </div>\n </div>\n <div class=\"section-body section-body--flex\">\n <praxis-layout-editor [config]=\"editedConfig\" (configChange)=\"onConfigChange($event)\"\n (select)=\"onLayoutSelect($event)\"></praxis-layout-editor>\n </div>\n </div>\n </mat-tab>\n <mat-tab label=\"Hooks\">\n <div class=\"tab-content\" data-testid=\"config-tab-panel-hooks\">\n <mat-card class=\"section-card\">\n <mat-card-content>\n <div class=\"section-header\">\n <div>\n <h3>Hooks de ciclo de vida</h3>\n <p class=\"text-caption\">Automatize a\u00E7\u00F5es nos eventos do formul\u00E1rio.</p>\n </div>\n </div>\n <praxis-hooks-editor [config]=\"editedConfig\" (configChange)=\"onConfigChange($event)\"></praxis-hooks-editor>\n </mat-card-content>\n </mat-card>\n </div>\n </mat-tab>\n <mat-tab *ngIf=\"isPresentationMode\" label=\"Modo Apresenta\u00E7\u00E3o\">\n <div class=\"tab-content\" data-testid=\"config-tab-panel-presentation\">\n <mat-card class=\"section-card\">\n <mat-card-content>\n <div class=\"section-header\">\n <div>\n <h3>Modo Apresenta\u00E7\u00E3o</h3>\n <p class=\"text-caption\">Configura\u00E7\u00F5es de exibi\u00E7\u00E3o aplicadas somente ao modo apresenta\u00E7\u00E3o.</p>\n </div>\n </div>\n\n <div class=\"presentation-layout\">\n <div class=\"presentation-controls\">\n <div class=\"block-status control-span\" [class.block-status--implicit]=\"!isPresentationBlockPersisted()\">\n <strong>Presentation</strong>\n <span *ngIf=\"isPresentationBlockPersisted(); else presentationMissing\">`contextSnapshot.presentation` ser\u00E1 persistido no documento can\u00F4nico.</span>\n <ng-template #presentationMissing>Bloco ausente no documento can\u00F4nico. A pr\u00E9-visualiza\u00E7\u00E3o usa fallback local e n\u00E3o ser\u00E1 salva at\u00E9 voc\u00EA editar.</ng-template>\n </div>\n <div class=\"control\">\n <label class=\"control__label\">Posi\u00E7\u00E3o do r\u00F3tulo</label>\n <mat-button-toggle-group [(ngModel)]=\"presentationPrefs.labelPosition\" (ngModelChange)=\"onPresentationPrefsChange('presentation-label-position')\" aria-label=\"Posi\u00E7\u00E3o do r\u00F3tulo\"\n class=\"toggle-group\">\n <mat-button-toggle value=\"above\">\n <mat-icon>view_stream</mat-icon>\n <span class=\"sr-only\">Acima</span>\n </mat-button-toggle>\n <mat-button-toggle value=\"left\">\n <mat-icon>view_column</mat-icon>\n <span class=\"sr-only\">\u00C0 esquerda</span>\n </mat-button-toggle>\n </mat-button-toggle-group>\n </div>\n\n <div class=\"control\">\n <label class=\"control__label\">Densidade</label>\n <mat-button-toggle-group [(ngModel)]=\"presentationPrefs.density\" (ngModelChange)=\"onPresentationPrefsChange('presentation-density')\" aria-label=\"Densidade\"\n class=\"toggle-group\">\n <mat-button-toggle value=\"comfortable\">Confort\u00E1vel</mat-button-toggle>\n <mat-button-toggle value=\"cozy\">Intermedi\u00E1ria</mat-button-toggle>\n <mat-button-toggle value=\"compact\">Compacta</mat-button-toggle>\n </mat-button-toggle-group>\n </div>\n\n <div class=\"control\">\n <label class=\"control__label\">Alinhamento do r\u00F3tulo</label>\n <mat-button-toggle-group [(ngModel)]=\"presentationPrefs.labelAlign\" (ngModelChange)=\"onPresentationPrefsChange('presentation-label-align')\" aria-label=\"Alinhamento do r\u00F3tulo\"\n class=\"toggle-group\">\n <mat-button-toggle value=\"start\"><mat-icon>format_align_left</mat-icon></mat-button-toggle>\n <mat-button-toggle value=\"center\"><mat-icon>format_align_center</mat-icon></mat-button-toggle>\n <mat-button-toggle value=\"end\"><mat-icon>format_align_right</mat-icon></mat-button-toggle>\n </mat-button-toggle-group>\n </div>\n\n <div class=\"control\">\n <label class=\"control__label\">Alinhamento do valor</label>\n <mat-button-toggle-group [(ngModel)]=\"presentationPrefs.valueAlign\" (ngModelChange)=\"onPresentationPrefsChange('presentation-value-align')\" aria-label=\"Alinhamento do valor\"\n class=\"toggle-group\">\n <mat-button-toggle value=\"start\"><mat-icon>format_align_left</mat-icon></mat-button-toggle>\n <mat-button-toggle value=\"center\"><mat-icon>format_align_center</mat-icon></mat-button-toggle>\n <mat-button-toggle value=\"end\"><mat-icon>format_align_right</mat-icon></mat-button-toggle>\n </mat-button-toggle-group>\n </div>\n\n <div class=\"control\">\n <mat-form-field appearance=\"fill\" class=\"w-100\">\n <mat-label>Tamanho do r\u00F3tulo</mat-label>\n <input matInput type=\"number\" min=\"10\" [(ngModel)]=\"presentationPrefs.labelFontSize\" (ngModelChange)=\"onPresentationPrefsChange('presentation-label-font-size')\"\n placeholder=\"ex.: 12\" />\n <span matSuffix>px</span>\n </mat-form-field>\n </div>\n\n <div class=\"control\">\n <mat-form-field appearance=\"fill\" class=\"w-100\">\n <mat-label>Tamanho do valor</mat-label>\n <input matInput type=\"number\" min=\"10\" [(ngModel)]=\"presentationPrefs.valueFontSize\" (ngModelChange)=\"onPresentationPrefsChange('presentation-value-font-size')\"\n placeholder=\"ex.: 16\" />\n <span matSuffix>px</span>\n </mat-form-field>\n </div>\n\n <div class=\"control\">\n <mat-form-field appearance=\"fill\" class=\"w-100\">\n <mat-label>Largura do r\u00F3tulo (label \u00E0 esquerda)</mat-label>\n <input matInput type=\"number\" min=\"60\" [(ngModel)]=\"presentationPrefs.labelWidth\" (ngModelChange)=\"onPresentationPrefsChange('presentation-label-width')\"\n placeholder=\"ex.: 140\" />\n <span matSuffix>px</span>\n </mat-form-field>\n </div>\n\n <div class=\"control control--inline\">\n <mat-slide-toggle [(ngModel)]=\"presentationPrefs.compact\" (ngModelChange)=\"onPresentationPrefsChange('presentation-compact')\">\n Modo compacto\n </mat-slide-toggle>\n <mat-checkbox [(ngModel)]=\"truncatePreview\">\n Truncar valores\n </mat-checkbox>\n </div>\n </div>\n\n <div class=\"presentation-preview\">\n <h4>Pr\u00E9\u2011visualiza\u00E7\u00E3o</h4>\n <div class=\"preview-card\">\n <div class=\"praxis-dynamic-form presentation-mode\" [ngClass]=\"{\n 'pres-compact': !!presentationPrefs.compact,\n 'pres-label-left': presentationPrefs.labelPosition === 'left',\n 'pres-label-above': presentationPrefs.labelPosition === 'above',\n 'pres-density-compact': presentationPrefs.density === 'compact',\n 'pres-density-cozy': presentationPrefs.density === 'cozy'\n }\" [style.--pfx-pres-label-size]=\"(presentationPrefs.labelFontSize || 12) + 'px'\"\n [style.--pfx-pres-value-size]=\"(presentationPrefs.valueFontSize || 16) + 'px'\"\n [style.--pfx-pres-label-width]=\"(presentationPrefs.labelWidth || 140) + 'px'\"\n [style.--pfx-pres-label-align]=\"presentationPrefs.labelAlign || 'start'\"\n [style.--pfx-pres-value-align]=\"presentationPrefs.valueAlign || 'start'\">\n <div class=\"preview-row\" *ngFor=\"let item of previewItems\">\n <span class=\"praxis-presentation__label\">{{ item.label }}</span>\n <span class=\"praxis-presentation__value\" [title]=\"truncatePreview ? item.value : null\"\n [style.maxWidth]=\"truncatePreview ? '280px' : null\"\n [style.overflow]=\"truncatePreview ? 'hidden' : null\"\n [style.textOverflow]=\"truncatePreview ? 'ellipsis' : null\"\n [style.whiteSpace]=\"truncatePreview ? 'nowrap' : 'normal'\">\n {{ item.value }}\n </span>\n </div>\n </div>\n </div>\n </div>\n </div>\n\n <div class=\"section-help\">\n <span class=\"section-help__label\">Entenda:</span>\n <button\n mat-icon-button\n class=\"help-icon-button\"\n type=\"button\"\n [matTooltip]=\"'Ajustes aplicam somente ao Modo Apresenta\u00E7\u00E3o e s\u00E3o salvos por formul\u00E1rio (via CSS/Classes).'\"\n matTooltipPosition=\"above\"\n >\n <mat-icon>help_outline</mat-icon>\n </button>\n </div>\n </mat-card-content>\n </mat-card>\n </div>\n </mat-tab>\n <mat-tab label=\"Comportamento\">\n <div class=\"tab-content\" data-testid=\"config-tab-panel-comportamento\">\n <mat-card class=\"section-card\">\n <mat-card-content>\n <div class=\"section-header\">\n <div>\n <h3>Comportamento</h3>\n <p class=\"text-caption\">Ajuste valida\u00E7\u00F5es e respostas do formul\u00E1rio.</p>\n </div>\n </div>\n <praxis-behavior-editor [config]=\"editedConfig\" (configChange)=\"onConfigChange($event)\"></praxis-behavior-editor>\n </mat-card-content>\n </mat-card>\n </div>\n </mat-tab>\n <mat-tab label=\"Dicas e Tooltips\">\n <div class=\"tab-content tab-content--dense\" data-testid=\"config-tab-panel-hints\">\n <mat-card class=\"section-card\">\n <mat-card-content>\n <div class=\"section-header\">\n <div>\n <h3>Dicas dos modos</h3>\n <p class=\"text-caption\">Personalize textos de apoio para modos de dados e estados de UI.</p>\n </div>\n </div>\n <p class=\"text-caption\">Personalize os textos usados como tooltip/hint para modos de dados e modos globais de\n UI.</p>\n\n <div class=\"grid-2-cols grid-2-cols--compact\">\n <section>\n <h4>Modos de dados</h4>\n <mat-form-field appearance=\"fill\" class=\"w-100\">\n <mat-label>Criar</mat-label>\n <input matInput [(ngModel)]=\"editedConfig.hints!.dataModes.create\"\n (ngModelChange)=\"updateDirtyState('hints-data-create')\" placeholder=\"Ex.: Preencha os campos obrigat\u00F3rios.\" />\n </mat-form-field>\n <mat-form-field appearance=\"fill\" class=\"w-100\">\n <mat-label>Editar</mat-label>\n <input matInput [(ngModel)]=\"editedConfig.hints!.dataModes.edit\"\n (ngModelChange)=\"updateDirtyState('hints-data-edit')\" placeholder=\"Ex.: Revise os dados antes de salvar.\" />\n </mat-form-field>\n <mat-form-field appearance=\"fill\" class=\"w-100\">\n <mat-label>Visualizar</mat-label>\n <input matInput [(ngModel)]=\"editedConfig.hints!.dataModes.view\"\n (ngModelChange)=\"updateDirtyState('hints-data-view')\" placeholder=\"Ex.: Visualiza\u00E7\u00E3o somente leitura.\" />\n </mat-form-field>\n </section>\n\n <section>\n <h4>Modos de UI</h4>\n <mat-form-field appearance=\"fill\" class=\"w-100\">\n <mat-label>Apresenta\u00E7\u00E3o</mat-label>\n <textarea matInput rows=\"2\" [(ngModel)]=\"editedConfig.hints!.uiModes.presentation\"\n (ngModelChange)=\"updateDirtyState('hints-ui-presentation')\" placeholder=\"Ex.: Modo compacto para leitura r\u00E1pida.\"></textarea>\n </mat-form-field>\n <mat-form-field appearance=\"fill\" class=\"w-100\">\n <mat-label>Leitura (readonly)</mat-label>\n <textarea matInput rows=\"2\" [(ngModel)]=\"editedConfig.hints!.uiModes.readonly\"\n (ngModelChange)=\"updateDirtyState('hints-ui-readonly')\" placeholder=\"Ex.: Campos bloqueados para edi\u00E7\u00E3o.\"></textarea>\n </mat-form-field>\n <mat-form-field appearance=\"fill\" class=\"w-100\">\n <mat-label>Desabilitado</mat-label>\n <textarea matInput rows=\"2\" [(ngModel)]=\"editedConfig.hints!.uiModes.disabled\"\n (ngModelChange)=\"updateDirtyState('hints-ui-disabled')\" placeholder=\"Ex.: Fun\u00E7\u00E3o indispon\u00EDvel no momento.\"></textarea>\n </mat-form-field>\n <mat-form-field appearance=\"fill\" class=\"w-100\">\n <mat-label>Vis\u00EDvel</mat-label>\n <textarea matInput rows=\"2\" [(ngModel)]=\"editedConfig.hints!.uiModes.visible\"\n (ngModelChange)=\"updateDirtyState('hints-ui-visible')\" placeholder=\"Ex.: Campo exibido conforme permiss\u00F5es.\"></textarea>\n </mat-form-field>\n </section>\n </div>\n\n <div class=\"actions-row\">\n <button mat-stroked-button color=\"primary\" type=\"button\" (click)=\"restoreHintsDefaults()\">\n <mat-icon>restore</mat-icon>\n Restaurar padr\u00F5es\n </button>\n </div>\n </mat-card-content>\n </mat-card>\n </div>\n </mat-tab>\n <mat-tab label=\"A\u00E7\u00F5es\">\n <div class=\"tab-content\" data-testid=\"config-tab-panel-acoes\">\n <mat-card class=\"section-card\">\n <mat-card-content>\n <div class=\"section-header\">\n <div>\n <h3>A\u00E7\u00F5es do formul\u00E1rio</h3>\n <p class=\"text-caption\">Configure bot\u00F5es padr\u00E3o e a\u00E7\u00F5es customizadas.</p>\n </div>\n </div>\n <praxis-actions-editor [config]=\"editedConfig\" (configChange)=\"onConfigChange($event)\"></praxis-actions-editor>\n </mat-card-content>\n </mat-card>\n </div>\n </mat-tab>\n <mat-tab label=\"Regras\">\n <div class=\"tab-content visual-builder-content\" data-testid=\"config-tab-panel-regras\">\n <div class=\"builder-header\">\n <div>\n <h3>Regras visuais</h3>\n <p class=\"text-caption\">Defina visibilidade, estilos e rea\u00E7\u00F5es baseadas em condi\u00E7\u00F5es.</p>\n </div>\n </div>\n <praxis-visual-builder [config]=\"ruleBuilderConfig\" [initialRules]=\"ruleBuilderState\"\n (rulesChanged)=\"onRulesChanged($event)\"></praxis-visual-builder>\n </div>\n </mat-tab>\n <mat-tab label=\"Cascatas\">\n <div class=\"tab-content\" data-testid=\"config-tab-panel-cascatas\">\n <mat-card class=\"section-card\">\n <mat-card-content>\n <div class=\"section-header\">\n <div>\n <h3>Depend\u00EAncias entre campos</h3>\n <p class=\"text-caption\">Configure cascatas e carregamento entre campos.</p>\n </div>\n </div>\n <praxis-cascade-manager-tab [fields]=\"editedConfig.fieldMetadata || []\"\n (apply)=\"onCascadeApply($event)\"></praxis-cascade-manager-tab>\n </mat-card-content>\n </mat-card>\n </div>\n </mat-tab>\n <mat-tab label=\"Mensagens\">\n <div class=\"tab-content\" data-testid=\"config-tab-panel-mensagens\">\n <mat-card class=\"section-card\">\n <mat-card-content>\n <div class=\"section-header\">\n <div>\n <h3>Mensagens e feedback</h3>\n <p class=\"text-caption\">Padronize textos de sucesso, erro e confirma\u00E7\u00F5es.</p>\n </div>\n </div>\n <praxis-messages-editor [config]=\"editedConfig\" (configChange)=\"onConfigChange($event)\"></praxis-messages-editor>\n </mat-card-content>\n </mat-card>\n </div>\n </mat-tab>\n <mat-tab label=\"Navega\u00E7\u00E3o\">\n <div class=\"tab-content\" data-testid=\"config-tab-panel-navegacao\">\n <mat-card class=\"section-card\">\n <mat-card-content>\n <div class=\"section-header\">\n <div>\n <h3>Navega\u00E7\u00E3o de retorno</h3>\n <p class=\"text-caption\">Defina o comportamento do bot\u00E3o Voltar em fluxos CRUD.</p>\n </div>\n </div>\n <div class=\"form-grid\">\n <div class=\"form-row\">\n <mat-form-field appearance=\"fill\" class=\"w-100\">\n <mat-label>Rota de retorno</mat-label>\n <input matInput id=\"returnTo\" [(ngModel)]=\"backConfig!.returnTo\" (ngModelChange)=\"onBackConfigChange()\" placeholder=\"/funcionarios\" />\n </mat-form-field>\n </div>\n <div class=\"form-row\">\n <mat-slide-toggle [(ngModel)]=\"backConfig!.confirmOnDirty\" (ngModelChange)=\"onBackConfigChange()\">\n Confirmar ao sair com altera\u00E7\u00F5es\n </mat-slide-toggle>\n </div>\n <div class=\"block-status\" [class.block-status--implicit]=\"!isBackConfigBlockPersisted()\">\n <strong>Back Config</strong>\n <span *ngIf=\"isBackConfigBlockPersisted(); else backConfigMissing\">`contextSnapshot.backConfig` ser\u00E1 persistido no documento can\u00F4nico.</span>\n <ng-template #backConfigMissing>Bloco ausente no documento can\u00F4nico. Os valores exibidos s\u00E3o fallback visual e n\u00E3o ser\u00E3o salvos at\u00E9 voc\u00EA editar.</ng-template>\n </div>\n </div>\n <div class=\"section-help\">\n <span class=\"section-help__label\">Observa\u00E7\u00E3o:</span>\n <button\n mat-icon-button\n class=\"help-icon-button\"\n type=\"button\"\n [matTooltip]=\"'Afeta o fluxo de navega\u00E7\u00E3o do CRUD e pode ser sobreposto por metadados do recurso.'\"\n matTooltipPosition=\"above\"\n >\n <mat-icon>help_outline</mat-icon>\n </button>\n </div>\n </mat-card-content>\n </mat-card>\n </div>\n </mat-tab>\n <mat-tab label=\"JSON\">\n <div class=\"tab-content\" data-testid=\"config-tab-panel-json\">\n <mat-card class=\"section-card\">\n <mat-card-content>\n <div class=\"section-header\">\n <div>\n <h3>Configura\u00E7\u00E3o JSON</h3>\n <p class=\"text-caption\">Edi\u00E7\u00E3o avan\u00E7ada do documento can\u00F4nico de autoria do formul\u00E1rio.</p>\n </div>\n </div>\n <form-json-config-editor [document]=\"jsonDocument\" (documentChange)=\"onJsonConfigChange($event)\"\n (validationChange)=\"onJsonValidationChange($event)\" (editorEvent)=\"onJsonEditorEvent($event)\">\n </form-json-config-editor>\n </mat-card-content>\n </mat-card>\n </div>\n </mat-tab>\n</mat-tab-group>\n", styles: [":host{display:block;height:100%}.config-tabs{height:100%}::ng-deep .mat-mdc-tab-group.config-tabs{display:flex;flex-direction:column}::ng-deep .mat-mdc-tab-group.config-tabs .mat-mdc-tab-body-wrapper{flex:1;min-height:0}::ng-deep .mat-mdc-tab-group.config-tabs .mat-mdc-tab-body-content{height:100%;overflow:auto;display:flex;flex-direction:column;padding:0!important}.tab-content{padding:16px 20px 20px;flex:1}.form-row{display:block;margin-bottom:12px}.form-grid{display:grid;gap:12px}.section-help{display:inline-flex;align-items:center;gap:8px;color:var(--md-sys-color-on-surface-variant);font-size:12px}.section-help__label{font-weight:500}.help-icon-button{--mdc-icon-button-state-layer-size: 28px;--mdc-icon-button-icon-size: 18px;width:28px;height:28px;padding:0;display:inline-flex;align-items:center;justify-content:center}.help-icon-button mat-icon{font-size:18px;width:18px;height:18px}.grid-2-cols{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:16px}.grid-2-cols--compact{gap:12px}.w-100{width:100%}.visual-builder-content{padding:0!important;height:100%;overflow:hidden;display:flex;flex-direction:column}.visual-builder-content>praxis-visual-builder{flex:1;min-height:0}.json-field{width:100%}.presentation-layout{display:flex;gap:20px;align-items:flex-start}.presentation-controls{flex:1 1 60%;display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:14px 18px}.control{display:flex;flex-direction:column}.control-span{grid-column:1/-1}.block-status{grid-column:1/-1;display:flex;gap:8px;align-items:flex-start;padding:10px 12px;border-radius:10px;background:color-mix(in srgb,var(--md-sys-color-primary-container) 35%,transparent);color:var(--md-sys-color-on-surface);font-size:12px;line-height:1.45}.block-status strong{min-width:112px;font-size:11px;letter-spacing:.04em;text-transform:uppercase;color:var(--md-sys-color-on-surface-variant)}.block-status--implicit{background:color-mix(in srgb,var(--md-sys-color-surface-variant) 42%,transparent);border:1px dashed var(--md-sys-color-outline)}.block-status--readonly{margin-bottom:12px}.block-status code{font-family:var(--md-ref-typeface-mono, \"Courier New\", monospace);font-size:11px}.control__label{font-size:12px;color:var(--md-sys-color-on-surface-variant);margin-bottom:6px}.toggle-group{width:100%}.control--inline{grid-column:1/-1;display:flex;gap:16px;align-items:center}.presentation-preview{flex:0 0 360px;position:sticky;top:16px}.preview-card{border:1px solid var(--md-sys-color-outline);border-radius:12px;padding:14px;background:var(--md-sys-color-surface-container)}.preview-row{display:flex;gap:8px;padding:6px 0;border-bottom:1px dashed var(--md-sys-color-outline-variant)}.preview-row:last-child{border-bottom:none}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.presentation-preview .praxis-presentation__label{color:var(--md-sys-color-on-surface-variant)}.presentation-preview .praxis-presentation__value{color:var(--md-sys-color-on-surface)}.presentation-preview .presentation-mode.pres-density-cozy .preview-row{padding:6px 0}.presentation-preview .presentation-mode.pres-density-compact .preview-row,.presentation-preview .presentation-mode.pres-compact .preview-row{padding:2px 0}.presentation-preview .presentation-mode.pres-label-left .preview-row{display:grid;grid-template-columns:var(--pfx-pres-label-width, 140px) 1fr;align-items:baseline;column-gap:10px}.presentation-preview .presentation-mode.pres-label-left .praxis-presentation__label{margin:0;text-align:var(--pfx-pres-label-align, start)}.presentation-preview .presentation-mode .praxis-presentation__value{text-align:var(--pfx-pres-value-align, start)}.presentation-preview h4{margin:0 0 8px;color:var(--md-sys-color-on-surface);font-size:1rem}.section-card{border:1px solid var(--md-sys-color-outline-variant);background:var(--md-sys-color-surface-container);box-shadow:var(--md-sys-elevation-level1, none)}.section-header{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;margin-bottom:12px}.tab-content--dense{padding:12px 16px 16px}.tab-content--dense .section-header,.tab-content--dense .form-row{margin-bottom:8px}.section-header h3{margin:0;font-size:1.05rem;color:var(--md-sys-color-on-surface)}.text-caption{margin:6px 0 0;font-size:.9rem;color:var(--md-sys-color-on-surface-variant)}.section-header--compact{margin-bottom:8px}.section-body{display:block}.section-body--flex{flex:1;min-height:0;display:flex;flex-direction:column}.builder-header{padding:12px 16px;border-bottom:1px solid var(--md-sys-color-outline-variant);background:var(--md-sys-color-surface)}.builder-header h3{margin:0;font-size:1rem;color:var(--md-sys-color-on-surface)}.hint{margin:8px 0 0;padding:10px 12px;border-radius:10px;border:1px dashed var(--md-sys-color-outline-variant);background:var(--md-sys-color-surface-container);color:var(--md-sys-color-on-surface-variant)}.meta-card{display:grid;gap:6px;padding:10px 12px;border-radius:10px;border:1px solid var(--md-sys-color-outline-variant);background:var(--md-sys-color-surface);color:var(--md-sys-color-on-surface)}.meta-row{display:flex;align-items:center;justify-content:space-between;gap:12px;font-size:.9rem;color:var(--md-sys-color-on-surface-variant)}.meta-row strong{color:var(--md-sys-color-on-surface);font-weight:600}.runtime-contract-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:12px}.runtime-contract-field{display:grid;gap:6px;padding:12px;border-radius:10px;border:1px solid var(--md-sys-color-outline-variant);background:var(--md-sys-color-surface)}.runtime-contract-label{font-size:12px;color:var(--md-sys-color-on-surface-variant)}.runtime-contract-code{display:block;font-family:var(--md-ref-typeface-mono, \"Courier New\", monospace);font-size:12px;line-height:1.45;color:var(--md-sys-color-on-surface);word-break:break-word;white-space:normal}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$3.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$3.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1$3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$3.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i1$3.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: MatTabsModule }, { kind: "component", type: i7$3.MatTab, selector: "mat-tab", inputs: ["disabled", "label", "aria-label", "aria-labelledby", "labelClass", "bodyClass", "id"], exportAs: ["matTab"] }, { kind: "component", type: i7$3.MatTabGroup, selector: "mat-tab-group", inputs: ["color", "fitInkBarToContent", "mat-stretch-tabs", "mat-align-tabs", "dynamicHeight", "selectedIndex", "headerPosition", "animationDuration", "contentTabIndex", "disablePagination", "disableRipple", "preserveContent", "backgroundColor", "aria-label", "aria-labelledby"], outputs: ["selectedIndexChange", "focusChange", "animationDone", "selectedTabChange"], exportAs: ["matTabGroup"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i5.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatCardModule }, { kind: "component", type: i8.MatCard, selector: "mat-card", inputs: ["appearance"], exportAs: ["matCard"] }, { kind: "directive", type: i8.MatCardContent, selector: "mat-card-content" }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i4.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i4.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i4$1.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i4$1.MatLabel, selector: "mat-label" }, { kind: "directive", type: i4$1.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i6$3.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i6$3.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i7$1.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatCheckboxModule }, { kind: "component", type: i12$1.MatCheckbox, selector: "mat-checkbox", inputs: ["aria-label", "aria-labelledby", "aria-describedby", "aria-expanded", "aria-controls", "aria-owns", "id", "required", "labelPosition", "name", "value", "disableRipple", "tabIndex", "color", "disabledInteractive", "checked", "disabled", "indeterminate"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }, { kind: "ngmodule", type: MatSlideToggleModule }, { kind: "component", type: i6$2.MatSlideToggle, selector: "mat-slide-toggle", inputs: ["name", "id", "labelPosition", "aria-label", "aria-labelledby", "aria-describedby", "required", "color", "disabled", "disableRipple", "tabIndex", "checked", "hideIcon", "disabledInteractive"], outputs: ["change", "toggleChange"], exportAs: ["matSlideToggle"] }, { kind: "ngmodule", type: MatButtonToggleModule }, { kind: "directive", type: i7$2.MatButtonToggleGroup, selector: "mat-button-toggle-group", inputs: ["appearance", "name", "vertical", "value", "multiple", "disabled", "disabledInteractive", "hideSingleSelectionIndicator", "hideMultipleSelectionIndicator"], outputs: ["valueChange", "change"], exportAs: ["matButtonToggleGroup"] }, { kind: "component", type: i7$2.MatButtonToggle, selector: "mat-button-toggle", inputs: ["aria-label", "aria-labelledby", "id", "name", "value", "tabIndex", "disableRipple", "appearance", "checked", "disabled", "disabledInteractive"], outputs: ["change"], exportAs: ["matButtonToggle"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i9.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: JsonConfigEditorComponent, selector: "form-json-config-editor", inputs: ["config", "document"], outputs: ["configChange", "documentChange", "validationChange", "editorEvent"] }, { kind: "component", type: LayoutEditorComponent, selector: "praxis-layout-editor", inputs: ["config"], outputs: ["configChange", "select"] }, { kind: "component", type: BehaviorEditorComponent, selector: "praxis-behavior-editor", inputs: ["config"], outputs: ["configChange"] }, { kind: "component", type: ActionsEditorComponent$1, selector: "praxis-actions-editor", inputs: ["config"], outputs: ["configChange"] }, { kind: "component", type: MessagesEditorComponent, selector: "praxis-messages-editor", inputs: ["config"], outputs: ["configChange"] }, { kind: "component", type: HooksEditorComponent, selector: "praxis-hooks-editor", inputs: ["config"], outputs: ["configChange"] }, { kind: "component", type: PraxisVisualBuilder, selector: "praxis-visual-builder", inputs: ["config", "initialRules"], outputs: ["rulesChanged", "exportRequested", "importRequested"] }, { kind: "component", type: CascadeManagerTabComponent, selector: "praxis-cascade-manager-tab", inputs: ["fields", "connections"], outputs: ["apply", "cancel"] }] });
19691
19709
  }
19692
19710
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisDynamicFormConfigEditor, decorators: [{
@@ -19719,6 +19737,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
19719
19737
  args: [ASYNC_CONFIG_STORAGE]
19720
19738
  }] }, { type: FormConfigService }, { type: i7.SettingsPanelService }, { type: i0.ChangeDetectorRef }, { type: undefined, decorators: [{
19721
19739
  type: Optional
19740
+ }, {
19741
+ type: Inject,
19742
+ args: [GLOBAL_ACTION_CATALOG]
19743
+ }] }, { type: undefined, decorators: [{
19744
+ type: Optional
19722
19745
  }, {
19723
19746
  type: Inject,
19724
19747
  args: [SETTINGS_PANEL_DATA]
@@ -20087,6 +20110,7 @@ const PRAXIS_DYNAMIC_FORM_COMPONENT_METADATA = {
20087
20110
  inputs: [
20088
20111
  { name: 'resourcePath', type: 'string', label: 'Recurso', description: 'Recurso base para CRUD/schemas' },
20089
20112
  { name: 'resourceId', type: 'string | number', label: 'ID do Registro', description: 'Identificador do registro' },
20113
+ { name: 'initialValue', type: 'Record<string, unknown> | null', label: 'Valor inicial', description: 'Valores iniciais locais aplicados antes da interacao do usuario' },
20090
20114
  { name: 'editorialContext', type: 'Record<string, unknown> | null', label: 'Contexto editorial', description: 'Contexto compartilhado usado para resolver bindings do rich content hospedado no form' },
20091
20115
  { name: 'mode', type: "'create' | 'edit' | 'view'", default: 'create', label: 'Modo', description: 'Modo do formulario' },
20092
20116
  {
@@ -20533,9 +20557,7 @@ class SectionEditorComponent {
20533
20557
  isBusy$ = new BehaviorSubject(false);
20534
20558
  globalActionCatalogSource = inject(GLOBAL_ACTION_CATALOG, { optional: true }) ?? [];
20535
20559
  i18n = inject(PraxisI18nService);
20536
- legacyActionSpecs = GLOBAL_ACTION_SPEC_CATALOG;
20537
20560
  globalActionCatalog = buildGlobalActionCatalog({
20538
- legacySpecs: this.legacyActionSpecs,
20539
20561
  injectedCatalog: getGlobalActionCatalog(this.globalActionCatalogSource),
20540
20562
  fallbackCatalog: PRAXIS_GLOBAL_ACTION_CATALOG,
20541
20563
  mapCatalogEntry: (entry) => this.mapCatalogEntryToActionSpec(entry),
@@ -20642,63 +20664,66 @@ class SectionEditorComponent {
20642
20664
  getHeaderActionSpecById(id) {
20643
20665
  return this.globalActionCatalog.find((item) => item.id === id);
20644
20666
  }
20645
- getHeaderActionSelectValue(value) {
20646
- const parsed = parseActionValue(value, this.globalActionCatalog, this.customActionValue);
20647
- return parsed.isGlobal ? parsed.id : this.customActionValue;
20667
+ getHeaderActionSelectValue(value, globalAction) {
20668
+ return globalAction?.actionId || this.customActionValue;
20648
20669
  }
20649
- getHeaderActionParam(value) {
20650
- const parsed = parseActionValue(value, this.globalActionCatalog, this.customActionValue);
20651
- return parsed.isGlobal ? parsed.param : '';
20670
+ getHeaderActionParam(value, globalAction) {
20671
+ const payload = globalAction?.payload;
20672
+ if (payload === undefined || payload === null)
20673
+ return '';
20674
+ return typeof payload === 'string' ? payload : JSON.stringify(payload, null, 2);
20652
20675
  }
20653
- getHeaderActionCustomValue(value) {
20654
- const parsed = parseActionValue(value, this.globalActionCatalog, this.customActionValue);
20655
- return parsed.isGlobal ? '' : parsed.raw;
20676
+ getHeaderActionCustomValue(value, globalAction) {
20677
+ return globalAction ? '' : value || '';
20656
20678
  }
20657
- getHeaderGlobalActionSchema(value) {
20658
- const parsed = parseActionValue(value, this.globalActionCatalog, this.customActionValue);
20659
- if (!parsed.isGlobal)
20660
- return undefined;
20661
- return getGlobalActionUiSchema(parsed.id);
20679
+ getHeaderGlobalActionSchema(value, globalAction) {
20680
+ return getGlobalActionUiSchema(globalAction?.actionId);
20662
20681
  }
20663
- getHeaderSurfaceOpenActionPayload(value) {
20664
- const info = getActionParamInfo(value, this.globalActionCatalog, this.customActionValue);
20665
- const payload = info.isJson && info.json && typeof info.json === 'object' && !Array.isArray(info.json)
20666
- ? info.json
20667
- : undefined;
20668
- return normalizeSurfaceOpenPayload(payload);
20682
+ getHeaderSurfaceOpenActionPayload(value, globalAction) {
20683
+ return normalizeSurfaceOpenPayload(globalAction?.payload);
20669
20684
  }
20670
- isHeaderActionParamMissing(value) {
20671
- return isRequiredParamMissing(value, this.globalActionCatalog, this.customActionValue);
20685
+ isHeaderActionParamMissing(value, globalAction) {
20686
+ return isRequiredParamMissing(globalAction, this.globalActionCatalog);
20672
20687
  }
20673
20688
  onHeaderActionSelectChange(index, value) {
20674
20689
  const group = this.headerActionsArray.at(index);
20675
20690
  if (!group)
20676
20691
  return;
20677
- const currentValue = String(group.get('action')?.value || '');
20678
20692
  if (value === this.customActionValue) {
20679
- const parsed = parseActionValue(currentValue, this.globalActionCatalog, this.customActionValue);
20680
- if (parsed.isGlobal) {
20681
- group.get('action')?.setValue('');
20682
- }
20693
+ group.get('globalAction')?.setValue(undefined);
20683
20694
  return;
20684
20695
  }
20685
- const parsed = parseActionValue(currentValue, this.globalActionCatalog, this.customActionValue);
20686
- const param = parsed.isGlobal && parsed.id === value ? parsed.param : '';
20687
- group.get('action')?.setValue(param ? `${value}:${param}` : value);
20696
+ const current = group.get('globalAction')?.value;
20697
+ const payload = current?.actionId === value ? current.payload : undefined;
20698
+ group.get('globalAction')?.setValue(payload !== undefined ? { actionId: value, payload } : { actionId: value });
20699
+ group.get('action')?.setValue('');
20688
20700
  }
20689
20701
  onHeaderActionParamChange(index, value) {
20690
20702
  const group = this.headerActionsArray.at(index);
20691
20703
  if (!group)
20692
20704
  return;
20693
- const parsed = parseActionValue(String(group.get('action')?.value || ''), this.globalActionCatalog, this.customActionValue);
20694
- const id = parsed.isGlobal ? parsed.id : '';
20695
- group.get('action')?.setValue(id ? (value ? `${id}:${value}` : id) : value);
20705
+ const current = group.get('globalAction')?.value;
20706
+ const id = current?.actionId;
20707
+ if (!id)
20708
+ return;
20709
+ const trimmed = String(value || '').trim();
20710
+ if (!trimmed) {
20711
+ group.get('globalAction')?.setValue({ actionId: id });
20712
+ return;
20713
+ }
20714
+ try {
20715
+ group.get('globalAction')?.setValue({ actionId: id, payload: JSON.parse(trimmed) });
20716
+ }
20717
+ catch {
20718
+ group.get('globalAction')?.setValue({ actionId: id, payload: trimmed });
20719
+ }
20696
20720
  }
20697
20721
  onHeaderActionCustomChange(index, value) {
20698
20722
  const group = this.headerActionsArray.at(index);
20699
20723
  if (!group)
20700
20724
  return;
20701
20725
  group.get('action')?.setValue(value);
20726
+ group.get('globalAction')?.setValue(undefined);
20702
20727
  }
20703
20728
  async pickHeaderActionIcon(index) {
20704
20729
  const group = this.headerActionsArray.at(index);
@@ -20791,7 +20816,11 @@ class SectionEditorComponent {
20791
20816
  const group = this.headerActionsArray.at(index);
20792
20817
  if (!group)
20793
20818
  return;
20794
- group.get('action')?.setValue(`surface.open:${JSON.stringify(normalizeSurfaceOpenPayload(payload))}`);
20819
+ group.get('globalAction')?.setValue({
20820
+ actionId: 'surface.open',
20821
+ payload: normalizeSurfaceOpenPayload(payload),
20822
+ });
20823
+ group.get('action')?.setValue('');
20795
20824
  }
20796
20825
  async pickIcon() {
20797
20826
  const current = this.form.value.icon;
@@ -21009,6 +21038,7 @@ class SectionEditorComponent {
21009
21038
  label: [action?.label ?? ''],
21010
21039
  icon: [action?.icon ?? ''],
21011
21040
  action: [action?.action ?? ''],
21041
+ globalAction: [action?.globalAction],
21012
21042
  tooltip: [action?.tooltip ?? ''],
21013
21043
  color: [action?.color ?? ''],
21014
21044
  visible: [action?.visible ?? true],
@@ -21052,6 +21082,7 @@ class SectionEditorComponent {
21052
21082
  icon,
21053
21083
  };
21054
21084
  const action = this.normalizeOptionalString(item?.action);
21085
+ const globalAction = this.normalizeHeaderGlobalAction(item?.globalAction);
21055
21086
  const tooltip = this.normalizeOptionalString(item?.tooltip);
21056
21087
  const normalizedColor = color === 'primary' ||
21057
21088
  color === 'accent' ||
@@ -21061,8 +21092,12 @@ class SectionEditorComponent {
21061
21092
  : undefined;
21062
21093
  const className = this.normalizeOptionalString(item?.className);
21063
21094
  const style = this.parseHeaderActionStyle(item?.styleJson ?? item?.style);
21064
- if (action !== undefined)
21095
+ if (globalAction !== undefined) {
21096
+ nextAction.globalAction = globalAction;
21097
+ }
21098
+ else if (action !== undefined) {
21065
21099
  nextAction.action = action;
21100
+ }
21066
21101
  if (tooltip !== undefined)
21067
21102
  nextAction.tooltip = tooltip;
21068
21103
  if (normalizedColor !== undefined)
@@ -21105,6 +21140,15 @@ class SectionEditorComponent {
21105
21140
  }
21106
21141
  return spec?.description || '';
21107
21142
  }
21143
+ normalizeHeaderGlobalAction(value) {
21144
+ if (!value || typeof value !== 'object')
21145
+ return undefined;
21146
+ const actionId = this.normalizeOptionalString(value.actionId);
21147
+ if (!actionId || !isGlobalActionId(actionId, this.globalActionCatalog))
21148
+ return undefined;
21149
+ const payload = value.payload;
21150
+ return payload === undefined ? { actionId } : { actionId, payload };
21151
+ }
21108
21152
  headerActionStyleValidator = (control) => {
21109
21153
  const rawValue = control.value;
21110
21154
  if (rawValue === null || rawValue === undefined || rawValue === '') {
@@ -21464,7 +21508,7 @@ class SectionEditorComponent {
21464
21508
  <mat-form-field appearance="fill">
21465
21509
  <mat-label>{{ tx('headerActions.fields.action', 'Ação') }}</mat-label>
21466
21510
  <mat-select
21467
- [ngModel]="getHeaderActionSelectValue(actionGroup.get('action')?.value)"
21511
+ [ngModel]="getHeaderActionSelectValue(actionGroup.get('action')?.value, actionGroup.get('globalAction')?.value)"
21468
21512
  (ngModelChange)="onHeaderActionSelectChange(index, $event)"
21469
21513
  [ngModelOptions]="{ standalone: true }"
21470
21514
  >
@@ -21488,23 +21532,23 @@ class SectionEditorComponent {
21488
21532
  </button>
21489
21533
  </mat-form-field>
21490
21534
 
21491
- @if (getHeaderActionSelectValue(actionGroup.get('action')?.value) === customActionValue) {
21535
+ @if (getHeaderActionSelectValue(actionGroup.get('action')?.value, actionGroup.get('globalAction')?.value) === customActionValue) {
21492
21536
  <mat-form-field appearance="fill">
21493
21537
  <mat-label>{{ tx('headerActions.fields.customAction', 'Action customizada') }}</mat-label>
21494
21538
  <input
21495
21539
  matInput
21496
- [ngModel]="getHeaderActionCustomValue(actionGroup.get('action')?.value)"
21540
+ [ngModel]="getHeaderActionCustomValue(actionGroup.get('action')?.value, actionGroup.get('globalAction')?.value)"
21497
21541
  (ngModelChange)="onHeaderActionCustomChange(index, $event)"
21498
21542
  [ngModelOptions]="{ standalone: true }"
21499
21543
  [placeholder]="tx('headerActions.placeholders.action', 'Opcional. Se vazio, usa o ID')"
21500
21544
  />
21501
21545
  </mat-form-field>
21502
- } @else if (getHeaderActionSpecById(getHeaderActionSelectValue(actionGroup.get('action')?.value)); as actionSpec) {
21503
- @if (getHeaderGlobalActionSchema(actionGroup.get('action')?.value); as schema) {
21546
+ } @else if (getHeaderActionSpecById(getHeaderActionSelectValue(actionGroup.get('action')?.value, actionGroup.get('globalAction')?.value)); as actionSpec) {
21547
+ @if (getHeaderGlobalActionSchema(actionGroup.get('action')?.value, actionGroup.get('globalAction')?.value); as schema) {
21504
21548
  @if (schema.editorMode === 'surface-open') {
21505
21549
  <div class="row-1">
21506
21550
  <praxis-surface-open-action-editor
21507
- [value]="getHeaderSurfaceOpenActionPayload(actionGroup.get('action')?.value)"
21551
+ [value]="getHeaderSurfaceOpenActionPayload(actionGroup.get('action')?.value, actionGroup.get('globalAction')?.value)"
21508
21552
  hostKind="form"
21509
21553
  (valueChange)="onHeaderSurfaceOpenActionPayloadChange(index, $event)"
21510
21554
  ></praxis-surface-open-action-editor>
@@ -21514,7 +21558,7 @@ class SectionEditorComponent {
21514
21558
  <mat-label>{{ actionSpec.param.label || tx('headerActions.fields.actionParam', 'Parâmetro') }}</mat-label>
21515
21559
  <input
21516
21560
  matInput
21517
- [ngModel]="getHeaderActionParam(actionGroup.get('action')?.value)"
21561
+ [ngModel]="getHeaderActionParam(actionGroup.get('action')?.value, actionGroup.get('globalAction')?.value)"
21518
21562
  (ngModelChange)="onHeaderActionParamChange(index, $event)"
21519
21563
  [ngModelOptions]="{ standalone: true }"
21520
21564
  [placeholder]="actionSpec.param.placeholder || ''"
@@ -21523,7 +21567,7 @@ class SectionEditorComponent {
21523
21567
  @if (actionSpec.param.hint) {
21524
21568
  <mat-hint>{{ actionSpec.param.hint }}</mat-hint>
21525
21569
  }
21526
- @if (actionSpec.param.required && isHeaderActionParamMissing(actionGroup.get('action')?.value)) {
21570
+ @if (actionSpec.param.required && isHeaderActionParamMissing(actionGroup.get('action')?.value, actionGroup.get('globalAction')?.value)) {
21527
21571
  <mat-error>{{ tx('headerActions.validation.parameterRequired', 'Parâmetro obrigatório.') }}</mat-error>
21528
21572
  }
21529
21573
  </mat-form-field>
@@ -21533,7 +21577,7 @@ class SectionEditorComponent {
21533
21577
  <mat-label>{{ actionSpec.param.label || tx('headerActions.fields.actionParam', 'Parâmetro') }}</mat-label>
21534
21578
  <input
21535
21579
  matInput
21536
- [ngModel]="getHeaderActionParam(actionGroup.get('action')?.value)"
21580
+ [ngModel]="getHeaderActionParam(actionGroup.get('action')?.value, actionGroup.get('globalAction')?.value)"
21537
21581
  (ngModelChange)="onHeaderActionParamChange(index, $event)"
21538
21582
  [ngModelOptions]="{ standalone: true }"
21539
21583
  [placeholder]="actionSpec.param.placeholder || ''"
@@ -21542,7 +21586,7 @@ class SectionEditorComponent {
21542
21586
  @if (actionSpec.param.hint) {
21543
21587
  <mat-hint>{{ actionSpec.param.hint }}</mat-hint>
21544
21588
  }
21545
- @if (actionSpec.param.required && isHeaderActionParamMissing(actionGroup.get('action')?.value)) {
21589
+ @if (actionSpec.param.required && isHeaderActionParamMissing(actionGroup.get('action')?.value, actionGroup.get('globalAction')?.value)) {
21546
21590
  <mat-error>{{ tx('headerActions.validation.parameterRequired', 'Parâmetro obrigatório.') }}</mat-error>
21547
21591
  }
21548
21592
  </mat-form-field>
@@ -21555,7 +21599,7 @@ class SectionEditorComponent {
21555
21599
  </mat-form-field>
21556
21600
  </div>
21557
21601
 
21558
- @if (getHeaderActionSelectValue(actionGroup.get('action')?.value) !== customActionValue) {
21602
+ @if (getHeaderActionSelectValue(actionGroup.get('action')?.value, actionGroup.get('globalAction')?.value) !== customActionValue) {
21559
21603
  <div class="header-action-callout">
21560
21604
  <div class="header-action-callout-title">
21561
21605
  {{ tx('headerActions.callout.globalTitle', 'Ação global do app') }}
@@ -22136,7 +22180,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
22136
22180
  <mat-form-field appearance="fill">
22137
22181
  <mat-label>{{ tx('headerActions.fields.action', 'Ação') }}</mat-label>
22138
22182
  <mat-select
22139
- [ngModel]="getHeaderActionSelectValue(actionGroup.get('action')?.value)"
22183
+ [ngModel]="getHeaderActionSelectValue(actionGroup.get('action')?.value, actionGroup.get('globalAction')?.value)"
22140
22184
  (ngModelChange)="onHeaderActionSelectChange(index, $event)"
22141
22185
  [ngModelOptions]="{ standalone: true }"
22142
22186
  >
@@ -22160,23 +22204,23 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
22160
22204
  </button>
22161
22205
  </mat-form-field>
22162
22206
 
22163
- @if (getHeaderActionSelectValue(actionGroup.get('action')?.value) === customActionValue) {
22207
+ @if (getHeaderActionSelectValue(actionGroup.get('action')?.value, actionGroup.get('globalAction')?.value) === customActionValue) {
22164
22208
  <mat-form-field appearance="fill">
22165
22209
  <mat-label>{{ tx('headerActions.fields.customAction', 'Action customizada') }}</mat-label>
22166
22210
  <input
22167
22211
  matInput
22168
- [ngModel]="getHeaderActionCustomValue(actionGroup.get('action')?.value)"
22212
+ [ngModel]="getHeaderActionCustomValue(actionGroup.get('action')?.value, actionGroup.get('globalAction')?.value)"
22169
22213
  (ngModelChange)="onHeaderActionCustomChange(index, $event)"
22170
22214
  [ngModelOptions]="{ standalone: true }"
22171
22215
  [placeholder]="tx('headerActions.placeholders.action', 'Opcional. Se vazio, usa o ID')"
22172
22216
  />
22173
22217
  </mat-form-field>
22174
- } @else if (getHeaderActionSpecById(getHeaderActionSelectValue(actionGroup.get('action')?.value)); as actionSpec) {
22175
- @if (getHeaderGlobalActionSchema(actionGroup.get('action')?.value); as schema) {
22218
+ } @else if (getHeaderActionSpecById(getHeaderActionSelectValue(actionGroup.get('action')?.value, actionGroup.get('globalAction')?.value)); as actionSpec) {
22219
+ @if (getHeaderGlobalActionSchema(actionGroup.get('action')?.value, actionGroup.get('globalAction')?.value); as schema) {
22176
22220
  @if (schema.editorMode === 'surface-open') {
22177
22221
  <div class="row-1">
22178
22222
  <praxis-surface-open-action-editor
22179
- [value]="getHeaderSurfaceOpenActionPayload(actionGroup.get('action')?.value)"
22223
+ [value]="getHeaderSurfaceOpenActionPayload(actionGroup.get('action')?.value, actionGroup.get('globalAction')?.value)"
22180
22224
  hostKind="form"
22181
22225
  (valueChange)="onHeaderSurfaceOpenActionPayloadChange(index, $event)"
22182
22226
  ></praxis-surface-open-action-editor>
@@ -22186,7 +22230,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
22186
22230
  <mat-label>{{ actionSpec.param.label || tx('headerActions.fields.actionParam', 'Parâmetro') }}</mat-label>
22187
22231
  <input
22188
22232
  matInput
22189
- [ngModel]="getHeaderActionParam(actionGroup.get('action')?.value)"
22233
+ [ngModel]="getHeaderActionParam(actionGroup.get('action')?.value, actionGroup.get('globalAction')?.value)"
22190
22234
  (ngModelChange)="onHeaderActionParamChange(index, $event)"
22191
22235
  [ngModelOptions]="{ standalone: true }"
22192
22236
  [placeholder]="actionSpec.param.placeholder || ''"
@@ -22195,7 +22239,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
22195
22239
  @if (actionSpec.param.hint) {
22196
22240
  <mat-hint>{{ actionSpec.param.hint }}</mat-hint>
22197
22241
  }
22198
- @if (actionSpec.param.required && isHeaderActionParamMissing(actionGroup.get('action')?.value)) {
22242
+ @if (actionSpec.param.required && isHeaderActionParamMissing(actionGroup.get('action')?.value, actionGroup.get('globalAction')?.value)) {
22199
22243
  <mat-error>{{ tx('headerActions.validation.parameterRequired', 'Parâmetro obrigatório.') }}</mat-error>
22200
22244
  }
22201
22245
  </mat-form-field>
@@ -22205,7 +22249,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
22205
22249
  <mat-label>{{ actionSpec.param.label || tx('headerActions.fields.actionParam', 'Parâmetro') }}</mat-label>
22206
22250
  <input
22207
22251
  matInput
22208
- [ngModel]="getHeaderActionParam(actionGroup.get('action')?.value)"
22252
+ [ngModel]="getHeaderActionParam(actionGroup.get('action')?.value, actionGroup.get('globalAction')?.value)"
22209
22253
  (ngModelChange)="onHeaderActionParamChange(index, $event)"
22210
22254
  [ngModelOptions]="{ standalone: true }"
22211
22255
  [placeholder]="actionSpec.param.placeholder || ''"
@@ -22214,7 +22258,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
22214
22258
  @if (actionSpec.param.hint) {
22215
22259
  <mat-hint>{{ actionSpec.param.hint }}</mat-hint>
22216
22260
  }
22217
- @if (actionSpec.param.required && isHeaderActionParamMissing(actionGroup.get('action')?.value)) {
22261
+ @if (actionSpec.param.required && isHeaderActionParamMissing(actionGroup.get('action')?.value, actionGroup.get('globalAction')?.value)) {
22218
22262
  <mat-error>{{ tx('headerActions.validation.parameterRequired', 'Parâmetro obrigatório.') }}</mat-error>
22219
22263
  }
22220
22264
  </mat-form-field>
@@ -22227,7 +22271,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
22227
22271
  </mat-form-field>
22228
22272
  </div>
22229
22273
 
22230
- @if (getHeaderActionSelectValue(actionGroup.get('action')?.value) !== customActionValue) {
22274
+ @if (getHeaderActionSelectValue(actionGroup.get('action')?.value, actionGroup.get('globalAction')?.value) !== customActionValue) {
22231
22275
  <div class="header-action-callout">
22232
22276
  <div class="header-action-callout-title">
22233
22277
  {{ tx('headerActions.callout.globalTitle', 'Ação global do app') }}
@@ -23591,6 +23635,12 @@ const FORM_COMPONENT_AI_CAPABILITIES = {
23591
23635
  valueKind: 'string',
23592
23636
  description: 'ID do registro atual (string ou number).',
23593
23637
  },
23638
+ {
23639
+ path: 'inputs.initialValue',
23640
+ category: 'inputs',
23641
+ valueKind: 'object',
23642
+ description: 'Valores iniciais locais para campos host-authored ou complementares.',
23643
+ },
23594
23644
  {
23595
23645
  path: 'inputs.mode',
23596
23646
  category: 'inputs',
@@ -23991,6 +24041,787 @@ const FORM_COMPONENT_AI_CAPABILITIES = {
23991
24041
  ],
23992
24042
  };
23993
24043
 
24044
+ /**
24045
+ * Manifesto de authoring canônico para o componente praxis-dynamic-form.
24046
+ * Este arquivo define as operações permitidas pela IA para editar formulários.
24047
+ *
24048
+ * @version 1.4.0
24049
+ * @status COMPLIANT - Alinhado com contrato v2, gate de aceitação e semântica local/schema-backed.
24050
+ */
24051
+ const PRAXIS_DYNAMIC_FORM_AUTHORING_MANIFEST = {
24052
+ schemaVersion: '1.0.0',
24053
+ componentId: 'praxis-dynamic-form',
24054
+ ownerPackage: '@praxisui/dynamic-form',
24055
+ configSchemaId: 'FormConfig',
24056
+ manifestVersion: '1.4.0',
24057
+ runtimeInputs: [
24058
+ { name: 'config', type: 'FormConfig', description: 'Configuração completa do formulário' },
24059
+ { name: 'resourcePath', type: 'string', description: 'Caminho do recurso (API)' },
24060
+ { name: 'mode', type: 'string', allowedValues: ['create', 'edit', 'view'], description: 'Modo de operação' },
24061
+ { name: 'enableCustomization', type: 'boolean', description: 'Habilita edição visual' }
24062
+ ],
24063
+ editableTargets: [
24064
+ /**
24065
+ * 'field' é o target genérico para operações de metadado (label, placeholder, etc.).
24066
+ * Para distinguir schema-backed de local, use 'schemaBackedField' ou 'localField'.
24067
+ */
24068
+ { kind: 'field', resolver: 'field-by-name-or-label', description: 'Campos do formulário (metadados schema-backed ou local; use para ops de propriedade)' },
24069
+ { kind: 'schemaBackedField', resolver: 'field-by-name-or-label', description: 'Campos com origem no schema do backend; não removíveis via authoring' },
24070
+ { kind: 'localField', resolver: 'field-by-name', description: 'Campos locais com source:"local"; adicionados/removidos via authoring' },
24071
+ { kind: 'section', resolver: 'section-by-id-or-title', description: 'Seções de layout identificadas por id estável' },
24072
+ { kind: 'row', resolver: 'row-by-id-in-section', description: 'Linhas dentro de seções, identificadas por id estável' },
24073
+ { kind: 'column', resolver: 'column-by-id-in-row', description: 'Colunas dentro de linhas, identificadas por id estável' },
24074
+ /**
24075
+ * 'layoutPlacement' representa a posição de um campo no layout (seção+linha+coluna).
24076
+ * Usado conceitualmente por layout.field.move; declarado como target para futuras ops
24077
+ * de reposicionamento granular (ex: layout.placement.swap).
24078
+ */
24079
+ { kind: 'layoutPlacement', resolver: 'layout-placement-by-field', description: 'Posicionamento de campo no layout; target para ops de reposicionamento' },
24080
+ { kind: 'formAction', resolver: 'action-by-id-or-label', description: 'Ações da toolbar (submit, cancel, reset)' },
24081
+ { kind: 'formRule', resolver: 'rule-by-id-or-name', description: 'Regras de visibilidade e validação' },
24082
+ /**
24083
+ * 'message' declara suporte a mensagens inline de formulário (info, warning, error).
24084
+ * Declarado aqui para conformidade com o gate; ops de criação/remoção de messages
24085
+ * serão adicionadas na próxima versão do manifesto.
24086
+ */
24087
+ { kind: 'message', resolver: 'message-by-id', description: 'Mensagens inline do formulário (info, warning, error); ops em desenvolvimento' }
24088
+ ],
24089
+ operations: [
24090
+ // ─── FIELD PROPERTY OPERATIONS ───────────────────────────────────────────
24091
+ {
24092
+ operationId: 'field.label.set',
24093
+ title: 'Alterar rótulo do campo',
24094
+ scope: 'field',
24095
+ targetKind: 'field',
24096
+ target: { kind: 'field', resolver: 'field-by-name-or-label', ambiguityPolicy: 'fail', required: true },
24097
+ inputSchema: {
24098
+ type: 'object',
24099
+ required: ['label'],
24100
+ properties: { label: { type: 'string', minLength: 1 } }
24101
+ },
24102
+ effects: [{ kind: 'merge-by-key', path: 'fieldMetadata[]', key: 'name' }],
24103
+ validators: ['field-exists'],
24104
+ affectedPaths: ['fieldMetadata[].label'],
24105
+ submissionImpact: false,
24106
+ preconditions: ['config-initialized', 'target-exists']
24107
+ },
24108
+ {
24109
+ operationId: 'field.placeholder.set',
24110
+ title: 'Alterar placeholder do campo',
24111
+ scope: 'field',
24112
+ targetKind: 'field',
24113
+ target: { kind: 'field', resolver: 'field-by-name-or-label', ambiguityPolicy: 'fail', required: true },
24114
+ inputSchema: {
24115
+ type: 'object',
24116
+ required: ['placeholder'],
24117
+ properties: { placeholder: { type: 'string' } }
24118
+ },
24119
+ effects: [{ kind: 'merge-by-key', path: 'fieldMetadata[]', key: 'name' }],
24120
+ validators: ['field-exists'],
24121
+ affectedPaths: ['fieldMetadata[].placeholder'],
24122
+ submissionImpact: false,
24123
+ preconditions: ['config-initialized', 'target-exists']
24124
+ },
24125
+ {
24126
+ operationId: 'field.required.set',
24127
+ title: 'Definir obrigatoriedade',
24128
+ scope: 'field',
24129
+ targetKind: 'field',
24130
+ target: { kind: 'field', resolver: 'field-by-name-or-label', ambiguityPolicy: 'fail', required: true },
24131
+ inputSchema: {
24132
+ type: 'object',
24133
+ required: ['required'],
24134
+ properties: { required: { type: 'boolean' } }
24135
+ },
24136
+ effects: [{ kind: 'merge-by-key', path: 'fieldMetadata[]', key: 'name' }],
24137
+ validators: ['field-exists'],
24138
+ affectedPaths: ['fieldMetadata[].required'],
24139
+ submissionImpact: false,
24140
+ preconditions: ['config-initialized', 'target-exists']
24141
+ },
24142
+ {
24143
+ operationId: 'field.readOnly.set',
24144
+ title: 'Definir campo como somente leitura',
24145
+ scope: 'field',
24146
+ targetKind: 'field',
24147
+ target: { kind: 'field', resolver: 'field-by-name-or-label', ambiguityPolicy: 'fail', required: true },
24148
+ inputSchema: {
24149
+ type: 'object',
24150
+ required: ['readOnly'],
24151
+ properties: { readOnly: { type: 'boolean' } }
24152
+ },
24153
+ effects: [{ kind: 'merge-by-key', path: 'fieldMetadata[]', key: 'name' }],
24154
+ validators: ['field-exists'],
24155
+ affectedPaths: ['fieldMetadata[].readOnly'],
24156
+ submissionImpact: false,
24157
+ preconditions: ['config-initialized', 'target-exists']
24158
+ },
24159
+ {
24160
+ operationId: 'field.disabled.set',
24161
+ title: 'Definir campo como desabilitado',
24162
+ scope: 'field',
24163
+ targetKind: 'field',
24164
+ target: { kind: 'field', resolver: 'field-by-name-or-label', ambiguityPolicy: 'fail', required: true },
24165
+ inputSchema: {
24166
+ type: 'object',
24167
+ required: ['disabled'],
24168
+ properties: { disabled: { type: 'boolean' } }
24169
+ },
24170
+ effects: [{ kind: 'merge-by-key', path: 'fieldMetadata[]', key: 'name' }],
24171
+ validators: ['field-exists'],
24172
+ affectedPaths: ['fieldMetadata[].disabled'],
24173
+ submissionImpact: false,
24174
+ preconditions: ['config-initialized', 'target-exists']
24175
+ },
24176
+ {
24177
+ operationId: 'field.hidden.set',
24178
+ title: 'Definir visibilidade inicial',
24179
+ scope: 'field',
24180
+ targetKind: 'field',
24181
+ target: { kind: 'field', resolver: 'field-by-name-or-label', ambiguityPolicy: 'fail', required: true },
24182
+ inputSchema: {
24183
+ type: 'object',
24184
+ required: ['hidden'],
24185
+ properties: { hidden: { type: 'boolean' } }
24186
+ },
24187
+ effects: [{ kind: 'merge-by-key', path: 'fieldMetadata[]', key: 'name' }],
24188
+ validators: ['field-exists'],
24189
+ affectedPaths: ['fieldMetadata[].hidden'],
24190
+ submissionImpact: false,
24191
+ preconditions: ['config-initialized', 'target-exists']
24192
+ },
24193
+ {
24194
+ operationId: 'field.controlType.set',
24195
+ title: 'Alterar tipo de controle',
24196
+ scope: 'field',
24197
+ targetKind: 'field',
24198
+ target: { kind: 'field', resolver: 'field-by-name-or-label', ambiguityPolicy: 'fail', required: true },
24199
+ inputSchema: {
24200
+ type: 'object',
24201
+ required: ['controlType'],
24202
+ properties: { controlType: { type: 'string' } }
24203
+ },
24204
+ effects: [{ kind: 'merge-by-key', path: 'fieldMetadata[]', key: 'name' }],
24205
+ destructive: true,
24206
+ requiresConfirmation: true,
24207
+ validators: ['field-exists', 'control-type-compatible'],
24208
+ affectedPaths: ['fieldMetadata[].controlType'],
24209
+ submissionImpact: false,
24210
+ preconditions: ['config-initialized', 'target-exists']
24211
+ },
24212
+ {
24213
+ operationId: 'field.optionSource.set',
24214
+ title: 'Definir fonte de opções do campo',
24215
+ scope: 'field',
24216
+ targetKind: 'field',
24217
+ target: { kind: 'field', resolver: 'field-by-name-or-label', ambiguityPolicy: 'fail', required: true },
24218
+ inputSchema: {
24219
+ type: 'object',
24220
+ required: ['optionSource'],
24221
+ properties: {
24222
+ optionSource: {
24223
+ type: 'object',
24224
+ required: ['type'],
24225
+ properties: {
24226
+ type: { enum: ['static', 'remote', 'context'] },
24227
+ items: { type: 'array', description: 'Opções estáticas (type: static)' },
24228
+ resourcePath: { type: 'string', description: 'URL da API (type: remote)' },
24229
+ contextKey: { type: 'string', description: 'Chave de contexto (type: context)' }
24230
+ },
24231
+ if: { properties: { type: { const: 'static' } } },
24232
+ then: { required: ['items'] },
24233
+ else: {
24234
+ if: { properties: { type: { const: 'remote' } } },
24235
+ then: { required: ['resourcePath'] },
24236
+ else: { required: ['contextKey'] }
24237
+ }
24238
+ }
24239
+ }
24240
+ },
24241
+ effects: [{ kind: 'merge-by-key', path: 'fieldMetadata[]', key: 'name' }],
24242
+ validators: ['field-exists', 'option-source-valid'],
24243
+ affectedPaths: ['fieldMetadata[].optionSource'],
24244
+ submissionImpact: false,
24245
+ preconditions: ['config-initialized', 'target-exists']
24246
+ },
24247
+ // ─── LOCAL FIELD OPERATIONS ───────────────────────────────────────────────
24248
+ {
24249
+ operationId: 'field.local.add',
24250
+ title: 'Adicionar campo local',
24251
+ scope: 'global',
24252
+ targetKind: 'localField',
24253
+ target: { kind: 'localField', resolver: 'field-by-name', ambiguityPolicy: 'fail', required: false },
24254
+ inputSchema: {
24255
+ type: 'object',
24256
+ required: ['name', 'label', 'controlType', 'source'],
24257
+ properties: {
24258
+ name: { type: 'string' },
24259
+ label: { type: 'string' },
24260
+ controlType: { type: 'string' },
24261
+ /**
24262
+ * source:'local' é OBRIGATÓRIO e distingue campos locais de campos schema-backed.
24263
+ * O backend usa este discriminador para aplicar regras de ciclo de vida diferentes.
24264
+ */
24265
+ source: { const: 'local', description: 'Obrigatório. Discriminador que distingue campos locais de schema-backed.' },
24266
+ /**
24267
+ * transient:true significa que o valor não deve persistir entre navegações nem
24268
+ * ser incluído em snapshots. É independente de submitPolicy.
24269
+ */
24270
+ transient: { type: 'boolean', default: false, description: 'Se true, o valor não persiste entre navegações.' },
24271
+ /**
24272
+ * submitPolicy controla como o valor é tratado no payload de submissão.
24273
+ * VALORES VÁLIDOS: 'omit' | 'include' | 'includeWhenDirty'
24274
+ * NOTA: 'transient' NÃO é um valor válido aqui — use a propriedade transient:boolean acima.
24275
+ */
24276
+ submitPolicy: { enum: ['omit', 'include', 'includeWhenDirty'], default: 'omit' }
24277
+ }
24278
+ },
24279
+ effects: [{ kind: 'append-unique', path: 'fieldMetadata[]', key: 'name' }],
24280
+ validators: ['field-name-unique', 'local-schema-name-no-collision'],
24281
+ affectedPaths: ['fieldMetadata[]'],
24282
+ submissionImpact: true,
24283
+ preconditions: ['config-initialized']
24284
+ },
24285
+ {
24286
+ operationId: 'field.local.remove',
24287
+ title: 'Remover campo local',
24288
+ scope: 'field',
24289
+ targetKind: 'localField',
24290
+ target: { kind: 'localField', resolver: 'field-by-name', ambiguityPolicy: 'fail', required: true },
24291
+ inputSchema: { type: 'object', properties: {} },
24292
+ effects: [
24293
+ { kind: 'remove-by-key', path: 'fieldMetadata[]', key: 'name' },
24294
+ /**
24295
+ * Efeito cascata: remove a referência do campo de todos os slots de layout.
24296
+ * Sem isso, o layout mantém referências órfãs que causam erros de renderização.
24297
+ */
24298
+ {
24299
+ kind: 'compile-domain-patch',
24300
+ handler: 'form-layout-field-cleanup',
24301
+ handlerContract: {
24302
+ reads: ['fieldMetadata[].name', 'sections[].rows[].columns[].fields'],
24303
+ writes: ['sections[].rows[].columns[].fields'],
24304
+ identityKeys: ['fieldMetadata[].name', 'sections[].id', 'sections[].rows[].id', 'sections[].rows[].columns[].id'],
24305
+ inputSchema: { type: 'object', required: ['target.name'], properties: { 'target.name': { type: 'string' } } },
24306
+ failureModes: ['target-field-not-found', 'target-field-not-local', 'layout-reference-conflict'],
24307
+ description: 'Remove every layout placement that references the removed local field by fieldMetadata[].name.'
24308
+ }
24309
+ }
24310
+ ],
24311
+ destructive: true,
24312
+ requiresConfirmation: true,
24313
+ validators: ['field-exists', 'field-is-local'],
24314
+ affectedPaths: ['fieldMetadata[]', 'sections[].rows[].columns[].fields'],
24315
+ submissionImpact: true,
24316
+ preconditions: ['config-initialized', 'target-exists']
24317
+ },
24318
+ {
24319
+ operationId: 'field.submitPolicy.set',
24320
+ title: 'Definir política de submissão do campo',
24321
+ scope: 'field',
24322
+ targetKind: 'localField',
24323
+ target: { kind: 'localField', resolver: 'field-by-name', ambiguityPolicy: 'fail', required: true },
24324
+ inputSchema: {
24325
+ type: 'object',
24326
+ required: ['submitPolicy'],
24327
+ properties: {
24328
+ submitPolicy: { enum: ['omit', 'include', 'includeWhenDirty'] }
24329
+ }
24330
+ },
24331
+ effects: [{ kind: 'merge-by-key', path: 'fieldMetadata[]', key: 'name' }],
24332
+ validators: ['field-exists', 'field-is-local', 'editor-round-trip-preserve'],
24333
+ affectedPaths: ['fieldMetadata[].submitPolicy'],
24334
+ submissionImpact: true,
24335
+ preconditions: ['config-initialized', 'target-exists']
24336
+ },
24337
+ // ─── LAYOUT OPERATIONS ────────────────────────────────────────────────────
24338
+ {
24339
+ operationId: 'layout.section.add',
24340
+ title: 'Adicionar nova seção',
24341
+ scope: 'global',
24342
+ targetKind: 'section',
24343
+ target: { kind: 'section', resolver: 'section-by-id-or-title', ambiguityPolicy: 'fail', required: false },
24344
+ inputSchema: {
24345
+ type: 'object',
24346
+ required: ['id', 'title'],
24347
+ properties: {
24348
+ id: { type: 'string' },
24349
+ title: { type: 'string' },
24350
+ icon: { type: 'string' },
24351
+ collapsible: { type: 'boolean' }
24352
+ }
24353
+ },
24354
+ effects: [{ kind: 'append-unique', path: 'sections[]', key: 'id' }],
24355
+ validators: ['section-id-unique'],
24356
+ affectedPaths: ['sections[]'],
24357
+ submissionImpact: false,
24358
+ preconditions: ['config-initialized']
24359
+ },
24360
+ {
24361
+ operationId: 'layout.section.remove',
24362
+ title: 'Remover seção',
24363
+ scope: 'section',
24364
+ targetKind: 'section',
24365
+ target: { kind: 'section', resolver: 'section-by-id-or-title', ambiguityPolicy: 'fail', required: true },
24366
+ inputSchema: { type: 'object', properties: {} },
24367
+ effects: [
24368
+ { kind: 'remove-by-key', path: 'sections[]', key: 'id' },
24369
+ /**
24370
+ * Cascata: remove todas as linhas, colunas e referências de campos da seção excluída.
24371
+ * Os registros em fieldMetadata NÃO são removidos — apenas as posições de layout.
24372
+ * O validator field-exists-in-layout sinalizará campos sem posição após esta operação.
24373
+ */
24374
+ {
24375
+ kind: 'compile-domain-patch',
24376
+ handler: 'form-layout-section-cleanup',
24377
+ handlerContract: {
24378
+ reads: ['sections[].id', 'sections[].rows[].id', 'sections[].rows[].columns[].id', 'sections[].rows[].columns[].fields'],
24379
+ writes: ['sections[]'],
24380
+ identityKeys: ['sections[].id'],
24381
+ inputSchema: { type: 'object', required: ['target.id'], properties: { 'target.id': { type: 'string' } } },
24382
+ failureModes: ['target-section-not-found', 'ambiguous-section-target'],
24383
+ description: 'Removes the target section and its nested rows, columns, and field placements without deleting fieldMetadata records.'
24384
+ }
24385
+ }
24386
+ ],
24387
+ destructive: true,
24388
+ requiresConfirmation: true,
24389
+ validators: ['section-exists'],
24390
+ affectedPaths: ['sections[]', 'sections[].rows[]', 'sections[].rows[].columns[]', 'sections[].rows[].columns[].fields'],
24391
+ submissionImpact: false,
24392
+ preconditions: ['config-initialized', 'target-exists']
24393
+ },
24394
+ {
24395
+ operationId: 'layout.row.add',
24396
+ title: 'Adicionar linha numa seção',
24397
+ scope: 'section',
24398
+ targetKind: 'section',
24399
+ target: { kind: 'section', resolver: 'section-by-id-or-title', ambiguityPolicy: 'fail', required: true },
24400
+ inputSchema: {
24401
+ type: 'object',
24402
+ required: ['id'],
24403
+ properties: {
24404
+ id: { type: 'string' },
24405
+ columns: { type: 'number', minimum: 1, maximum: 12, default: 1 }
24406
+ }
24407
+ },
24408
+ effects: [{ kind: 'append-unique', path: 'sections[].rows[]', key: 'id' }],
24409
+ validators: ['section-exists', 'row-id-unique-in-section'],
24410
+ affectedPaths: ['sections[].rows[]'],
24411
+ submissionImpact: false,
24412
+ preconditions: ['config-initialized', 'target-exists']
24413
+ },
24414
+ {
24415
+ operationId: 'layout.column.add',
24416
+ title: 'Adicionar coluna numa linha',
24417
+ scope: 'row',
24418
+ targetKind: 'row',
24419
+ target: { kind: 'row', resolver: 'row-by-id-in-section', ambiguityPolicy: 'fail', required: true },
24420
+ inputSchema: {
24421
+ type: 'object',
24422
+ required: ['id'],
24423
+ properties: {
24424
+ id: { type: 'string' },
24425
+ span: { type: 'number', minimum: 1, maximum: 12, default: 6 }
24426
+ }
24427
+ },
24428
+ effects: [{ kind: 'append-unique', path: 'sections[].rows[].columns[]', key: 'id' }],
24429
+ validators: ['row-exists', 'column-id-unique-in-row'],
24430
+ affectedPaths: ['sections[].rows[].columns[]'],
24431
+ submissionImpact: false,
24432
+ preconditions: ['config-initialized', 'target-exists']
24433
+ },
24434
+ {
24435
+ operationId: 'layout.field.move',
24436
+ title: 'Mover campo no layout',
24437
+ scope: 'field',
24438
+ targetKind: 'field',
24439
+ target: { kind: 'field', resolver: 'field-by-name-or-label', ambiguityPolicy: 'fail', required: true },
24440
+ inputSchema: {
24441
+ type: 'object',
24442
+ required: ['targetSectionId', 'targetRowId', 'targetColumnId'],
24443
+ properties: {
24444
+ targetSectionId: { type: 'string' },
24445
+ targetRowId: { type: 'string' },
24446
+ targetColumnId: { type: 'string' },
24447
+ index: { type: 'number' }
24448
+ }
24449
+ },
24450
+ effects: [{
24451
+ kind: 'compile-domain-patch',
24452
+ handler: 'form-layout-reconciler',
24453
+ handlerContract: {
24454
+ reads: [
24455
+ 'fieldMetadata[].name',
24456
+ 'sections[].id',
24457
+ 'sections[].rows[].id',
24458
+ 'sections[].rows[].columns[].id',
24459
+ 'sections[].rows[].columns[].fields'
24460
+ ],
24461
+ writes: ['sections[].rows[].columns[].fields'],
24462
+ identityKeys: ['fieldMetadata[].name', 'sections[].id', 'sections[].rows[].id', 'sections[].rows[].columns[].id'],
24463
+ inputSchema: {
24464
+ type: 'object',
24465
+ required: ['target.name', 'targetSectionId', 'targetRowId', 'targetColumnId'],
24466
+ properties: {
24467
+ 'target.name': { type: 'string' },
24468
+ targetSectionId: { type: 'string' },
24469
+ targetRowId: { type: 'string' },
24470
+ targetColumnId: { type: 'string' },
24471
+ index: { type: 'number' }
24472
+ }
24473
+ },
24474
+ failureModes: ['target-field-not-found', 'target-layout-location-not-found', 'ambiguous-field-target', 'duplicate-field-placement'],
24475
+ description: 'Moves a field placement by stable section, row, column, and field ids; index is insertion position only.'
24476
+ }
24477
+ }],
24478
+ validators: ['field-exists', 'layout-target-exists', 'no-index-as-identity', 'field-exists-in-layout'],
24479
+ affectedPaths: ['sections[].rows[].columns[].fields'],
24480
+ submissionImpact: false,
24481
+ preconditions: ['config-initialized', 'target-exists']
24482
+ },
24483
+ // ─── RULE OPERATIONS ──────────────────────────────────────────────────────
24484
+ {
24485
+ operationId: 'rule.visibility.add',
24486
+ title: 'Adicionar regra de visibilidade',
24487
+ scope: 'global',
24488
+ targetKind: 'formRule',
24489
+ target: { kind: 'formRule', resolver: 'rule-by-id-or-name', ambiguityPolicy: 'fail', required: false },
24490
+ inputSchema: {
24491
+ type: 'object',
24492
+ required: ['id', 'type', 'targets', 'condition'],
24493
+ properties: {
24494
+ id: { type: 'string' },
24495
+ name: { type: 'string' },
24496
+ /** Discriminador de tipo armazenado no item formRules[]; distingue regras de visibilidade das de validação. */
24497
+ type: { const: 'visibility', description: 'Discriminador de tipo da regra; obrigatório para distinção em formRules[]' },
24498
+ targetType: { enum: ['field', 'section', 'row', 'column', 'action'], default: 'field' },
24499
+ targets: { type: 'array', items: { type: 'string' }, minItems: 1 },
24500
+ condition: { type: 'object', description: 'JsonLogic AST' }
24501
+ }
24502
+ },
24503
+ effects: [{ kind: 'append-unique', path: 'formRules[]', key: 'id' }],
24504
+ validators: ['rule-id-unique', 'rule-target-refs-exist', 'json-logic-valid'],
24505
+ affectedPaths: ['formRules[]'],
24506
+ submissionImpact: false,
24507
+ preconditions: ['config-initialized']
24508
+ },
24509
+ {
24510
+ operationId: 'rule.validation.add',
24511
+ title: 'Adicionar regra de validação',
24512
+ scope: 'global',
24513
+ targetKind: 'formRule',
24514
+ target: { kind: 'formRule', resolver: 'rule-by-id-or-name', ambiguityPolicy: 'fail', required: false },
24515
+ inputSchema: {
24516
+ type: 'object',
24517
+ required: ['id', 'type', 'targets', 'condition', 'message'],
24518
+ properties: {
24519
+ id: { type: 'string' },
24520
+ name: { type: 'string' },
24521
+ /** Discriminador de tipo armazenado no item formRules[]; distingue regras de validação das de visibilidade. */
24522
+ type: { const: 'validation', description: 'Discriminador de tipo da regra; obrigatório para distinção em formRules[]' },
24523
+ /**
24524
+ * targetType espelha o campo equivalente de rule.visibility.add.
24525
+ * Regras de validação podem se aplicar a campos, seções ou ações da toolbar.
24526
+ */
24527
+ targetType: { enum: ['field', 'section', 'row', 'column', 'action'], default: 'field' },
24528
+ targets: { type: 'array', items: { type: 'string' }, minItems: 1 },
24529
+ condition: { type: 'object', description: 'JsonLogic AST; quando true, exibe mensagem de erro' },
24530
+ message: { type: 'string', description: 'Mensagem de erro a exibir ao usuário' },
24531
+ level: { enum: ['error', 'warning'], default: 'error' }
24532
+ }
24533
+ },
24534
+ effects: [{ kind: 'append-unique', path: 'formRules[]', key: 'id' }],
24535
+ validators: ['rule-id-unique', 'rule-target-refs-exist', 'json-logic-valid'],
24536
+ affectedPaths: ['formRules[]'],
24537
+ submissionImpact: false,
24538
+ preconditions: ['config-initialized']
24539
+ },
24540
+ {
24541
+ operationId: 'rule.remove',
24542
+ title: 'Remover regra',
24543
+ scope: 'rule',
24544
+ targetKind: 'formRule',
24545
+ target: { kind: 'formRule', resolver: 'rule-by-id-or-name', ambiguityPolicy: 'fail', required: true },
24546
+ inputSchema: { type: 'object', properties: {} },
24547
+ effects: [{ kind: 'remove-by-key', path: 'formRules[]', key: 'id' }],
24548
+ destructive: true,
24549
+ requiresConfirmation: true,
24550
+ validators: ['rule-exists'],
24551
+ affectedPaths: ['formRules[]'],
24552
+ submissionImpact: false,
24553
+ preconditions: ['config-initialized', 'target-exists']
24554
+ },
24555
+ // ─── ACTION OPERATIONS ────────────────────────────────────────────────────
24556
+ {
24557
+ operationId: 'action.submit.configure',
24558
+ title: 'Configurar botão de envio',
24559
+ scope: 'global',
24560
+ targetKind: 'formAction',
24561
+ target: { kind: 'formAction', resolver: 'action-by-id-or-label', ambiguityPolicy: 'fail', required: false },
24562
+ inputSchema: {
24563
+ type: 'object',
24564
+ minProperties: 1,
24565
+ properties: {
24566
+ label: { type: 'string' },
24567
+ icon: { type: 'string' },
24568
+ visible: { type: 'boolean' },
24569
+ color: { enum: ['primary', 'accent', 'warn', 'basic'] }
24570
+ }
24571
+ },
24572
+ effects: [{ kind: 'merge-object', path: 'actions.submit' }],
24573
+ validators: ['editor-round-trip-preserve'],
24574
+ affectedPaths: ['actions.submit'],
24575
+ submissionImpact: false,
24576
+ preconditions: ['config-initialized']
24577
+ },
24578
+ {
24579
+ operationId: 'action.cancel.configure',
24580
+ title: 'Configurar botão de cancelar',
24581
+ scope: 'global',
24582
+ targetKind: 'formAction',
24583
+ target: { kind: 'formAction', resolver: 'action-by-id-or-label', ambiguityPolicy: 'fail', required: false },
24584
+ inputSchema: {
24585
+ type: 'object',
24586
+ minProperties: 1,
24587
+ properties: {
24588
+ label: { type: 'string' },
24589
+ icon: { type: 'string' },
24590
+ visible: { type: 'boolean' },
24591
+ color: { enum: ['primary', 'accent', 'warn', 'basic'] },
24592
+ navigateTo: { type: 'string' }
24593
+ }
24594
+ },
24595
+ effects: [{ kind: 'merge-object', path: 'actions.cancel' }],
24596
+ validators: ['editor-round-trip-preserve'],
24597
+ affectedPaths: ['actions.cancel'],
24598
+ submissionImpact: false,
24599
+ preconditions: ['config-initialized']
24600
+ },
24601
+ {
24602
+ operationId: 'action.reset.configure',
24603
+ title: 'Configurar botão de reset',
24604
+ scope: 'global',
24605
+ targetKind: 'formAction',
24606
+ target: { kind: 'formAction', resolver: 'action-by-id-or-label', ambiguityPolicy: 'fail', required: false },
24607
+ inputSchema: {
24608
+ type: 'object',
24609
+ minProperties: 1,
24610
+ properties: {
24611
+ label: { type: 'string' },
24612
+ icon: { type: 'string' },
24613
+ visible: { type: 'boolean' },
24614
+ color: { enum: ['primary', 'accent', 'warn', 'basic'] },
24615
+ confirmBeforeReset: { type: 'boolean' }
24616
+ }
24617
+ },
24618
+ effects: [{ kind: 'merge-object', path: 'actions.reset' }],
24619
+ validators: ['editor-round-trip-preserve'],
24620
+ affectedPaths: ['actions.reset'],
24621
+ submissionImpact: false,
24622
+ preconditions: ['config-initialized']
24623
+ }
24624
+ ],
24625
+ validators: [
24626
+ {
24627
+ validatorId: 'field-exists',
24628
+ level: 'error',
24629
+ code: 'DF001',
24630
+ description: 'O campo alvo deve existir em fieldMetadata.'
24631
+ },
24632
+ {
24633
+ validatorId: 'field-name-unique',
24634
+ level: 'error',
24635
+ code: 'DF002',
24636
+ description: 'Nomes de campos devem ser únicos em fieldMetadata.'
24637
+ },
24638
+ {
24639
+ validatorId: 'section-id-unique',
24640
+ level: 'error',
24641
+ code: 'DF003',
24642
+ description: 'IDs de seções devem ser únicos em sections[].'
24643
+ },
24644
+ {
24645
+ validatorId: 'section-exists',
24646
+ level: 'error',
24647
+ code: 'DF004',
24648
+ description: 'A seção alvo deve existir em sections[].'
24649
+ },
24650
+ {
24651
+ validatorId: 'field-exists-in-layout',
24652
+ level: 'warning',
24653
+ code: 'DF005',
24654
+ description: 'Campo definido em fieldMetadata mas ausente no layout.'
24655
+ },
24656
+ {
24657
+ validatorId: 'local-schema-name-no-collision',
24658
+ level: 'error',
24659
+ code: 'DF006',
24660
+ description: 'Nome de campo local não pode colidir com campo schema-backed existente.'
24661
+ },
24662
+ {
24663
+ validatorId: 'field-is-local',
24664
+ level: 'error',
24665
+ code: 'DF007',
24666
+ description: 'Operação restrita a campos locais (source:"local"). Campo schema-backed não pode ser removido ou ter submitPolicy alterada.'
24667
+ },
24668
+ {
24669
+ validatorId: 'rule-id-unique',
24670
+ level: 'error',
24671
+ code: 'DF008',
24672
+ description: 'IDs de regras devem ser únicos em formRules[].'
24673
+ },
24674
+ {
24675
+ validatorId: 'rule-exists',
24676
+ level: 'error',
24677
+ code: 'DF009',
24678
+ description: 'A regra alvo deve existir em formRules[].'
24679
+ },
24680
+ {
24681
+ validatorId: 'rule-target-refs-exist',
24682
+ level: 'error',
24683
+ code: 'DF010',
24684
+ description: 'Todos os targets da regra devem referenciar campos, seções ou ações existentes.'
24685
+ },
24686
+ {
24687
+ validatorId: 'json-logic-valid',
24688
+ level: 'error',
24689
+ code: 'DF011',
24690
+ description: 'A expressão JsonLogic deve ser um AST válido (suporte canônico à spec JsonLogic).'
24691
+ },
24692
+ {
24693
+ validatorId: 'layout-target-exists',
24694
+ level: 'error',
24695
+ code: 'DF012',
24696
+ description: 'A seção/linha/coluna de destino devem existir antes de mover o campo.'
24697
+ },
24698
+ {
24699
+ validatorId: 'no-index-as-identity',
24700
+ level: 'error',
24701
+ code: 'DF013',
24702
+ description: 'Operações de layout não devem usar índice de array como identidade canônica; use IDs estáveis.'
24703
+ },
24704
+ {
24705
+ validatorId: 'row-exists',
24706
+ level: 'error',
24707
+ code: 'DF014',
24708
+ description: 'A linha alvo deve existir na seção especificada.'
24709
+ },
24710
+ {
24711
+ validatorId: 'row-id-unique-in-section',
24712
+ level: 'error',
24713
+ code: 'DF015',
24714
+ description: 'O id da nova linha deve ser único dentro da seção alvo.'
24715
+ },
24716
+ {
24717
+ validatorId: 'column-id-unique-in-row',
24718
+ level: 'error',
24719
+ code: 'DF016',
24720
+ description: 'O id da nova coluna deve ser único dentro da linha alvo.'
24721
+ },
24722
+ {
24723
+ validatorId: 'control-type-compatible',
24724
+ level: 'error',
24725
+ code: 'DF017',
24726
+ description: 'O tipo de controle deve ser compatível com o tipo de dado do campo.'
24727
+ },
24728
+ {
24729
+ validatorId: 'option-source-valid',
24730
+ level: 'error',
24731
+ code: 'DF018',
24732
+ description: 'A fonte de opções deve ser válida para o tipo de controle especificado.'
24733
+ },
24734
+ {
24735
+ validatorId: 'editor-round-trip-preserve',
24736
+ level: 'error',
24737
+ code: 'DF019',
24738
+ description: 'Edição não deve corromper propriedades críticas para o round-trip do editor. Aplicado após operações que afetam submitPolicy ou source de campos locais.'
24739
+ }
24740
+ ],
24741
+ roundTripRequirements: [
24742
+ 'strip-undefined',
24743
+ 'normalize-json-logic',
24744
+ 'stable-field-order',
24745
+ 'preserve-local-field-source',
24746
+ 'preserve-section-row-column-ids'
24747
+ ],
24748
+ examples: [
24749
+ {
24750
+ id: 'add-local-transient-field',
24751
+ request: 'Adicionar campo auxiliar local "notasInternas" que não deve aparecer no payload de envio',
24752
+ operationId: 'field.local.add',
24753
+ params: {
24754
+ name: 'notasInternas',
24755
+ label: 'Notas Internas',
24756
+ controlType: 'textarea',
24757
+ source: 'local',
24758
+ transient: true,
24759
+ submitPolicy: 'omit'
24760
+ },
24761
+ isPositive: true
24762
+ },
24763
+ {
24764
+ id: 'relabel-schema-backed-field',
24765
+ request: 'Mudar o label do campo email para "E-mail Corporativo"',
24766
+ operationId: 'field.label.set',
24767
+ target: 'email',
24768
+ params: { label: 'E-mail Corporativo' },
24769
+ isPositive: true
24770
+ },
24771
+ {
24772
+ id: 'move-field-to-column',
24773
+ request: 'Mover campo telefone para a primeira coluna da seção de contato',
24774
+ operationId: 'layout.field.move',
24775
+ target: 'telefone',
24776
+ params: { targetSectionId: 'secao-contato', targetRowId: 'row-1', targetColumnId: 'col-1' },
24777
+ isPositive: true
24778
+ },
24779
+ {
24780
+ id: 'add-visibility-rule',
24781
+ request: 'Ocultar o campo cpf quando o tipo de pessoa for jurídica',
24782
+ operationId: 'rule.visibility.add',
24783
+ params: {
24784
+ id: 'ocultar-cpf-pj',
24785
+ name: 'Ocultar CPF para PJ',
24786
+ targetType: 'field',
24787
+ targets: ['cpf'],
24788
+ condition: { '==': [{ var: 'tipoPessoa' }, 'juridica'] }
24789
+ },
24790
+ isPositive: true
24791
+ },
24792
+ {
24793
+ id: 'reject-unknown-field',
24794
+ request: 'Alterar o label do campo "campoInexistente"',
24795
+ operationId: 'field.label.set',
24796
+ target: 'campoInexistente',
24797
+ params: { label: 'Novo Label' },
24798
+ isPositive: false
24799
+ },
24800
+ {
24801
+ id: 'require-confirmation-remove-field',
24802
+ request: 'Remover o campo auxiliar "notasInternas"',
24803
+ operationId: 'field.local.remove',
24804
+ target: 'notasInternas',
24805
+ isPositive: true
24806
+ },
24807
+ {
24808
+ id: 'require-confirmation-remove-section',
24809
+ request: 'Remover a seção "Dados Adicionais"',
24810
+ operationId: 'layout.section.remove',
24811
+ target: 'secao-dados-adicionais',
24812
+ isPositive: true
24813
+ },
24814
+ {
24815
+ id: 'round-trip-no-drift',
24816
+ request: 'Salvar e reabrir o formulário editado sem drift de configuração',
24817
+ operationId: 'field.label.set',
24818
+ target: 'nome',
24819
+ params: { label: 'Nome Completo' },
24820
+ isPositive: true
24821
+ }
24822
+ ]
24823
+ };
24824
+
23994
24825
  /*
23995
24826
  * Public API Surface of praxis-dynamic-form
23996
24827
  */
@@ -23999,4 +24830,4 @@ const FORM_COMPONENT_AI_CAPABILITIES = {
23999
24830
  * Generated bundle index. Do not edit.
24000
24831
  */
24001
24832
 
24002
- export { ActionsEditorComponent, CanvasStateService, CanvasToolbarComponent, ColumnEditorComponent, DynamicFormLayoutService, FORM_AI_CAPABILITIES, FORM_COMPONENT_AI_CAPABILITIES, FieldConfiguratorComponent, FieldEditorComponent, FormConfigService, FormContextService, FormLayoutService, JsonConfigEditorComponent, LayoutColorToken, LayoutEditorComponent, LayoutPrefsService, PRAXIS_DYNAMIC_FORM_COMPONENT_METADATA, PRAXIS_FILTER_FORM_COMPONENT_METADATA, PraxisDynamicForm, PraxisDynamicFormConfigEditor, PraxisFilterForm, PraxisFormActionsComponent, RowConfiguratorComponent, RowEditorComponent, SETTINGS_PANEL_DYNAMIC_FORM_PROVIDER, SectionEditorComponent, TASK_PRESETS, applyVisibilityRules, buildDynamicFormApplyPlan, createDynamicFormAuthoringDocument, formLayoutRulesToBuilderState, getFormAiCatalog, getFormCapabilities, getFormEnum, isRuleSatisfied, normalizeDateArrays, normalizeDynamicFormAuthoringDocument, parseLegacyOrDynamicFormDocument, providePraxisDynamicFormMetadata, providePraxisFilterFormMetadata, provideSettingsPanelDynamicForm, ruleBuilderStateToFormLayoutRules, serializeDynamicFormAuthoringDocument, stripLegacyFieldMetadata, toCanonicalDynamicFormConfig, validateDynamicFormAuthoringDocument, validateDynamicFormAuthoringInput };
24833
+ export { ActionsEditorComponent, CanvasStateService, CanvasToolbarComponent, ColumnEditorComponent, DynamicFormLayoutService, FORM_AI_CAPABILITIES, FORM_COMPONENT_AI_CAPABILITIES, FieldConfiguratorComponent, FieldEditorComponent, FormConfigService, FormContextService, FormLayoutService, JsonConfigEditorComponent, LayoutColorToken, LayoutEditorComponent, LayoutPrefsService, PRAXIS_DYNAMIC_FORM_AUTHORING_MANIFEST, PRAXIS_DYNAMIC_FORM_COMPONENT_METADATA, PRAXIS_FILTER_FORM_COMPONENT_METADATA, PraxisDynamicForm, PraxisDynamicFormConfigEditor, PraxisFilterForm, PraxisFormActionsComponent, RowConfiguratorComponent, RowEditorComponent, SETTINGS_PANEL_DYNAMIC_FORM_PROVIDER, SectionEditorComponent, TASK_PRESETS, applyVisibilityRules, buildDynamicFormApplyPlan, createDynamicFormAuthoringDocument, formLayoutRulesToBuilderState, getFormAiCatalog, getFormCapabilities, getFormEnum, isRuleSatisfied, normalizeDateArrays, normalizeDynamicFormAuthoringDocument, parseLegacyOrDynamicFormDocument, providePraxisDynamicFormMetadata, providePraxisFilterFormMetadata, provideSettingsPanelDynamicForm, ruleBuilderStateToFormLayoutRules, serializeDynamicFormAuthoringDocument, stripLegacyFieldMetadata, toCanonicalDynamicFormConfig, validateDynamicFormAuthoringDocument, validateDynamicFormAuthoringInput };