@praxisui/metadata-editor 10.0.0-rc.36 → 10.0.0-rc.38

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -166,6 +166,8 @@ Funcionalidades:
166
166
  - Edição de dependentes, estratégia de merge, debounce, modo de carregamento (imediato/manual/respeitar loadOn).
167
167
  - Mapeamento de filtros com dot‑path e valuePath por dependente, preview com exemplo gerado.
168
168
 
169
+ Abrir uma cascata e salvá-la sem alterações preserva mapeamentos de filtro não idênticos (entradas string ou objeto) e a extração `dependencyValuePath` em formato string ou mapa por dependência. Sugestões completam apenas valores ausentes. Durante a edição, as linhas de mapeamento representam o rascunho atual; um snapshot JSON anterior não sobrescreve alterações feitas na tabela. Remover uma dependência remove também o seu mapeamento e override de extração; adicioná-la novamente não recupera caminhos antigos. O preview ilustrativo não comprova a execução do filtro na API.
170
+
169
171
  ### Reactive Determination Diagnostics (somente leitura)
170
172
 
171
173
  `ReactiveDeterminationDiagnosticsComponent` recebe o documento exato de
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": "1.0.0",
3
- "generatedAt": "2026-10-21T13:45:30.091Z",
3
+ "generatedAt": "2026-10-23T20:24:09.136Z",
4
4
  "packageName": "@praxisui/metadata-editor",
5
- "packageVersion": "10.0.0-rc.36",
5
+ "packageVersion": "10.0.0-rc.38",
6
6
  "sourceRegistry": "praxis-component-registry-ingestion",
7
7
  "sourceRegistryVersion": "1.0.0",
8
8
  "componentCount": 1,
@@ -6,7 +6,7 @@ import * as i3$2 from '@angular/common';
6
6
  import { CommonModule } from '@angular/common';
7
7
  import { isValidCssColor, DynamicFieldLoaderDirective, normalizeCssColorToRgb, providePraxisDynamicFieldsI18n } from '@praxisui/dynamic-fields';
8
8
  import * as i2$2 from '@praxisui/core';
9
- import { providePraxisI18n, PraxisI18nService, PraxisIconDirective, FieldControlType, INLINE_FILTER_CONTROL_TYPES, resolveInlineFilterControlType, ResourceDiscoveryService, normalizeControlTypeToken, providePraxisI18nConfig, RESOURCE_DISCOVERY_I18N_CONFIG, normalizeReactiveDeterminations, ComponentMetadataRegistry } from '@praxisui/core';
9
+ import { providePraxisI18n, PraxisI18nService, PraxisIconDirective, FieldControlType, INLINE_FILTER_CONTROL_TYPES, resolveInlineFilterControlType, ResourceDiscoveryService, mapCatalogFieldsToMetadata, humanizeCatalogFieldName, normalizeControlTypeToken, hasSemanticFieldCalibrationDiff, calibrateFieldMetadataFromSemantics, providePraxisI18nConfig, RESOURCE_DISCOVERY_I18N_CONFIG, normalizeReactiveDeterminations, ComponentMetadataRegistry } from '@praxisui/core';
10
10
  import * as i5$1 from '@angular/material/expansion';
11
11
  import { MatExpansionModule } from '@angular/material/expansion';
12
12
  import * as i2$1 from '@angular/material/icon';
@@ -653,6 +653,8 @@ const METADATA_EDITOR_EN_US = {
653
653
  'praxis.metadataEditor.colorPalette.moveDown': 'Move down',
654
654
  'praxis.metadataEditor.colorPalette.remove': 'Remove',
655
655
  'praxis.metadataEditor.colorPalette.invalid': 'Enter a valid CSS color or var(--token).',
656
+ 'praxis.metadataEditor.smartCalibrate.action': 'Calibrate from schema',
657
+ 'praxis.metadataEditor.smartCalibrate.tooltip': 'Automatically infers control type (Currency, CPF/CNPJ, Date, Email, Phone, Select, Textarea), humanized label, and corporate defaults from field name and metadata.',
656
658
  };
657
659
 
658
660
  const METADATA_EDITOR_PT_BR = {
@@ -905,6 +907,8 @@ const METADATA_EDITOR_PT_BR = {
905
907
  'praxis.metadataEditor.colorPalette.moveDown': 'Mover para baixo',
906
908
  'praxis.metadataEditor.colorPalette.remove': 'Remover',
907
909
  'praxis.metadataEditor.colorPalette.invalid': 'Informe uma cor CSS válida ou var(--token).',
910
+ 'praxis.metadataEditor.smartCalibrate.action': 'Calibrar pelo schema',
911
+ 'praxis.metadataEditor.smartCalibrate.tooltip': 'Infere automaticamente o tipo de controle (Moeda, CPF/CNPJ, Data, E-mail, Telefone, Seleção, Texto longo), rótulo humanizado e padrões corporativos a partir do nome e metadados do campo.',
908
912
  };
909
913
 
910
914
  function createPraxisMetadataEditorI18nConfig(options = {}) {
@@ -10925,6 +10929,7 @@ class FieldMetadataEditorComponent {
10925
10929
  renderedProps = [];
10926
10930
  initialSnapshot = null;
10927
10931
  initialControlType;
10932
+ initialSeed;
10928
10933
  baselineReady = false;
10929
10934
  formRebuild$ = new Subject();
10930
10935
  hostApplyPatch;
@@ -11104,17 +11109,86 @@ class FieldMetadataEditorComponent {
11104
11109
  if (!option?.responseFields?.length) {
11105
11110
  continue;
11106
11111
  }
11107
- const key = this.normalizeResourceCacheKey(option.value);
11108
- if (key && !this.resourceSchemaFieldsByPath.has(key)) {
11109
- this.resourceSchemaFieldsByPath.set(key, option.responseFields
11110
- .filter((field) => Boolean(field?.name && String(field.name).trim()))
11111
- .map((field) => ({
11112
- name: String(field.name).trim(),
11113
- label: String(field.label ?? '').trim() || String(field.name).trim(),
11114
- type: String(field.type ?? field.controlType ?? 'string').trim() || 'string',
11115
- controlType: String(field.controlType ?? '').trim() || undefined,
11112
+ const mappedMetas = mapCatalogFieldsToMetadata(option.responseFields);
11113
+ const metaByName = new Map(mappedMetas.map((meta) => [meta.name, meta]));
11114
+ const definitions = option.responseFields
11115
+ .filter((field) => Boolean(field?.name && String(field.name).trim()))
11116
+ .map((field) => {
11117
+ const name = String(field.name).trim();
11118
+ const rawLabel = String(field.label ?? '').trim();
11119
+ const label = rawLabel && rawLabel !== name
11120
+ ? rawLabel
11121
+ : humanizeCatalogFieldName(name);
11122
+ const mapped = metaByName.get(name);
11123
+ return {
11124
+ name,
11125
+ label,
11126
+ type: String(field.type ?? field.controlType ?? 'string').trim() ||
11127
+ 'string',
11128
+ controlType: String(field.controlType ?? mapped?.controlType ?? '').trim() ||
11129
+ undefined,
11116
11130
  description: String(field.description ?? '').trim() || undefined,
11117
- })));
11131
+ ...(typeof field.format === 'string' && field.format.trim()
11132
+ ? { format: field.format.trim() }
11133
+ : {}),
11134
+ ...(typeof field.required === 'boolean'
11135
+ ? { required: field.required }
11136
+ : {}),
11137
+ ...(typeof field.minimum === 'number'
11138
+ ? { minimum: field.minimum }
11139
+ : {}),
11140
+ ...(typeof field.maximum === 'number'
11141
+ ? { maximum: field.maximum }
11142
+ : {}),
11143
+ ...(typeof field.minLength === 'number'
11144
+ ? { minLength: field.minLength }
11145
+ : {}),
11146
+ ...(typeof field.maxLength === 'number'
11147
+ ? { maxLength: field.maxLength }
11148
+ : {}),
11149
+ ...(typeof field.pattern === 'string' && field.pattern.trim()
11150
+ ? { pattern: field.pattern.trim() }
11151
+ : {}),
11152
+ ...(Array.isArray(field.enumValues)
11153
+ ? { enumValues: field.enumValues }
11154
+ : {}),
11155
+ ...(mapped?.options?.length
11156
+ ? {
11157
+ options: mapped.options.map((opt) => {
11158
+ const optLabel = String(opt.label ?? opt.text ?? opt.value);
11159
+ return {
11160
+ key: String(opt.value),
11161
+ value: String(opt.value),
11162
+ label: optLabel,
11163
+ text: opt.text ?? optLabel,
11164
+ };
11165
+ }),
11166
+ }
11167
+ : {}),
11168
+ };
11169
+ });
11170
+ if (!definitions.length) {
11171
+ continue;
11172
+ }
11173
+ const candidateKeys = new Set();
11174
+ for (const candidate of [option.value, option.path, option.resourceKey]) {
11175
+ const raw = String(candidate ?? '').trim();
11176
+ if (!raw)
11177
+ continue;
11178
+ const normalized = this.normalizeResourceCacheKey(raw);
11179
+ if (normalized)
11180
+ candidateKeys.add(normalized);
11181
+ const stripped = raw.replace(/^\/+|\/+$/g, '');
11182
+ if (stripped)
11183
+ candidateKeys.add(stripped);
11184
+ const withoutApi = stripped.replace(/^api\/+/i, '');
11185
+ if (withoutApi)
11186
+ candidateKeys.add(withoutApi);
11187
+ }
11188
+ for (const cacheKey of candidateKeys) {
11189
+ if (!this.resourceSchemaFieldsByPath.has(cacheKey)) {
11190
+ this.resourceSchemaFieldsByPath.set(cacheKey, definitions);
11191
+ }
11118
11192
  }
11119
11193
  }
11120
11194
  }
@@ -11256,6 +11330,9 @@ class FieldMetadataEditorComponent {
11256
11330
  if (this.initialControlType === undefined) {
11257
11331
  this.initialControlType = ct;
11258
11332
  }
11333
+ if (this.initialSeed === undefined) {
11334
+ this.initialSeed = this.cloneSnapshot(this.seed ?? {});
11335
+ }
11259
11336
  this.controlType = ct;
11260
11337
  let props = this.registry.getProperties(this.controlType);
11261
11338
  const controlTypeToken = normalizeControlTypeToken(this.controlType);
@@ -11999,6 +12076,9 @@ class FieldMetadataEditorComponent {
11999
12076
  }
12000
12077
  }
12001
12078
  reset() {
12079
+ if (this.initialSeed !== undefined) {
12080
+ this.seed = this.cloneSnapshot(this.initialSeed);
12081
+ }
12002
12082
  if (this.initialControlType !== undefined && this.controlType !== this.initialControlType) {
12003
12083
  this.rebuildEditorForType(this.initialControlType);
12004
12084
  }
@@ -12019,6 +12099,79 @@ class FieldMetadataEditorComponent {
12019
12099
  if (value != null)
12020
12100
  this.onControlTypeChange(value);
12021
12101
  }
12102
+ tx(key, fallback) {
12103
+ const namespacedKey = key.startsWith('praxis.metadataEditor.')
12104
+ ? key
12105
+ : `praxis.metadataEditor.${key}`;
12106
+ return this.i18n?.t(namespacedKey, undefined, fallback) ?? fallback;
12107
+ }
12108
+ buildCurrentSemanticFieldSnapshot() {
12109
+ const currentRaw = this.form?.getRawValue?.() ?? {};
12110
+ const merged = {
12111
+ ...(this.seed || {}),
12112
+ };
12113
+ for (const [k, v] of Object.entries(currentRaw)) {
12114
+ if (v !== undefined && v !== null && v !== '') {
12115
+ merged[k] = v;
12116
+ }
12117
+ }
12118
+ merged['name'] = String(currentRaw?.name ?? this.seed?.name ?? '').trim();
12119
+ merged['controlType'] = this.controlType;
12120
+ return merged;
12121
+ }
12122
+ canCalibrateFromSemantics() {
12123
+ const snapshot = this.buildCurrentSemanticFieldSnapshot();
12124
+ return hasSemanticFieldCalibrationDiff(snapshot);
12125
+ }
12126
+ calibrateFromSemantics() {
12127
+ const mergedField = this.buildCurrentSemanticFieldSnapshot();
12128
+ if (!hasSemanticFieldCalibrationDiff(mergedField)) {
12129
+ return;
12130
+ }
12131
+ const calibrated = calibrateFieldMetadataFromSemantics(mergedField);
12132
+ this.seed = {
12133
+ ...(this.seed || {}),
12134
+ ...calibrated,
12135
+ };
12136
+ const nextControlType = (calibrated.controlType ?? this.controlType);
12137
+ if (nextControlType && nextControlType !== this.controlType) {
12138
+ this.rebuildEditorForType(nextControlType);
12139
+ }
12140
+ const getByPath = (obj, path) => {
12141
+ if (!obj || typeof obj !== 'object' || !path)
12142
+ return undefined;
12143
+ return path.split('.').reduce((acc, part) => (acc == null ? undefined : acc[part]), obj);
12144
+ };
12145
+ const formPatch = {};
12146
+ for (const prop of this.normalizedProps || []) {
12147
+ const propName = String(prop?.name ?? '').trim();
12148
+ if (!propName)
12149
+ continue;
12150
+ const rawVal = getByPath(calibrated, propName);
12151
+ if (rawVal === undefined)
12152
+ continue;
12153
+ const controlVal = this.toEditorControlValue(prop, rawVal);
12154
+ if (controlVal !== undefined && controlVal !== null) {
12155
+ try {
12156
+ this.factory.setValueByPath(this.form, propName, controlVal);
12157
+ }
12158
+ catch { }
12159
+ if (!propName.includes('.') && this.form?.get(propName)) {
12160
+ formPatch[propName] = controlVal;
12161
+ }
12162
+ }
12163
+ }
12164
+ if (calibrated.options !== undefined &&
12165
+ !this.form?.get('options') &&
12166
+ this.form?.get('providerOptions')) {
12167
+ const prop = this.normalizedProps?.find((p) => p?.name === 'providerOptions');
12168
+ formPatch['providerOptions'] = this.toEditorControlValue(prop, calibrated.options);
12169
+ }
12170
+ if (Object.keys(formPatch).length > 0) {
12171
+ this.form.patchValue(formPatch, { emitEvent: true });
12172
+ }
12173
+ this.isDirty$.next(true);
12174
+ }
12022
12175
  withSharedFieldMetadataProperties(props) {
12023
12176
  const propNames = new Set(props.map((prop) => prop.name));
12024
12177
  const sharedProps = [...fieldPresentationProperties, ...fieldSubmitProperties];
@@ -12323,6 +12476,7 @@ class FieldMetadataEditorComponent {
12323
12476
  const statusKey = findExact([
12324
12477
  'status',
12325
12478
  'situacao',
12479
+ 'situacaofuncional',
12326
12480
  'ativo',
12327
12481
  'active',
12328
12482
  'estado',
@@ -12460,6 +12614,20 @@ class FieldMetadataEditorComponent {
12460
12614
  ], ngImport: i0, template: `
12461
12615
  @if (form) {
12462
12616
  <div class="p-3">
12617
+ @if (canCalibrateFromSemantics()) {
12618
+ <div class="d-flex justify-content-end mb-2">
12619
+ <button
12620
+ mat-stroked-button
12621
+ type="button"
12622
+ data-testid="field-metadata-smart-calibrate-btn"
12623
+ (click)="calibrateFromSemantics()"
12624
+ [matTooltip]="tx('smartCalibrate.tooltip', 'Infere automaticamente o tipo de controle, rótulo humanizado e padrões corporativos a partir do nome e metadados do campo.')"
12625
+ >
12626
+ <mat-icon aria-hidden="true">auto_fix_high</mat-icon>
12627
+ {{ tx('smartCalibrate.action', 'Calibrar pelo schema') }}
12628
+ </button>
12629
+ </div>
12630
+ }
12463
12631
  <praxis-dynamic-editor-renderer
12464
12632
  [properties]="renderedProps"
12465
12633
  [form]="form"
@@ -12472,7 +12640,7 @@ class FieldMetadataEditorComponent {
12472
12640
  }
12473
12641
  </div>
12474
12642
  }
12475
- `, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "component", type: DynamicEditorRendererComponent, selector: "praxis-dynamic-editor-renderer", inputs: ["properties", "form"] }, { kind: "component", type: ArrayItemSchemaFieldsEditorComponent, selector: "praxis-array-item-schema-fields-editor", inputs: ["control", "arrayControl"] }] });
12643
+ `, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i1$2.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: "ngmodule", type: MatIconModule }, { kind: "component", type: i2$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i3$1.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: DynamicEditorRendererComponent, selector: "praxis-dynamic-editor-renderer", inputs: ["properties", "form"] }, { kind: "component", type: ArrayItemSchemaFieldsEditorComponent, selector: "praxis-array-item-schema-fields-editor", inputs: ["control", "arrayControl"] }] });
12476
12644
  }
12477
12645
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: FieldMetadataEditorComponent, decorators: [{
12478
12646
  type: Component,
@@ -12481,6 +12649,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
12481
12649
  standalone: true,
12482
12650
  imports: [
12483
12651
  ReactiveFormsModule,
12652
+ MatButtonModule,
12653
+ MatIconModule,
12654
+ MatTooltipModule,
12484
12655
  DynamicEditorRendererComponent,
12485
12656
  ArrayItemSchemaFieldsEditorComponent
12486
12657
  ],
@@ -12492,6 +12663,20 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
12492
12663
  template: `
12493
12664
  @if (form) {
12494
12665
  <div class="p-3">
12666
+ @if (canCalibrateFromSemantics()) {
12667
+ <div class="d-flex justify-content-end mb-2">
12668
+ <button
12669
+ mat-stroked-button
12670
+ type="button"
12671
+ data-testid="field-metadata-smart-calibrate-btn"
12672
+ (click)="calibrateFromSemantics()"
12673
+ [matTooltip]="tx('smartCalibrate.tooltip', 'Infere automaticamente o tipo de controle, rótulo humanizado e padrões corporativos a partir do nome e metadados do campo.')"
12674
+ >
12675
+ <mat-icon aria-hidden="true">auto_fix_high</mat-icon>
12676
+ {{ tx('smartCalibrate.action', 'Calibrar pelo schema') }}
12677
+ </button>
12678
+ </div>
12679
+ }
12495
12680
  <praxis-dynamic-editor-renderer
12496
12681
  [properties]="renderedProps"
12497
12682
  [form]="form"
@@ -12677,10 +12862,13 @@ class CascadeRulesService {
12677
12862
  dependencyLoadOnChange: null,
12678
12863
  };
12679
12864
  }
12680
- /** Suggest a basic mapping dep->dep and a default valuePath */
12865
+ /** Fill missing mappings and value paths without replacing authored values. */
12681
12866
  suggest(rule, inferredValueKey) {
12682
- const map = {};
12683
- (rule.dependencyFields || []).forEach((dep) => (map[dep] = dep));
12867
+ const map = { ...rule.dependencyFilterMap };
12868
+ (rule.dependencyFields || []).forEach((dep) => {
12869
+ if (!Object.prototype.hasOwnProperty.call(map, dep))
12870
+ map[dep] = dep;
12871
+ });
12684
12872
  return {
12685
12873
  ...rule,
12686
12874
  dependencyFilterMap: Object.keys(map).length ? map : rule.dependencyFilterMap,
@@ -12729,12 +12917,13 @@ class CascadeManagerTabComponent {
12729
12917
  editLoadOn = 'respectLoadOn';
12730
12918
  editDebounce = 150;
12731
12919
  editValuePath = '';
12732
- editFilterMapJson = '';
12733
12920
  importMode = false;
12734
12921
  importJson = '';
12735
12922
  feedbackMessage = '';
12736
12923
  feedbackTone = 'info';
12737
12924
  editMapRows = [];
12925
+ editInitialFilterMap = {};
12926
+ editInitialValuePath = 'id';
12738
12927
  setFeedback(message, tone = 'info') {
12739
12928
  this.feedbackMessage = message;
12740
12929
  this.feedbackTone = tone;
@@ -12836,12 +13025,9 @@ class CascadeManagerTabComponent {
12836
13025
  this.editLoadOn = suggested.dependencyLoadOnChange || 'respectLoadOn';
12837
13026
  this.editDebounce = suggested.dependencyDebounceMs ?? 150;
12838
13027
  this.editValuePath = (typeof suggested.dependencyValuePath === 'string' ? suggested.dependencyValuePath : 'id') || '';
12839
- try {
12840
- this.editFilterMapJson = suggested.dependencyFilterMap ? JSON.stringify(suggested.dependencyFilterMap, null, 2) : '';
12841
- }
12842
- catch {
12843
- this.editFilterMapJson = '';
12844
- }
13028
+ this.editInitialFilterMap = structuredClone(suggested.dependencyFilterMap || {});
13029
+ this.editInitialValuePath = structuredClone(suggested.dependencyValuePath || 'id');
13030
+ this.editMapRows = [];
12845
13031
  // Build mapping rows from dependencies
12846
13032
  this.buildMapRowsFromDeps();
12847
13033
  this.editing.set(true);
@@ -12851,20 +13037,45 @@ class CascadeManagerTabComponent {
12851
13037
  saveEdit(name) {
12852
13038
  // Build rule from editor
12853
13039
  const deps = this.editDepList.filter((s) => !!s && s !== name);
12854
- // Build map from rows; fallback to JSON when provided
12855
- let map = {};
12856
- this.editMapRows.forEach((r) => { if (!r.key && r.dep)
12857
- r.key = r.dep; if (r.dep)
12858
- map[r.dep] = r.valuePath ? { key: r.key || r.dep, valuePath: r.valuePath } : (r.key || r.dep); });
12859
- const raw = (this.editFilterMapJson || '').trim();
12860
- if (raw) {
12861
- try {
12862
- const fromJson = JSON.parse(raw);
12863
- map = { ...map, ...fromJson };
13040
+ // The rows own the draft. The opening snapshot only preserves entry shape;
13041
+ // merging the original JSON after the rows would silently undo visual edits.
13042
+ const map = structuredClone(this.editInitialFilterMap);
13043
+ const dependenciesRemoved = (this.ruleFor(name)?.dependencyFields || [])
13044
+ .some((dep) => !deps.includes(dep));
13045
+ if (dependenciesRemoved) {
13046
+ for (const dep of Object.keys(map)) {
13047
+ if (!deps.includes(dep))
13048
+ delete map[dep];
12864
13049
  }
12865
- catch {
12866
- this.setFeedback('Mapeamento de filtros inválido (JSON).', 'error');
12867
- return;
13050
+ }
13051
+ for (const row of this.editMapRows) {
13052
+ if (!deps.includes(row.dep))
13053
+ continue;
13054
+ const initial = this.editInitialFilterMap[row.dep];
13055
+ const initialPath = this.initialRowValuePath(row.dep);
13056
+ if (typeof initial === 'object' && initial !== null) {
13057
+ const entry = { ...initial, key: row.key || row.dep };
13058
+ if (row.valuePath !== initialPath) {
13059
+ if (row.valuePath)
13060
+ entry.valuePath = row.valuePath;
13061
+ else
13062
+ delete entry.valuePath;
13063
+ }
13064
+ map[row.dep] = entry;
13065
+ }
13066
+ else {
13067
+ map[row.dep] = row.valuePath && row.valuePath !== initialPath
13068
+ ? { key: row.key || row.dep, valuePath: row.valuePath }
13069
+ : row.key || row.dep;
13070
+ }
13071
+ }
13072
+ const valuePaths = this.buildValuePathOverrides(deps);
13073
+ // Removed dependencies must lose their extraction overrides as well as their
13074
+ // filter mappings, otherwise re-adding one resurrects stale extraction state.
13075
+ if (dependenciesRemoved && typeof valuePaths === 'object') {
13076
+ for (const dep of Object.keys(valuePaths)) {
13077
+ if (!deps.includes(dep))
13078
+ delete valuePaths[dep];
12868
13079
  }
12869
13080
  }
12870
13081
  this.setFeedback('');
@@ -12874,7 +13085,7 @@ class CascadeManagerTabComponent {
12874
13085
  enableDependencyCascade: this.editEnable,
12875
13086
  resetOnDependentChange: this.editReset,
12876
13087
  dependencyFilterMap: map,
12877
- dependencyValuePath: this.buildValuePathOverrides(deps),
13088
+ dependencyValuePath: valuePaths,
12878
13089
  dependencyMergeStrategy: this.editMerge,
12879
13090
  dependencyDebounceMs: this.editDebounce ?? 150,
12880
13091
  dependencyLoadOnChange: this.editLoadOn,
@@ -12989,33 +13200,65 @@ class CascadeManagerTabComponent {
12989
13200
  this.editLoadOn = 'respectLoadOn';
12990
13201
  this.editDebounce = 150;
12991
13202
  this.editValuePath = 'id';
13203
+ this.editInitialFilterMap = {};
13204
+ this.editInitialValuePath = 'id';
13205
+ this.editMapRows = [];
12992
13206
  this.buildMapRowsFromDeps();
12993
13207
  this.editing.set(true);
12994
13208
  }
12995
13209
  buildMapRowsFromDeps() {
12996
13210
  const deps = this.editDepList;
12997
- const existing = (() => { try {
12998
- return JSON.parse(this.editFilterMapJson || '{}');
12999
- }
13000
- catch {
13001
- return {};
13002
- } })();
13211
+ const current = new Map(this.editMapRows.map((row) => [row.dep, row]));
13003
13212
  this.editMapRows = deps.map((dep) => {
13004
- const entry = existing[dep];
13005
- if (!entry)
13006
- return { dep, key: dep };
13007
- if (typeof entry === 'string')
13008
- return { dep, key: entry };
13009
- return { dep, key: entry.key || dep, valuePath: entry.valuePath };
13213
+ const edited = current.get(dep);
13214
+ if (edited)
13215
+ return edited;
13216
+ const entry = this.editInitialFilterMap[dep];
13217
+ const key = typeof entry === 'string' ? entry : entry?.key || dep;
13218
+ return { dep, key, valuePath: this.initialRowValuePath(dep) };
13010
13219
  });
13011
13220
  }
13221
+ initialRowValuePath(dep) {
13222
+ const entry = this.editInitialFilterMap[dep];
13223
+ if (typeof entry === 'object' && entry?.valuePath)
13224
+ return entry.valuePath;
13225
+ return typeof this.editInitialValuePath === 'object'
13226
+ ? this.editInitialValuePath[dep]
13227
+ : undefined;
13228
+ }
13012
13229
  buildValuePathOverrides(deps) {
13013
- // If any row defines a valuePath, return a record; else return default string
13014
- const record = {};
13015
- this.editMapRows.forEach((r) => { if (r.valuePath)
13016
- record[r.dep] = r.valuePath; });
13017
- const hasAny = Object.keys(record).length > 0;
13018
- return hasAny ? record : (this.editValuePath || 'id');
13230
+ const initialDefault = typeof this.editInitialValuePath === 'string'
13231
+ ? this.editInitialValuePath
13232
+ : 'id';
13233
+ const defaultChanged = this.editValuePath !== initialDefault;
13234
+ const pathsChanged = this.editMapRows.some((row) => row.valuePath !== this.initialRowValuePath(row.dep));
13235
+ if (!defaultChanged && !pathsChanged)
13236
+ return structuredClone(this.editInitialValuePath);
13237
+ const wasRecord = typeof this.editInitialValuePath === 'object';
13238
+ if (!pathsChanged && !wasRecord)
13239
+ return this.editValuePath || 'id';
13240
+ // A specific extraction path must not erase the default/overrides used by
13241
+ // other dependencies. Unchanged documents keep their original shape above.
13242
+ const record = typeof this.editInitialValuePath === 'object'
13243
+ ? { ...this.editInitialValuePath }
13244
+ : {};
13245
+ for (const dep of deps) {
13246
+ const row = this.editMapRows.find((candidate) => candidate.dep === dep);
13247
+ if (row && row.valuePath !== this.initialRowValuePath(dep)) {
13248
+ record[dep] = row.valuePath || this.editValuePath || 'id';
13249
+ }
13250
+ else if (typeof this.editInitialValuePath === 'object') {
13251
+ const existing = this.editInitialValuePath[dep];
13252
+ if (existing !== undefined)
13253
+ record[dep] = existing;
13254
+ else if (defaultChanged)
13255
+ record[dep] = this.editValuePath || 'id';
13256
+ }
13257
+ else {
13258
+ record[dep] = this.editValuePath || 'id';
13259
+ }
13260
+ }
13261
+ return record;
13019
13262
  }
13020
13263
  buildPreview() {
13021
13264
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@praxisui/metadata-editor",
3
- "version": "10.0.0-rc.36",
3
+ "version": "10.0.0-rc.38",
4
4
  "description": "Metadata editor for Praxis UI fields and components with runtime integration.",
5
5
  "peerDependencies": {
6
6
  "@angular/common": "^21.0.0",
@@ -8,10 +8,10 @@
8
8
  "@angular/cdk": "^21.0.0",
9
9
  "@angular/forms": "^21.0.0",
10
10
  "@angular/material": "^21.0.0",
11
- "@praxisui/ai": "^10.0.0-rc.36",
12
- "@praxisui/core": "^10.0.0-rc.36",
13
- "@praxisui/dynamic-fields": "^10.0.0-rc.36",
14
- "@praxisui/settings-panel": "^10.0.0-rc.36",
11
+ "@praxisui/ai": "^10.0.0-rc.38",
12
+ "@praxisui/core": "^10.0.0-rc.38",
13
+ "@praxisui/dynamic-fields": "^10.0.0-rc.38",
14
+ "@praxisui/settings-panel": "^10.0.0-rc.38",
15
15
  "rxjs": "~7.8.0"
16
16
  },
17
17
  "dependencies": {
@@ -160,6 +160,7 @@ declare class FieldMetadataEditorComponent implements OnInit {
160
160
  renderedProps: any[];
161
161
  private initialSnapshot;
162
162
  private initialControlType?;
163
+ private initialSeed?;
163
164
  private baselineReady;
164
165
  private readonly formRebuild$;
165
166
  private readonly hostApplyPatch?;
@@ -227,6 +228,10 @@ declare class FieldMetadataEditorComponent implements OnInit {
227
228
  reset(): void;
228
229
  onControlTypeChange(next: FieldControlType): void;
229
230
  onControlTypeChangeEvent(event: Event): void;
231
+ tx(key: string, fallback: string): string;
232
+ private buildCurrentSemanticFieldSnapshot;
233
+ canCalibrateFromSemantics(): boolean;
234
+ calibrateFromSemantics(): void;
230
235
  private withSharedFieldMetadataProperties;
231
236
  get isArrayEditor(): boolean;
232
237
  private static readonly ROOT_DEPENDENT_FIELD_KEY_PROPS;
@@ -298,7 +303,7 @@ declare class CascadeRulesService {
298
303
  hydrateRule(field: FieldDefinition | FieldMetadata): CascadeRuleViewModel | null;
299
304
  dehydratePatch(rule: CascadeRuleViewModel): Partial<FieldDefinition>;
300
305
  clearPatch(): Partial<FieldDefinition>;
301
- /** Suggest a basic mapping dep->dep and a default valuePath */
306
+ /** Fill missing mappings and value paths without replacing authored values. */
302
307
  suggest(rule: CascadeRuleViewModel, inferredValueKey?: string): CascadeRuleViewModel;
303
308
  private normalizeDependencyFields;
304
309
  static ɵfac: i0.ɵɵFactoryDeclaration<CascadeRulesService, never>;
@@ -327,7 +332,6 @@ declare class CascadeManagerTabComponent implements OnChanges {
327
332
  editLoadOn: 'respectLoadOn' | 'immediate' | 'manual';
328
333
  editDebounce: number | null;
329
334
  editValuePath: string;
330
- editFilterMapJson: string;
331
335
  importMode: boolean;
332
336
  importJson: string;
333
337
  feedbackMessage: string;
@@ -337,6 +341,8 @@ declare class CascadeManagerTabComponent implements OnChanges {
337
341
  key: string;
338
342
  valuePath?: string;
339
343
  }>;
344
+ private editInitialFilterMap;
345
+ private editInitialValuePath;
340
346
  private setFeedback;
341
347
  constructor(svc: CascadeRulesService);
342
348
  ngOnInit(): void;
@@ -366,6 +372,7 @@ declare class CascadeManagerTabComponent implements OnChanges {
366
372
  applyImport(): void;
367
373
  startTemplate(name: string, tpl: 'rota-destino' | 'rota-destino-propriedade'): void;
368
374
  buildMapRowsFromDeps(): void;
375
+ private initialRowValuePath;
369
376
  buildValuePathOverrides(deps: string[]): string | Record<string, string>;
370
377
  buildPreview(): string;
371
378
  copyPreview(): void;