@praxisui/metadata-editor 10.0.0-rc.31 → 10.0.0-rc.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": "1.0.0",
3
- "generatedAt": "2026-10-16T17:45:27.774Z",
3
+ "generatedAt": "2026-10-18T21:58:32.782Z",
4
4
  "packageName": "@praxisui/metadata-editor",
5
- "packageVersion": "10.0.0-rc.31",
5
+ "packageVersion": "10.0.0-rc.33",
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, normalizeControlTypeToken, normalizeReactiveDeterminations, ComponentMetadataRegistry } 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';
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';
@@ -26,7 +26,7 @@ import { MatInputModule } from '@angular/material/input';
26
26
  import * as i6 from '@angular/material/select';
27
27
  import { MatSelectModule } from '@angular/material/select';
28
28
  import { SETTINGS_PANEL_DATA } from '@praxisui/settings-panel';
29
- import { BehaviorSubject } from 'rxjs';
29
+ import { Subject, BehaviorSubject, takeUntil, catchError, of } from 'rxjs';
30
30
  import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
31
31
  import { MatChipsModule } from '@angular/material/chips';
32
32
  import { MatDividerModule } from '@angular/material/divider';
@@ -103,14 +103,11 @@ class SchemaNormalizerService {
103
103
  if (seed && clone.defaultValue === undefined) {
104
104
  const val = this.getByPath(seed, clone.name);
105
105
  if (val !== undefined) {
106
- // Stringify complex defaults for textarea-friendly editors
107
- const needsJson = (clone.editorType === 'textarea' &&
108
- (clone.name === 'options' ||
109
- clone.name === 'filterCriteria' ||
110
- clone.name === 'paletteSettings.colors' ||
111
- clone.name === 'views' ||
112
- clone.name === 'adaptiveSubtitle'));
113
- clone.defaultValue = needsJson && typeof val !== 'string' ? JSON.stringify(val, null, 2) : val;
106
+ // Stringify complex defaults (arrays/objects) for textarea-friendly JSON editors
107
+ const needsJson = clone.editorType === 'textarea' &&
108
+ val !== null &&
109
+ typeof val === 'object';
110
+ clone.defaultValue = needsJson ? JSON.stringify(val, null, 2) : val;
114
111
  try {
115
112
  if (clone.name === 'resourcePath' ||
116
113
  clone.name === 'filterCriteria' ||
@@ -443,6 +440,7 @@ const METADATA_EDITOR_EN_US = {
443
440
  'praxis.metadataEditor.arrayFields.control.date': 'Date',
444
441
  'praxis.metadataEditor.fieldResource.label': 'Governed resource',
445
442
  'praxis.metadataEditor.fieldResource.searchPlaceholder': 'Search by name, domain, or path',
443
+ 'praxis.metadataEditor.fieldResource.fieldSearchPlaceholder': 'Search field by name or label',
446
444
  'praxis.metadataEditor.fieldResource.hint': 'Select from the host governed catalog. The saved value remains resourcePath.',
447
445
  'praxis.metadataEditor.common.help': 'Help',
448
446
  'praxis.metadataEditor.common.iconMaterialSymbolsHint': 'Use the icon name (Material Symbols)',
@@ -694,6 +692,7 @@ const METADATA_EDITOR_PT_BR = {
694
692
  'praxis.metadataEditor.arrayFields.control.date': 'Data',
695
693
  'praxis.metadataEditor.fieldResource.label': 'Recurso governado',
696
694
  'praxis.metadataEditor.fieldResource.searchPlaceholder': 'Buscar por nome, dom\u00ednio ou caminho',
695
+ 'praxis.metadataEditor.fieldResource.fieldSearchPlaceholder': 'Buscar campo por nome ou r\u00f3tulo',
697
696
  'praxis.metadataEditor.fieldResource.hint': 'Selecione no cat\u00e1logo governado do host. O valor gravado continua sendo resourcePath.',
698
697
  'praxis.metadataEditor.common.help': 'Ajuda',
699
698
  'praxis.metadataEditor.common.iconMaterialSymbolsHint': 'Use o nome do \u00edcone (Material Symbols)',
@@ -2721,6 +2720,9 @@ class DynamicEditorRendererComponent {
2721
2720
  return options.map((option) => ({
2722
2721
  ...option,
2723
2722
  text: this.resolveEditorText(option.textKey, option.text) ?? option.text,
2723
+ code: option.code ?? undefined,
2724
+ badge: option.badge ?? undefined,
2725
+ description: option.description ?? undefined,
2724
2726
  }));
2725
2727
  }
2726
2728
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: DynamicEditorRendererComponent, deps: [{ token: i1$1.MatDialog }, { token: i2$2.IconPickerService }, { token: i2$2.PraxisI18nService, optional: true }], target: i0.ɵɵFactoryTarget.Component });
@@ -5546,6 +5548,46 @@ const numberProperties = [
5546
5548
  { name: 'step', label: 'Step (incremento)', editorType: 'number', group: 'Formato/Comportamento', hint: 'Incremento em unidades; combine com min/max (ex.: 0.01).' },
5547
5549
  { name: 'numberFormat.decimalPlaces', label: 'Casas decimais', editorType: 'number', group: 'Formato/Comportamento', row: 'formato.decimais', inline: true, hint: 'Precisão exibida/aceita (ex.: 2).' },
5548
5550
  { name: 'showGrouping', label: 'Agrupar milhares', editorType: 'checkbox', group: 'Formato/Comportamento', row: 'formato.decimais', inline: true, hint: 'Mostra separador de milhar sem alterar o valor salvo.' },
5551
+ { name: 'numberFormat.percentage', label: 'Modo Percentual (Percent Studio)', editorType: 'checkbox', group: 'Formato/Comportamento', hint: 'Ativa pílula de progresso, micro-anel interativo e barra/popover visual de seleção de percentual.' },
5552
+ {
5553
+ name: 'numericFormat',
5554
+ label: 'Formato numérico canônico',
5555
+ editorType: 'select',
5556
+ group: 'Formato/Comportamento',
5557
+ options: [
5558
+ { value: 'decimal', text: 'Decimal' },
5559
+ { value: 'integer', text: 'Inteiro' },
5560
+ { value: 'currency', text: 'Moeda' },
5561
+ { value: 'percent', text: 'Percentual (Percent Studio)' },
5562
+ { value: 'scientific', text: 'Científico' },
5563
+ ],
5564
+ hint: 'Formato semântico do campo numérico vindo do contrato x-ui.',
5565
+ },
5566
+ {
5567
+ name: 'showPercentStudio',
5568
+ label: 'Exibir barra Percent Studio no formulário',
5569
+ editorType: 'checkbox',
5570
+ group: 'Formato/Comportamento',
5571
+ hint: 'Mostra o trilho interativo, presets de quartis e steppers rápidos abaixo do pdx-number-input quando em modo percentual.',
5572
+ },
5573
+ {
5574
+ name: 'inlineVisualStyle',
5575
+ label: 'Estilo visual inline (percentual)',
5576
+ editorType: 'select',
5577
+ group: 'Formato/Comportamento',
5578
+ options: [
5579
+ { value: 'compact', text: 'Compacto (Pílula líquida + popover)' },
5580
+ { value: 'graphic', text: 'Gráfico expandido (Card com donut + slider)' },
5581
+ ],
5582
+ hint: 'Define a apresentação de filtros numéricos percentuais em superfícies inline.',
5583
+ },
5584
+ {
5585
+ name: 'percentPresets',
5586
+ label: 'Presets rápidos de percentual',
5587
+ editorType: 'json',
5588
+ group: 'Formato/Comportamento',
5589
+ hint: 'Lista de atalhos rápidos no Percent Studio (ex.: [0, 25, 50, 75, 100] ou [{"label": "Meta", "value": 12.5}]).',
5590
+ },
5549
5591
  {
5550
5592
  name: 'inputMode',
5551
5593
  label: 'Modo de entrada',
@@ -7718,6 +7760,7 @@ const rangeSliderProperties = [
7718
7760
  { value: 'compact', text: 'Compacto' },
7719
7761
  { value: 'storage', text: 'Armazenamento' },
7720
7762
  ], hint: 'Formatter declarativo para labels e aria-valuetext.' },
7763
+ { name: 'numberFormat.percentage', label: 'Percentual (%)', editorType: 'checkbox', group: 'Formato/Comportamento', hint: 'Ativa apresentação Percent Studio (pílula líquida de intervalo, micro-anel e quartis automáticos 0%–100%).' },
7721
7764
  { name: 'showTicks', label: 'Ticks', editorType: 'select', group: 'Formato/Comportamento', options: [
7722
7765
  { value: false, text: 'Sem ticks' },
7723
7766
  { value: true, text: 'Com ticks' },
@@ -10732,6 +10775,93 @@ function mergeEditorProperties(base, contextual) {
10732
10775
  }
10733
10776
  return merged;
10734
10777
  }
10778
+ const inlinePhoneEditorProperties = mergeEditorProperties(inputProperties, phoneInputProperties);
10779
+ const inlinePeriodRangeEditorProperties = mergeEditorProperties(rangeSliderProperties, [
10780
+ {
10781
+ name: 'granularity',
10782
+ label: 'Granularidade do período',
10783
+ editorType: 'select',
10784
+ group: 'Formato/Comportamento',
10785
+ options: [
10786
+ { value: 'month', text: 'Mês (1-12 ou YYYY-MM)' },
10787
+ { value: 'year', text: 'Ano (YYYY)' },
10788
+ ],
10789
+ hint: 'Define se o seletor opera em competência mensal ou exercício anual.',
10790
+ },
10791
+ {
10792
+ name: 'defaultGranularity',
10793
+ label: 'Granularidade inicial',
10794
+ editorType: 'select',
10795
+ group: 'Formato/Comportamento',
10796
+ options: [
10797
+ { value: 'month', text: 'Mês' },
10798
+ { value: 'year', text: 'Ano' },
10799
+ ],
10800
+ hint: 'Granularidade inicial aberta no painel.',
10801
+ },
10802
+ {
10803
+ name: 'selectionGrid',
10804
+ label: 'Exibir grade de seleção (Jan-Dez / Anos)',
10805
+ editorType: 'checkbox',
10806
+ group: 'Formato/Comportamento',
10807
+ hint: 'Mostra botões clicáveis de meses ou anos para seleção de único item ou faixa.',
10808
+ },
10809
+ {
10810
+ name: 'showSlider',
10811
+ label: 'Exibir slider complementar',
10812
+ editorType: 'checkbox',
10813
+ group: 'Formato/Comportamento',
10814
+ hint: 'Mostra barra deslizante sincronizada com a grade de meses/anos.',
10815
+ },
10816
+ {
10817
+ name: 'inlineChipDisplay',
10818
+ label: 'Exibição no chip (inline)',
10819
+ editorType: 'select',
10820
+ group: 'Formato/Comportamento',
10821
+ options: [
10822
+ { value: 'badge', text: 'Rótulo + badge (ex.: Mês: Jan – Jun)' },
10823
+ { value: 'value', text: 'Apenas valor (ex.: Jan – Jun)' },
10824
+ { value: 'label-value', text: 'Rótulo + valor' },
10825
+ ],
10826
+ hint: 'Define como o intervalo selecionado aparece na pílula fechada.',
10827
+ },
10828
+ {
10829
+ name: 'minYear',
10830
+ label: 'Ano mínimo',
10831
+ editorType: 'number',
10832
+ group: 'Validação',
10833
+ row: 'val.yearminmax',
10834
+ inline: true,
10835
+ hint: 'Ano inicial exibido na grade de anos (ex.: 2020).',
10836
+ },
10837
+ {
10838
+ name: 'maxYear',
10839
+ label: 'Ano máximo',
10840
+ editorType: 'number',
10841
+ group: 'Validação',
10842
+ row: 'val.yearminmax',
10843
+ inline: true,
10844
+ hint: 'Ano final exibido na grade de anos (ex.: 2030).',
10845
+ },
10846
+ {
10847
+ name: 'minPeriod',
10848
+ label: 'Período mínimo',
10849
+ editorType: 'text',
10850
+ group: 'Validação',
10851
+ row: 'val.periodminmax',
10852
+ inline: true,
10853
+ hint: 'Limite inferior de competência (ex.: 2020-01 ou 1).',
10854
+ },
10855
+ {
10856
+ name: 'maxPeriod',
10857
+ label: 'Período máximo',
10858
+ editorType: 'text',
10859
+ group: 'Validação',
10860
+ row: 'val.periodminmax',
10861
+ inline: true,
10862
+ hint: 'Limite superior de competência (ex.: 2027-12 ou 12).',
10863
+ },
10864
+ ]);
10735
10865
  function hasCanonicalDocumentSemantics(metadata) {
10736
10866
  if (!metadata || typeof metadata !== 'object' || Array.isArray(metadata)) {
10737
10867
  return false;
@@ -10747,12 +10877,12 @@ const INLINE_FILTER_EDITOR_PROPERTIES_BY_CONTROL_TYPE = Object.freeze({
10747
10877
  [INLINE_FILTER_CONTROL_TYPES.AUTOCOMPLETE]: selectProperties,
10748
10878
  [INLINE_FILTER_CONTROL_TYPES.MULTI_SELECT]: selectProperties,
10749
10879
  [INLINE_FILTER_CONTROL_TYPES.INPUT]: inputProperties,
10750
- [INLINE_FILTER_CONTROL_TYPES.PHONE]: inputProperties,
10880
+ [INLINE_FILTER_CONTROL_TYPES.PHONE]: inlinePhoneEditorProperties,
10751
10881
  [INLINE_FILTER_CONTROL_TYPES.TOGGLE]: toggleProperties,
10752
10882
  [INLINE_FILTER_CONTROL_TYPES.RANGE]: rangeSliderProperties,
10753
- [INLINE_FILTER_CONTROL_TYPES.PERIOD_RANGE]: rangeSliderProperties,
10754
- [INLINE_FILTER_CONTROL_TYPES.YEAR_RANGE]: rangeSliderProperties,
10755
- [INLINE_FILTER_CONTROL_TYPES.MONTH_RANGE]: rangeSliderProperties,
10883
+ [INLINE_FILTER_CONTROL_TYPES.PERIOD_RANGE]: inlinePeriodRangeEditorProperties,
10884
+ [INLINE_FILTER_CONTROL_TYPES.YEAR_RANGE]: inlinePeriodRangeEditorProperties,
10885
+ [INLINE_FILTER_CONTROL_TYPES.MONTH_RANGE]: inlinePeriodRangeEditorProperties,
10756
10886
  [INLINE_FILTER_CONTROL_TYPES.DATE]: dateProperties,
10757
10887
  [INLINE_FILTER_CONTROL_TYPES.DATE_RANGE]: dateRangeProperties,
10758
10888
  [INLINE_FILTER_CONTROL_TYPES.TIME]: timePickerProperties,
@@ -10794,7 +10924,10 @@ class FieldMetadataEditorComponent {
10794
10924
  normalizedProps = [];
10795
10925
  renderedProps = [];
10796
10926
  initialSnapshot = null;
10927
+ initialControlType;
10797
10928
  baselineReady = false;
10929
+ formRebuild$ = new Subject();
10930
+ hostApplyPatch;
10798
10931
  // Lista de tipos suportados no editor (curada para os tipos com config disponível)
10799
10932
  typeOptions = [
10800
10933
  { value: FieldControlType.INPUT, label: 'Texto (Input)' },
@@ -10847,6 +10980,9 @@ class FieldMetadataEditorComponent {
10847
10980
  isValid$ = new BehaviorSubject(true);
10848
10981
  isBusy$ = new BehaviorSubject(false);
10849
10982
  destroyRef = inject(DestroyRef);
10983
+ resourceDiscovery = inject(ResourceDiscoveryService, { optional: true });
10984
+ i18n = inject(PraxisI18nService, { optional: true });
10985
+ discoveredResourceOptions = [];
10850
10986
  lastCurrencyPreset = null;
10851
10987
  constructor(registry, normalizer, factory, ctxValidators) {
10852
10988
  this.registry = registry;
@@ -10861,10 +10997,7 @@ class FieldMetadataEditorComponent {
10861
10997
  if (data.seed)
10862
10998
  this.seed = data.seed;
10863
10999
  // Bridge: allow host to receive patches even if saved$/applied$ are missed
10864
- try {
10865
- this.__hostApplyPatch = data.hostBridge?.applyPatch;
10866
- }
10867
- catch { }
11000
+ this.hostApplyPatch = data.hostBridge?.applyPatch;
10868
11001
  }
10869
11002
  // Normalize legacy avatar seed (root imageSrc/imageAlt -> extra.* for editor binding)
10870
11003
  try {
@@ -10886,14 +11019,145 @@ class FieldMetadataEditorComponent {
10886
11019
  }
10887
11020
  catch { }
10888
11021
  }
11022
+ catalogLookupRequested = false;
11023
+ resourceSchemaFieldsByPath = new Map();
11024
+ resourceSchemaRequestIdByScope = {
11025
+ root: 0,
11026
+ optionSource: 0,
11027
+ };
10889
11028
  ngOnInit() {
10890
- try {
10891
- const hasBridge = typeof this.__hostApplyPatch === 'function';
10892
- (console.log || console.debug)('[FieldMetadataEditor] ngOnInit', { controlType: this.controlType, hasHostBridge: hasBridge });
10893
- }
10894
- catch { }
10895
11029
  this.rebuildEditorForType(this.controlType);
10896
11030
  }
11031
+ loadCatalogResourceOptionsIfNeeded() {
11032
+ const hasResourcePathProp = this.normalizedProps.some((prop) => prop?.name === 'resourcePath' ||
11033
+ prop?.name === 'optionSource.resourcePath');
11034
+ if (!hasResourcePathProp) {
11035
+ return;
11036
+ }
11037
+ const optionSourceTypeCtrl = this.form?.get('optionSource.type');
11038
+ if (optionSourceTypeCtrl) {
11039
+ optionSourceTypeCtrl.valueChanges
11040
+ .pipe(takeUntil(this.formRebuild$), takeUntilDestroyed(this.destroyRef))
11041
+ .subscribe(() => {
11042
+ if (this.discoveredResourceOptions.length > 0 && this.normalizedProps.length > 0) {
11043
+ this.renderedProps = this.buildRenderedProps(this.normalizedProps);
11044
+ }
11045
+ });
11046
+ }
11047
+ this.bindResourcePathSchemaSync('resourcePath', 'root');
11048
+ this.bindResourcePathSchemaSync('optionSource.resourcePath', 'optionSource');
11049
+ if (this.catalogLookupRequested || !this.resourceDiscovery) {
11050
+ return;
11051
+ }
11052
+ this.catalogLookupRequested = true;
11053
+ const endpointKey = this.resolveDiscoveryEndpointKey();
11054
+ this.resourceDiscovery
11055
+ .getDiscoveredResources({
11056
+ intent: 'any',
11057
+ ...(endpointKey ? { endpointKey } : {}),
11058
+ })
11059
+ .pipe(catchError(() => of([])), takeUntilDestroyed(this.destroyRef))
11060
+ .subscribe((options) => {
11061
+ this.discoveredResourceOptions = options;
11062
+ this.seedCatalogResponseFieldsCache(options);
11063
+ if (options.length > 0 && this.normalizedProps.length > 0) {
11064
+ this.renderedProps = this.buildRenderedProps(this.normalizedProps);
11065
+ }
11066
+ });
11067
+ }
11068
+ resolveDiscoveryEndpointKey() {
11069
+ const raw = this.form?.get('endpointKey')?.value ??
11070
+ this.form?.get('apiEndpointKey')?.value ??
11071
+ this.seed?.endpointKey ??
11072
+ this.seed?.apiEndpointKey;
11073
+ const trimmed = String(raw ?? '').trim();
11074
+ return trimmed || undefined;
11075
+ }
11076
+ bindResourcePathSchemaSync(controlPath, scope) {
11077
+ const ctrl = this.form?.get(controlPath);
11078
+ if (!ctrl) {
11079
+ return;
11080
+ }
11081
+ let lastPath = String(ctrl.value ?? '').trim();
11082
+ if (lastPath) {
11083
+ this.resolveAndApplyResourceSchemaFields(lastPath, scope, false);
11084
+ }
11085
+ ctrl.valueChanges
11086
+ .pipe(takeUntil(this.formRebuild$), takeUntilDestroyed(this.destroyRef))
11087
+ .subscribe((nextValue) => {
11088
+ const nextPath = String(nextValue ?? '').trim();
11089
+ if (nextPath === lastPath) {
11090
+ return;
11091
+ }
11092
+ lastPath = nextPath;
11093
+ if (!nextPath) {
11094
+ if (this.normalizedProps.length > 0) {
11095
+ this.renderedProps = this.buildRenderedProps(this.normalizedProps);
11096
+ }
11097
+ return;
11098
+ }
11099
+ this.resolveAndApplyResourceSchemaFields(nextPath, scope, this.baselineReady);
11100
+ });
11101
+ }
11102
+ seedCatalogResponseFieldsCache(options) {
11103
+ for (const option of options) {
11104
+ if (!option?.responseFields?.length) {
11105
+ continue;
11106
+ }
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,
11116
+ description: String(field.description ?? '').trim() || undefined,
11117
+ })));
11118
+ }
11119
+ }
11120
+ }
11121
+ resolveAndApplyResourceSchemaFields(resourcePath, scope, autoSuggestDefaults) {
11122
+ const trimmed = String(resourcePath ?? '').trim();
11123
+ if (!trimmed) {
11124
+ return;
11125
+ }
11126
+ const cached = this.getCachedFieldsForResource(trimmed);
11127
+ if (cached.length > 0) {
11128
+ if (autoSuggestDefaults) {
11129
+ this.applyAutoSuggestedDependentKeys(scope, cached);
11130
+ }
11131
+ if (this.normalizedProps.length > 0) {
11132
+ this.renderedProps = this.buildRenderedProps(this.normalizedProps);
11133
+ }
11134
+ }
11135
+ if (typeof this.resourceDiscovery?.getResourceSchemaFields !== 'function') {
11136
+ return;
11137
+ }
11138
+ const requestId = ++this.resourceSchemaRequestIdByScope[scope];
11139
+ const endpointKey = this.resolveDiscoveryEndpointKey();
11140
+ const request$ = endpointKey
11141
+ ? this.resourceDiscovery.getResourceSchemaFields(trimmed, { endpointKey })
11142
+ : this.resourceDiscovery.getResourceSchemaFields(trimmed);
11143
+ request$
11144
+ .pipe(catchError(() => of([])), takeUntilDestroyed(this.destroyRef))
11145
+ .subscribe((fields) => {
11146
+ if (requestId !== this.resourceSchemaRequestIdByScope[scope]) {
11147
+ return;
11148
+ }
11149
+ const validFields = (fields || []).filter((field) => Boolean(field?.name && String(field.name).trim()));
11150
+ if (validFields.length > 0) {
11151
+ this.setCachedFieldsForResource(trimmed, validFields);
11152
+ if (autoSuggestDefaults) {
11153
+ this.applyAutoSuggestedDependentKeys(scope, validFields);
11154
+ }
11155
+ }
11156
+ if (this.normalizedProps.length > 0) {
11157
+ this.renderedProps = this.buildRenderedProps(this.normalizedProps);
11158
+ }
11159
+ });
11160
+ }
10897
11161
  resolveCanonicalEditorProperties(controlType, controlTypeToken) {
10898
11162
  const inlineProps = resolveInlineFilterEditorProperties(controlTypeToken, this.seed);
10899
11163
  if (inlineProps?.length)
@@ -10988,6 +11252,10 @@ class FieldMetadataEditorComponent {
10988
11252
  return undefined;
10989
11253
  }
10990
11254
  rebuildEditorForType(ct) {
11255
+ this.formRebuild$.next();
11256
+ if (this.initialControlType === undefined) {
11257
+ this.initialControlType = ct;
11258
+ }
10991
11259
  this.controlType = ct;
10992
11260
  let props = this.registry.getProperties(this.controlType);
10993
11261
  const controlTypeToken = normalizeControlTypeToken(this.controlType);
@@ -11008,23 +11276,6 @@ class FieldMetadataEditorComponent {
11008
11276
  }
11009
11277
  }
11010
11278
  }
11011
- try {
11012
- (console.log || console.debug)('[FieldMetadataEditor] init', {
11013
- controlType: this.controlType,
11014
- seedPreview: {
11015
- resourcePath: this.seed?.resourcePath,
11016
- optionLabelKey: this.seed?.optionLabelKey,
11017
- optionValueKey: this.seed?.optionValueKey,
11018
- filterCriteria: typeof this.seed?.filterCriteria === 'string'
11019
- ? this.seed?.filterCriteria
11020
- : this.seed?.filterCriteria != null
11021
- ? JSON.stringify(this.seed?.filterCriteria)
11022
- : undefined,
11023
- },
11024
- propsBefore: props.map((p) => p.name),
11025
- });
11026
- }
11027
- catch { }
11028
11279
  // Fallback de registro mínimo para facilitar execução inicial
11029
11280
  if (!props.length && canonicalProps?.length) {
11030
11281
  this.registry.register(this.controlType, canonicalProps);
@@ -11197,12 +11448,6 @@ class FieldMetadataEditorComponent {
11197
11448
  props = treeSelectProperties;
11198
11449
  }
11199
11450
  }
11200
- try {
11201
- (console.log || console.debug)('[FieldMetadataEditor] props after registry resolution', {
11202
- resolvedProps: props.map((p) => p.name),
11203
- });
11204
- }
11205
- catch { }
11206
11451
  // Inject controlType selector into 'Geral' group so users can change field type
11207
11452
  try {
11208
11453
  const typeProp = {
@@ -11221,28 +11466,6 @@ class FieldMetadataEditorComponent {
11221
11466
  }
11222
11467
  catch { }
11223
11468
  this.normalizedProps = this.normalizer.normalize(props, this.seed);
11224
- try {
11225
- console.debug('[FieldMetadataEditor] normalized props', {
11226
- propNames: this.normalizedProps.map((p) => p.name),
11227
- defaults: this.normalizedProps
11228
- .filter((p) => ['resourcePath', 'filterCriteria', 'optionLabelKey', 'optionValueKey'].includes(p.name))
11229
- .map((p) => ({ name: p.name, defaultValue: p.defaultValue })),
11230
- optionsDefault: (() => {
11231
- const opt = this.normalizedProps.find((p) => p.name === 'options');
11232
- if (!opt)
11233
- return undefined;
11234
- const v = opt.defaultValue;
11235
- return {
11236
- type: v == null ? 'null' : typeof v,
11237
- preview: typeof v === 'string' ? String(v).slice(0, 80) : undefined,
11238
- lengthOrCount: Array.isArray(v)
11239
- ? v.length
11240
- : (typeof v === 'string' ? v.length : undefined),
11241
- };
11242
- })(),
11243
- });
11244
- }
11245
- catch { }
11246
11469
  // Backward-compatible bridge: if a config still uses providerOptions, hydrate from seed.options.
11247
11470
  try {
11248
11471
  const hasProviderOptions = this.normalizedProps.some((p) => p.name === 'providerOptions');
@@ -11255,10 +11478,6 @@ class FieldMetadataEditorComponent {
11255
11478
  return String(seedOptions);
11256
11479
  } })();
11257
11480
  this.normalizedProps = this.normalizedProps.map((p) => p.name === 'providerOptions' && p.defaultValue === undefined ? { ...p, defaultValue: v } : p);
11258
- (console.log || console.debug)('[FieldMetadataEditor] providerOptions default injected from seed.options', {
11259
- type: typeof seedOptions,
11260
- lengthOrCount: Array.isArray(seedOptions) ? seedOptions.length : (typeof seedOptions === 'string' ? seedOptions.length : undefined),
11261
- });
11262
11481
  }
11263
11482
  }
11264
11483
  catch { }
@@ -11305,61 +11524,19 @@ class FieldMetadataEditorComponent {
11305
11524
  this.form.patchValue(subset, { emitEvent: false });
11306
11525
  }
11307
11526
  catch { }
11308
- // Diagnostics for Rating seeding after applying seed → form
11309
- try {
11310
- const ctU = String(this.controlType || '').toUpperCase();
11311
- if (ctU === String(FieldControlType.RATING)) {
11312
- const pick = (k) => this.form.get(k)?.value;
11313
- console.debug('[FieldMetadataEditor][RATING] seeded form values', {
11314
- max: pick('max'),
11315
- allowHalf: pick('allowHalf'),
11316
- selection: pick('selection'),
11317
- size: pick('size'),
11318
- icon: pick('icon'),
11319
- emptyIcon: pick('emptyIcon'),
11320
- svgIcon: pick('svgIcon'),
11321
- svgIconOutline: pick('svgIconOutline'),
11322
- ratingColor: pick('ratingColor'),
11323
- outlineColor: pick('outlineColor'),
11324
- readonly: pick('readonly'),
11325
- tabIndex: pick('tabIndex'),
11326
- ariaLabel: pick('ariaLabel'),
11327
- });
11328
- }
11329
- }
11330
- catch { }
11331
- }
11332
- catch { }
11333
- // Diagnostics: dump initial values for Rating editor
11334
- try {
11335
- const ctU = String(this.controlType || '').toUpperCase();
11336
- if (ctU === String(FieldControlType.RATING)) {
11337
- const pick = (k) => this.form.get(k)?.value;
11338
- console.debug('[FieldMetadataEditor][RATING] initial form values', {
11339
- max: pick('max'),
11340
- allowHalf: pick('allowHalf'),
11341
- selection: pick('selection'),
11342
- size: pick('size'),
11343
- icon: pick('icon'),
11344
- emptyIcon: pick('emptyIcon'),
11345
- svgIcon: pick('svgIcon'),
11346
- svgIconOutline: pick('svgIconOutline'),
11347
- ratingColor: pick('ratingColor'),
11348
- outlineColor: pick('outlineColor'),
11349
- readonly: pick('readonly'),
11350
- tabIndex: pick('tabIndex'),
11351
- ariaLabel: pick('ariaLabel'),
11352
- });
11353
- }
11354
11527
  }
11355
11528
  catch { }
11356
11529
  // Capture baseline after seed is applied to avoid missing first user change
11357
11530
  try {
11358
- this.initialSnapshot = this.form.getRawValue();
11531
+ if (this.initialSnapshot == null) {
11532
+ this.initialSnapshot = this.form.getRawValue();
11533
+ }
11359
11534
  this.baselineReady = true;
11360
- if (this.isDirty$.value)
11535
+ const controlTypeChanged = this.initialControlType !== undefined &&
11536
+ this.controlType !== this.initialControlType;
11537
+ if (!controlTypeChanged && this.isDirty$.value) {
11361
11538
  this.isDirty$.next(false);
11362
- (console.log || console.debug)('[FieldMetadataEditor] baseline captured (immediate)');
11539
+ }
11363
11540
  }
11364
11541
  catch {
11365
11542
  this.baselineReady = true;
@@ -11367,12 +11544,12 @@ class FieldMetadataEditorComponent {
11367
11544
  // track validity/dirty with snapshot-based detection (avoid false dirty on init)
11368
11545
  this.isValid$.next(this.form.valid);
11369
11546
  this.form.statusChanges
11370
- .pipe(takeUntilDestroyed(this.destroyRef))
11547
+ .pipe(takeUntil(this.formRebuild$), takeUntilDestroyed(this.destroyRef))
11371
11548
  .subscribe(() => this.isValid$.next(this.form.valid));
11372
11549
  this.setupGradientStopValidationSync();
11373
11550
  this.setupCurrencyPresetSync();
11374
11551
  this.form.valueChanges
11375
- .pipe(takeUntilDestroyed(this.destroyRef))
11552
+ .pipe(takeUntil(this.formRebuild$), takeUntilDestroyed(this.destroyRef))
11376
11553
  .subscribe((val) => {
11377
11554
  if (!this.baselineReady) {
11378
11555
  // Ignore initial churn until baseline is captured
@@ -11382,12 +11559,10 @@ class FieldMetadataEditorComponent {
11382
11559
  this.initialSnapshot = val;
11383
11560
  if (this.isDirty$.value)
11384
11561
  this.isDirty$.next(false);
11385
- try {
11386
- (console.log || console.debug)('[FieldMetadataEditor] baseline set from first stable value');
11387
- }
11388
- catch { }
11389
11562
  return;
11390
11563
  }
11564
+ const controlTypeChanged = this.initialControlType !== undefined &&
11565
+ this.controlType !== this.initialControlType;
11391
11566
  let same = false;
11392
11567
  try {
11393
11568
  same = JSON.stringify(val) === JSON.stringify(this.initialSnapshot);
@@ -11395,15 +11570,15 @@ class FieldMetadataEditorComponent {
11395
11570
  catch {
11396
11571
  same = false;
11397
11572
  }
11398
- const dirty = !same;
11573
+ const dirty = controlTypeChanged || !same;
11399
11574
  if (dirty !== this.isDirty$.value) {
11400
- try {
11401
- (console.log || console.debug)('[FieldMetadataEditor] dirty ->', dirty);
11402
- }
11403
- catch { }
11404
11575
  this.isDirty$.next(dirty);
11405
11576
  }
11406
11577
  });
11578
+ if (this.discoveredResourceOptions.length > 0) {
11579
+ this.renderedProps = this.buildRenderedProps(this.normalizedProps);
11580
+ }
11581
+ this.loadCatalogResourceOptionsIfNeeded();
11407
11582
  }
11408
11583
  setupGradientStopValidationSync() {
11409
11584
  const rows = new Map();
@@ -11419,7 +11594,7 @@ class FieldMetadataEditorComponent {
11419
11594
  if (!source)
11420
11595
  continue;
11421
11596
  source.valueChanges
11422
- .pipe(takeUntilDestroyed(this.destroyRef))
11597
+ .pipe(takeUntil(this.formRebuild$), takeUntilDestroyed(this.destroyRef))
11423
11598
  .subscribe(() => {
11424
11599
  for (const targetField of fieldNames) {
11425
11600
  if (targetField === sourceField)
@@ -11471,7 +11646,7 @@ class FieldMetadataEditorComponent {
11471
11646
  VES: { locale: 'es-VE', decimalPlaces: 2, currencyPosition: 'before' },
11472
11647
  };
11473
11648
  currencyControl.valueChanges
11474
- .pipe(takeUntilDestroyed(this.destroyRef))
11649
+ .pipe(takeUntil(this.formRebuild$), takeUntilDestroyed(this.destroyRef))
11475
11650
  .subscribe((value) => {
11476
11651
  const code = String(value || '').toUpperCase();
11477
11652
  const preset = presets[code];
@@ -11522,13 +11697,7 @@ class FieldMetadataEditorComponent {
11522
11697
  // Sempre incluir controlType no patch quando presente
11523
11698
  patch.controlType = this.controlType;
11524
11699
  try {
11525
- console.debug('[FieldMetadataEditor] apply() patch', patch);
11526
- }
11527
- catch { }
11528
- try {
11529
- const fn = this.__hostApplyPatch;
11530
- if (typeof fn === 'function')
11531
- fn(patch);
11700
+ this.hostApplyPatch?.(patch);
11532
11701
  }
11533
11702
  catch { }
11534
11703
  this.applied.emit(patch);
@@ -11549,10 +11718,6 @@ class FieldMetadataEditorComponent {
11549
11718
  delete patch.providerOptions;
11550
11719
  }
11551
11720
  patch.controlType = this.controlType;
11552
- try {
11553
- console.debug('[FieldMetadataEditor] getSettingsValue() patch', patch);
11554
- }
11555
- catch { }
11556
11721
  return patch;
11557
11722
  }
11558
11723
  /**
@@ -11834,6 +11999,9 @@ class FieldMetadataEditorComponent {
11834
11999
  }
11835
12000
  }
11836
12001
  reset() {
12002
+ if (this.initialControlType !== undefined && this.controlType !== this.initialControlType) {
12003
+ this.rebuildEditorForType(this.initialControlType);
12004
+ }
11837
12005
  const snapshot = this.cloneSnapshot(this.initialSnapshot ?? this.form.getRawValue());
11838
12006
  this.form.reset(snapshot);
11839
12007
  this.form.markAsPristine();
@@ -11860,11 +12028,321 @@ class FieldMetadataEditorComponent {
11860
12028
  get isArrayEditor() {
11861
12029
  return this.controlType === FieldControlType.ARRAY_INPUT;
11862
12030
  }
12031
+ static ROOT_DEPENDENT_FIELD_KEY_PROPS = new Set([
12032
+ 'optionValueKey',
12033
+ 'optionLabelKey',
12034
+ 'lookupIdKey',
12035
+ 'lookupLabelKey',
12036
+ 'lookupSubtitleKey',
12037
+ 'parentIdKey',
12038
+ ]);
12039
+ static OPTION_SOURCE_DEPENDENT_FIELD_KEY_PROPS = new Set([
12040
+ 'optionSource.valuePropertyPath',
12041
+ 'optionSource.labelPropertyPath',
12042
+ 'optionSource.codePropertyPath',
12043
+ 'optionSource.statusPropertyPath',
12044
+ 'optionSource.subtitlePropertyPath',
12045
+ 'optionSource.descriptionPropertyPath',
12046
+ ]);
11863
12047
  buildRenderedProps(props) {
11864
- if (!this.isArrayEditor) {
11865
- return props;
12048
+ const base = !this.isArrayEditor
12049
+ ? props || []
12050
+ : (props || []).filter((prop) => prop?.name !== 'array.itemSchema.fields');
12051
+ if (!this.discoveredResourceOptions.length &&
12052
+ this.resourceSchemaFieldsByPath.size === 0) {
12053
+ return base;
12054
+ }
12055
+ return base.map((prop) => {
12056
+ const resourceEnriched = this.enrichResourcePathProperty(prop);
12057
+ return this.enrichDependentFieldKeyProperty(resourceEnriched);
12058
+ });
12059
+ }
12060
+ enrichResourcePathProperty(prop) {
12061
+ if (!prop ||
12062
+ (prop.name !== 'resourcePath' && prop.name !== 'optionSource.resourcePath')) {
12063
+ return prop;
12064
+ }
12065
+ const optionSourceType = String(this.form?.get('optionSource.type')?.value ??
12066
+ this.seed?.optionSource?.type ??
12067
+ '')
12068
+ .trim()
12069
+ .toLowerCase();
12070
+ const useStatsIntent = prop.name === 'optionSource.resourcePath' && optionSourceType === 'stats';
12071
+ const filteredResources = this.discoveredResourceOptions.filter((item) => useStatsIntent
12072
+ ? item.capabilities?.stats !== false
12073
+ : item.capabilities?.options !== false);
12074
+ if (!filteredResources.length) {
12075
+ return prop;
12076
+ }
12077
+ const currentValue = String(this.form?.get(prop.name)?.value ?? prop.defaultValue ?? '').trim();
12078
+ const matchedCatalogOption = currentValue
12079
+ ? this.resourceDiscovery?.findCatalogResourceOption?.(currentValue, filteredResources)
12080
+ : null;
12081
+ const readOnlyLabel = this.i18n?.t('quickConnect.readOnly', undefined, 'Somente leitura', 'resourceDiscovery') ?? 'Somente leitura';
12082
+ const options = filteredResources.map((item) => {
12083
+ const readOnlySuffix = item.readOnly ? readOnlyLabel : null;
12084
+ const hasDistinctPath = Boolean(item.path && item.path !== item.label);
12085
+ const code = hasDistinctPath ? item.path : null;
12086
+ const badge = [item.group, readOnlySuffix]
12087
+ .filter((part) => Boolean(part))
12088
+ .join(' · ') || null;
12089
+ const description = String(item.description ?? '').trim() || null;
12090
+ return {
12091
+ value: item === matchedCatalogOption && currentValue ? currentValue : item.value,
12092
+ text: item.label,
12093
+ icon: item.icon ?? 'api',
12094
+ code,
12095
+ badge,
12096
+ description,
12097
+ searchTerms: [
12098
+ ...item.searchTerms,
12099
+ ...(readOnlySuffix ? [readOnlySuffix] : []),
12100
+ ],
12101
+ };
12102
+ });
12103
+ if (currentValue && !matchedCatalogOption) {
12104
+ options.unshift({
12105
+ value: currentValue,
12106
+ text: currentValue,
12107
+ icon: 'link',
12108
+ code: null,
12109
+ badge: null,
12110
+ description: null,
12111
+ searchTerms: [currentValue],
12112
+ });
12113
+ }
12114
+ return {
12115
+ ...prop,
12116
+ editorType: 'select',
12117
+ searchable: true,
12118
+ searchPlaceholderKey: 'fieldResource.searchPlaceholder',
12119
+ searchPlaceholder: 'Buscar por nome, domínio ou caminho',
12120
+ options,
12121
+ };
12122
+ }
12123
+ enrichDependentFieldKeyProperty(prop) {
12124
+ const propName = String(prop?.name ?? '').trim();
12125
+ const isRootDependent = FieldMetadataEditorComponent.ROOT_DEPENDENT_FIELD_KEY_PROPS.has(propName);
12126
+ const isOptionSourceDependent = FieldMetadataEditorComponent.OPTION_SOURCE_DEPENDENT_FIELD_KEY_PROPS.has(propName);
12127
+ if (!isRootDependent && !isOptionSourceDependent) {
12128
+ return prop;
12129
+ }
12130
+ const resourceControlPath = isOptionSourceDependent
12131
+ ? 'optionSource.resourcePath'
12132
+ : 'resourcePath';
12133
+ const resourcePath = String(this.form?.get(resourceControlPath)?.value ??
12134
+ (isOptionSourceDependent
12135
+ ? this.seed?.optionSource?.resourcePath
12136
+ : this.seed?.resourcePath) ??
12137
+ '').trim();
12138
+ if (!resourcePath) {
12139
+ return prop;
12140
+ }
12141
+ const fields = this.getCachedFieldsForResource(resourcePath);
12142
+ if (!fields.length) {
12143
+ return prop;
12144
+ }
12145
+ const currentValue = String(this.form?.get(propName)?.value ?? prop.defaultValue ?? '').trim();
12146
+ const options = fields.map((field) => {
12147
+ const fieldName = String(field.name ?? '').trim();
12148
+ const fieldLabel = String(field.label ?? '').trim();
12149
+ const hasDistinctLabel = Boolean(fieldLabel && fieldLabel !== fieldName);
12150
+ const typeHint = String(field.type || field.controlType || '').trim();
12151
+ const description = String(field.description ?? '').trim() || null;
12152
+ return {
12153
+ value: fieldName,
12154
+ text: hasDistinctLabel ? fieldLabel : fieldName,
12155
+ code: hasDistinctLabel ? fieldName : null,
12156
+ badge: typeHint || null,
12157
+ description,
12158
+ searchTerms: [
12159
+ fieldName,
12160
+ fieldLabel,
12161
+ typeHint,
12162
+ description ?? '',
12163
+ ].filter(Boolean),
12164
+ };
12165
+ });
12166
+ if (currentValue &&
12167
+ !options.some((option) => option.value === currentValue)) {
12168
+ options.unshift({
12169
+ value: currentValue,
12170
+ text: currentValue,
12171
+ code: null,
12172
+ badge: null,
12173
+ description: null,
12174
+ searchTerms: [currentValue],
12175
+ });
12176
+ }
12177
+ return {
12178
+ ...prop,
12179
+ editorType: 'select',
12180
+ searchable: true,
12181
+ searchPlaceholderKey: 'fieldResource.fieldSearchPlaceholder',
12182
+ searchPlaceholder: 'Buscar campo por nome ou rótulo',
12183
+ options,
12184
+ };
12185
+ }
12186
+ normalizeResourceCacheKey(resourcePath) {
12187
+ const raw = String(resourcePath ?? '').trim();
12188
+ if (!raw) {
12189
+ return '';
12190
+ }
12191
+ if (this.discoveredResourceOptions.length > 0) {
12192
+ const matched = this.resourceDiscovery?.findCatalogResourceOption?.(raw, this.discoveredResourceOptions);
12193
+ if (matched?.value) {
12194
+ return matched.value.trim().replace(/^\/+|\/+$/g, '');
12195
+ }
12196
+ }
12197
+ return raw.replace(/^\/+|\/+$/g, '').replace(/^api\/+/i, '');
12198
+ }
12199
+ getCachedFieldsForResource(resourcePath) {
12200
+ const key = this.normalizeResourceCacheKey(resourcePath);
12201
+ if (!key) {
12202
+ return [];
12203
+ }
12204
+ const direct = this.resourceSchemaFieldsByPath.get(key);
12205
+ if (direct?.length) {
12206
+ return direct;
12207
+ }
12208
+ const rawKey = String(resourcePath ?? '').trim().replace(/^\/+|\/+$/g, '');
12209
+ return this.resourceSchemaFieldsByPath.get(rawKey) ?? [];
12210
+ }
12211
+ setCachedFieldsForResource(resourcePath, fields) {
12212
+ const key = this.normalizeResourceCacheKey(resourcePath);
12213
+ if (key) {
12214
+ this.resourceSchemaFieldsByPath.set(key, fields);
12215
+ }
12216
+ const rawKey = String(resourcePath ?? '').trim().replace(/^\/+|\/+$/g, '');
12217
+ if (rawKey) {
12218
+ this.resourceSchemaFieldsByPath.set(rawKey, fields);
12219
+ }
12220
+ }
12221
+ applyAutoSuggestedDependentKeys(scope, fields) {
12222
+ if (!this.form || !fields.length) {
12223
+ return;
11866
12224
  }
11867
- return (props || []).filter((prop) => prop?.name !== 'array.itemSchema.fields');
12225
+ const fieldNameSet = new Set(fields.map((field) => String(field.name ?? '').trim()).filter(Boolean));
12226
+ const suggestions = this.selectSuggestedFieldKeys(fields);
12227
+ const targets = scope === 'optionSource'
12228
+ ? [
12229
+ {
12230
+ path: 'optionSource.valuePropertyPath',
12231
+ suggested: suggestions.valueKey,
12232
+ },
12233
+ {
12234
+ path: 'optionSource.labelPropertyPath',
12235
+ suggested: suggestions.labelKey,
12236
+ },
12237
+ {
12238
+ path: 'optionSource.codePropertyPath',
12239
+ suggested: suggestions.codeKey,
12240
+ },
12241
+ {
12242
+ path: 'optionSource.statusPropertyPath',
12243
+ suggested: suggestions.statusKey,
12244
+ },
12245
+ ]
12246
+ : [
12247
+ { path: 'optionValueKey', suggested: suggestions.valueKey },
12248
+ { path: 'optionLabelKey', suggested: suggestions.labelKey },
12249
+ { path: 'lookupIdKey', suggested: suggestions.valueKey },
12250
+ { path: 'lookupLabelKey', suggested: suggestions.labelKey },
12251
+ { path: 'lookupSubtitleKey', suggested: suggestions.subtitleKey },
12252
+ ];
12253
+ for (const target of targets) {
12254
+ const ctrl = this.form.get(target.path);
12255
+ if (!ctrl) {
12256
+ continue;
12257
+ }
12258
+ const current = String(ctrl.value ?? '').trim();
12259
+ if (!current || !fieldNameSet.has(current)) {
12260
+ const nextValue = target.suggested ?? '';
12261
+ if (current !== nextValue) {
12262
+ ctrl.setValue(nextValue);
12263
+ }
12264
+ }
12265
+ }
12266
+ }
12267
+ selectSuggestedFieldKeys(fields) {
12268
+ const names = fields
12269
+ .map((field) => String(field.name ?? '').trim())
12270
+ .filter(Boolean);
12271
+ const findExact = (candidates, exclude = new Set()) => {
12272
+ for (const candidate of candidates) {
12273
+ const lower = candidate.toLowerCase();
12274
+ const found = names.find((name) => !exclude.has(name) && name.toLowerCase() === lower);
12275
+ if (found) {
12276
+ return found;
12277
+ }
12278
+ }
12279
+ return null;
12280
+ };
12281
+ const valueKey = findExact([
12282
+ 'id',
12283
+ 'codigo',
12284
+ 'code',
12285
+ 'uuid',
12286
+ 'key',
12287
+ 'valor',
12288
+ 'value',
12289
+ 'matricula',
12290
+ 'sigla',
12291
+ ]) ??
12292
+ names.find((name) => /(?:Id|Codigo|Code)$/.test(name)) ??
12293
+ names[0] ??
12294
+ null;
12295
+ const usedForValue = new Set(valueKey ? [valueKey] : []);
12296
+ const labelKey = findExact([
12297
+ 'nome',
12298
+ 'name',
12299
+ 'label',
12300
+ 'descricao',
12301
+ 'description',
12302
+ 'titulo',
12303
+ 'title',
12304
+ 'razaosocial',
12305
+ 'nomecompleto',
12306
+ 'rotulo',
12307
+ 'text',
12308
+ ], usedForValue) ??
12309
+ fields
12310
+ .find((field) => !usedForValue.has(String(field.name ?? '').trim()) &&
12311
+ String(field.type ?? 'string').toLowerCase() === 'string')
12312
+ ?.name?.trim() ??
12313
+ names.find((name) => !usedForValue.has(name)) ??
12314
+ valueKey;
12315
+ const codeKey = findExact([
12316
+ 'codigo',
12317
+ 'code',
12318
+ 'sigla',
12319
+ 'chave',
12320
+ 'matricula',
12321
+ 'referencia',
12322
+ ]);
12323
+ const statusKey = findExact([
12324
+ 'status',
12325
+ 'situacao',
12326
+ 'ativo',
12327
+ 'active',
12328
+ 'estado',
12329
+ 'enabled',
12330
+ ]);
12331
+ const usedForPrimary = new Set([valueKey, labelKey].filter((item) => Boolean(item)));
12332
+ const subtitleKey = findExact([
12333
+ 'descricao',
12334
+ 'description',
12335
+ 'subtitulo',
12336
+ 'subtitle',
12337
+ 'resumo',
12338
+ 'summary',
12339
+ 'email',
12340
+ 'cargo',
12341
+ 'departamento',
12342
+ 'sigla',
12343
+ 'codigo',
12344
+ ], usedForPrimary);
12345
+ return { valueKey, labelKey, codeKey, statusKey, subtitleKey };
11868
12346
  }
11869
12347
  cloneSnapshot(value) {
11870
12348
  return this.cloneMetadataValue(value, new WeakMap());
@@ -11977,6 +12455,7 @@ class FieldMetadataEditorComponent {
11977
12455
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: FieldMetadataEditorComponent, deps: [{ token: ConfigRegistryService }, { token: SchemaNormalizerService }, { token: DynamicFormFactoryService }, { token: ContextValidatorRegistryService }], target: i0.ɵɵFactoryTarget.Component });
11978
12456
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: FieldMetadataEditorComponent, isStandalone: true, selector: "praxis-field-metadata-editor", inputs: { controlType: "controlType", seed: "seed" }, outputs: { applied: "applied", cancel: "cancel" }, providers: [
11979
12457
  providePraxisMetadataEditorI18n(),
12458
+ providePraxisI18nConfig(RESOURCE_DISCOVERY_I18N_CONFIG),
11980
12459
  ...providePraxisDynamicFieldsI18n(),
11981
12460
  ], ngImport: i0, template: `
11982
12461
  @if (form) {
@@ -12007,6 +12486,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
12007
12486
  ],
12008
12487
  providers: [
12009
12488
  providePraxisMetadataEditorI18n(),
12489
+ providePraxisI18nConfig(RESOURCE_DISCOVERY_I18N_CONFIG),
12010
12490
  ...providePraxisDynamicFieldsI18n(),
12011
12491
  ],
12012
12492
  template: `
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@praxisui/metadata-editor",
3
- "version": "10.0.0-rc.31",
3
+ "version": "10.0.0-rc.33",
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.31",
12
- "@praxisui/core": "^10.0.0-rc.31",
13
- "@praxisui/dynamic-fields": "^10.0.0-rc.31",
14
- "@praxisui/settings-panel": "^10.0.0-rc.31",
11
+ "@praxisui/ai": "^10.0.0-rc.33",
12
+ "@praxisui/core": "^10.0.0-rc.33",
13
+ "@praxisui/dynamic-fields": "^10.0.0-rc.33",
14
+ "@praxisui/settings-panel": "^10.0.0-rc.33",
15
15
  "rxjs": "~7.8.0"
16
16
  },
17
17
  "dependencies": {
@@ -22,6 +22,11 @@ interface EditorProperty {
22
22
  value: any;
23
23
  text: string;
24
24
  textKey?: string;
25
+ icon?: string;
26
+ code?: string | null;
27
+ badge?: string | null;
28
+ description?: string | null;
29
+ searchTerms?: string[];
25
30
  }> | {
26
31
  type: 'static' | 'http' | 'fn';
27
32
  config?: any;
@@ -154,7 +159,10 @@ declare class FieldMetadataEditorComponent implements OnInit {
154
159
  normalizedProps: any[];
155
160
  renderedProps: any[];
156
161
  private initialSnapshot;
162
+ private initialControlType?;
157
163
  private baselineReady;
164
+ private readonly formRebuild$;
165
+ private readonly hostApplyPatch?;
158
166
  typeOptions: Array<{
159
167
  value: FieldControlType;
160
168
  label: string;
@@ -163,9 +171,20 @@ declare class FieldMetadataEditorComponent implements OnInit {
163
171
  isValid$: BehaviorSubject<boolean>;
164
172
  isBusy$: BehaviorSubject<boolean>;
165
173
  private readonly destroyRef;
174
+ private readonly resourceDiscovery;
175
+ private readonly i18n;
176
+ private discoveredResourceOptions;
166
177
  private lastCurrencyPreset;
167
178
  constructor(registry: ConfigRegistryService, normalizer: SchemaNormalizerService, factory: DynamicFormFactoryService, ctxValidators: ContextValidatorRegistryService);
179
+ private catalogLookupRequested;
180
+ private readonly resourceSchemaFieldsByPath;
181
+ private readonly resourceSchemaRequestIdByScope;
168
182
  ngOnInit(): void;
183
+ private loadCatalogResourceOptionsIfNeeded;
184
+ private resolveDiscoveryEndpointKey;
185
+ private bindResourcePathSchemaSync;
186
+ private seedCatalogResponseFieldsCache;
187
+ private resolveAndApplyResourceSchemaFields;
169
188
  private resolveCanonicalEditorProperties;
170
189
  private rebuildEditorForType;
171
190
  private setupGradientStopValidationSync;
@@ -210,7 +229,16 @@ declare class FieldMetadataEditorComponent implements OnInit {
210
229
  onControlTypeChangeEvent(event: Event): void;
211
230
  private withSharedFieldMetadataProperties;
212
231
  get isArrayEditor(): boolean;
232
+ private static readonly ROOT_DEPENDENT_FIELD_KEY_PROPS;
233
+ private static readonly OPTION_SOURCE_DEPENDENT_FIELD_KEY_PROPS;
213
234
  private buildRenderedProps;
235
+ private enrichResourcePathProperty;
236
+ private enrichDependentFieldKeyProperty;
237
+ private normalizeResourceCacheKey;
238
+ private getCachedFieldsForResource;
239
+ private setCachedFieldsForResource;
240
+ private applyAutoSuggestedDependentKeys;
241
+ private selectSuggestedFieldKeys;
214
242
  private cloneSnapshot;
215
243
  private cloneMetadataValue;
216
244
  private canEmitSettingsValue;