@praxisui/metadata-editor 10.0.0-rc.4 → 10.0.0-rc.41

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.
@@ -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, 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';
@@ -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)',
@@ -561,6 +559,16 @@ const METADATA_EDITOR_EN_US = {
561
559
  'praxis.metadataEditor.presentationFields.tooltip.hint': 'Short explanation shown as a tooltip.',
562
560
  'praxis.metadataEditor.presentationFields.presentationRules.label': 'Presentation rules',
563
561
  'praxis.metadataEditor.presentationFields.presentationRules.hint': 'Guided builder for ordered Json Logic rules that only change presentation. Later rules win.',
562
+ 'praxis.metadataEditor.presentationFields.quickViewVisible.label': 'Show in Quick View summary',
563
+ 'praxis.metadataEditor.presentationFields.quickViewVisible.hint': 'Controls x-ui.quickViewVisible when this resource is previewed inside a Quick View card.',
564
+ 'praxis.metadataEditor.presentationFields.quickViewOrder.label': 'Quick View summary order',
565
+ 'praxis.metadataEditor.presentationFields.quickViewOrder.hint': 'Numeric priority (x-ui.quickViewOrder) for this field in Quick View summary items.',
566
+ 'praxis.metadataEditor.presentationFields.quickViewPreviewSurfaceId.label': 'Quick View: preview surface',
567
+ 'praxis.metadataEditor.presentationFields.quickViewPreviewSurfaceId.hint': 'Optional dedicated read-only surface for this field\u2019s Quick View card (preserves canonical server recordOpen).',
568
+ 'praxis.metadataEditor.presentationFields.quickViewDetailPresentation.label': 'Quick View: detail handoff mode',
569
+ 'praxis.metadataEditor.presentationFields.quickViewDetailPresentation.hint': 'Defines whether the primary Quick View CTA opens the full surface in a drawer or modal.',
570
+ 'praxis.metadataEditor.presentationFields.quickViewDetailPresentation.option.drawer': 'Side drawer',
571
+ 'praxis.metadataEditor.presentationFields.quickViewDetailPresentation.option.modal': 'Modal dialog',
564
572
  'praxis.metadataEditor.presentationChoice.help': 'Help',
565
573
  'praxis.metadataEditor.presentationChoice.clear': 'Clear selection',
566
574
  'praxis.metadataEditor.presentationRulesBuilder.title': 'Presentation rules',
@@ -655,6 +663,8 @@ const METADATA_EDITOR_EN_US = {
655
663
  'praxis.metadataEditor.colorPalette.moveDown': 'Move down',
656
664
  'praxis.metadataEditor.colorPalette.remove': 'Remove',
657
665
  'praxis.metadataEditor.colorPalette.invalid': 'Enter a valid CSS color or var(--token).',
666
+ 'praxis.metadataEditor.smartCalibrate.action': 'Calibrate from schema',
667
+ '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.',
658
668
  };
659
669
 
660
670
  const METADATA_EDITOR_PT_BR = {
@@ -694,6 +704,7 @@ const METADATA_EDITOR_PT_BR = {
694
704
  'praxis.metadataEditor.arrayFields.control.date': 'Data',
695
705
  'praxis.metadataEditor.fieldResource.label': 'Recurso governado',
696
706
  'praxis.metadataEditor.fieldResource.searchPlaceholder': 'Buscar por nome, dom\u00ednio ou caminho',
707
+ 'praxis.metadataEditor.fieldResource.fieldSearchPlaceholder': 'Buscar campo por nome ou r\u00f3tulo',
697
708
  'praxis.metadataEditor.fieldResource.hint': 'Selecione no cat\u00e1logo governado do host. O valor gravado continua sendo resourcePath.',
698
709
  'praxis.metadataEditor.common.help': 'Ajuda',
699
710
  'praxis.metadataEditor.common.iconMaterialSymbolsHint': 'Use o nome do \u00edcone (Material Symbols)',
@@ -812,6 +823,16 @@ const METADATA_EDITOR_PT_BR = {
812
823
  'praxis.metadataEditor.presentationFields.tooltip.hint': 'Explica\u00e7\u00e3o curta exibida como tooltip.',
813
824
  'praxis.metadataEditor.presentationFields.presentationRules.label': 'Regras de apresenta\u00e7\u00e3o',
814
825
  'praxis.metadataEditor.presentationFields.presentationRules.hint': 'Builder guiado para regras Json Logic ordenadas que alteram somente a apresenta\u00e7\u00e3o. Regras posteriores vencem.',
826
+ 'praxis.metadataEditor.presentationFields.quickViewVisible.label': 'Exibir no resumo Quick View',
827
+ 'praxis.metadataEditor.presentationFields.quickViewVisible.hint': 'Controla x-ui.quickViewVisible quando este recurso \u00e9 exibido em um card de Quick View.',
828
+ 'praxis.metadataEditor.presentationFields.quickViewOrder.label': 'Ordem no resumo Quick View',
829
+ 'praxis.metadataEditor.presentationFields.quickViewOrder.hint': 'Prioridade num\u00e9rica (x-ui.quickViewOrder) deste campo na s\u00edntese do card Quick View.',
830
+ 'praxis.metadataEditor.presentationFields.quickViewPreviewSurfaceId.label': 'Quick View: surface de preview',
831
+ 'praxis.metadataEditor.presentationFields.quickViewPreviewSurfaceId.hint': 'Surface read-only opcional dedicada ao card de Quick View deste v\u00ednculo (preserva o recordOpen can\u00f4nico do servidor).',
832
+ 'praxis.metadataEditor.presentationFields.quickViewDetailPresentation.label': 'Quick View: abertura de detalhe',
833
+ 'praxis.metadataEditor.presentationFields.quickViewDetailPresentation.hint': 'Define se o CTA prim\u00e1rio do Quick View abre a superf\u00edcie completa em drawer ou modal.',
834
+ 'praxis.metadataEditor.presentationFields.quickViewDetailPresentation.option.drawer': 'Gaveta lateral (drawer)',
835
+ 'praxis.metadataEditor.presentationFields.quickViewDetailPresentation.option.modal': 'Modal (dialog)',
815
836
  'praxis.metadataEditor.presentationChoice.help': 'Ajuda',
816
837
  'praxis.metadataEditor.presentationChoice.clear': 'Limpar sele\u00e7\u00e3o',
817
838
  'praxis.metadataEditor.presentationRulesBuilder.title': 'Regras de apresenta\u00e7\u00e3o',
@@ -906,6 +927,8 @@ const METADATA_EDITOR_PT_BR = {
906
927
  'praxis.metadataEditor.colorPalette.moveDown': 'Mover para baixo',
907
928
  'praxis.metadataEditor.colorPalette.remove': 'Remover',
908
929
  'praxis.metadataEditor.colorPalette.invalid': 'Informe uma cor CSS válida ou var(--token).',
930
+ 'praxis.metadataEditor.smartCalibrate.action': 'Calibrar pelo schema',
931
+ '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.',
909
932
  };
910
933
 
911
934
  function createPraxisMetadataEditorI18nConfig(options = {}) {
@@ -2721,6 +2744,9 @@ class DynamicEditorRendererComponent {
2721
2744
  return options.map((option) => ({
2722
2745
  ...option,
2723
2746
  text: this.resolveEditorText(option.textKey, option.text) ?? option.text,
2747
+ code: option.code ?? undefined,
2748
+ badge: option.badge ?? undefined,
2749
+ description: option.description ?? undefined,
2724
2750
  }));
2725
2751
  }
2726
2752
  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 });
@@ -2874,7 +2900,7 @@ class DynamicEditorRendererComponent {
2874
2900
  }
2875
2901
  </mat-accordion>
2876
2902
  </div>
2877
- `, isInline: true, styles: [":host{display:block}.editor-surface{padding:8px 0 2px;border-radius:12px;background:transparent}.section-accordion{display:block}.section-card{margin:6px 0 8px;border-radius:12px;border:1px solid var(--md-sys-color-outline-variant);overflow:hidden}.section-card.mat-expanded{border-color:var(--md-sys-color-primary);box-shadow:0 2px 10px #0000000d}.section-card .mat-expansion-panel-body{background:var(--md-sys-color-surface-container-low)}.section-header{background:var(--md-sys-color-surface-container-low);border-bottom:1px solid var(--md-sys-color-outline-variant, var(--sicoob-stroke-medium));padding:2px 8px;min-height:32px;display:flex;align-items:center;color:var(--md-sys-color-on-surface)}.section-header .mat-expansion-panel-header-title,.section-header .mat-expansion-panel-header-description{display:flex;align-items:center;color:var(--md-sys-color-on-surface)}.section-title{display:inline-flex;align-items:center;gap:6px;font-weight:600;letter-spacing:.2px;color:var(--md-sys-color-on-surface);font-size:.9rem;line-height:1.2}.section-icon{color:var( --md-sys-color-primary, var(--md-sys-color-primary, #3f51b5) );opacity:.9}.editor-row{display:flex;gap:12px;align-items:flex-start;flex-wrap:wrap;margin-bottom:8px}.editor-row>praxis-field-shell{flex:1 1 100%;min-width:280px}.editor-row praxis-field-shell .mat-mdc-form-field{width:100%;margin-bottom:8px}.editor-row.row-inline>praxis-field-shell{flex:0 0 auto}.editor-row.row-2>praxis-field-shell{flex:1 1 calc(50% - 12px);min-width:260px}.editor-row.row-3>praxis-field-shell{flex:1 1 calc(33.333% - 12px);min-width:220px}.editor-row.row-gradient-stops>praxis-field-shell{flex:1 1 100%;min-width:0;width:100%}.editor-row.row-gradient-stops .editor-item>pdx-color-input{flex:1 1 auto;min-width:0}.gradient-stops-preview{display:flex;flex:1 1 100%;align-items:center;justify-content:space-between;gap:16px;min-width:0;padding:14px 16px;border:1px solid var(--md-sys-color-outline-variant);border-radius:12px;background:var(--md-sys-color-surface-container);color:var(--md-sys-color-on-surface)}.gradient-stops-preview__copy{display:grid;gap:3px;min-width:0}.gradient-stops-preview__copy strong{font:var(--md-sys-typescale-label-large-font, inherit);font-weight:650}.gradient-stops-preview__copy span{color:var(--md-sys-color-on-surface-variant);font:var(--md-sys-typescale-body-small-font, inherit);line-height:1.35}.gradient-stops-preview__stars{flex:0 0 auto;color:transparent;background-clip:text;-webkit-background-clip:text;font-size:30px;line-height:1;letter-spacing:2px}.gradient-stops-preview__radial{position:relative;flex:0 0 auto;width:76px;height:76px;border-radius:50%;box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--md-sys-color-outline-variant) 70%,transparent)}.gradient-stops-preview__radial:before,.gradient-stops-preview__radial:after,.gradient-stops-preview__radial>span{content:\"\";position:absolute;inset:12px;border:1px dashed color-mix(in srgb,var(--md-sys-color-on-surface) 35%,transparent);border-radius:50%}.gradient-stops-preview__radial:after{inset:25px}.gradient-stops-preview__radial>span{inset:35px;border:0;background:var(--md-sys-color-on-surface)}@media(forced-colors:active){.gradient-stops-preview{border-color:CanvasText}.gradient-stops-preview__stars,.gradient-stops-preview__radial{color:CanvasText;background:none}}.editor-row>praxis-field-shell[data-field-name=controlType],.editor-row>praxis-field-shell[data-field-name=label],.editor-row>praxis-field-shell[data-field-name=placeholder]{flex:1 1 calc(50% - 12px);min-width:300px}.editor-row>praxis-presentation-rules-builder.presentation-rules-field{flex:1 1 100%;min-width:0}.editor-row>praxis-color-palette-editor.color-palette-field{flex:1 1 100%;min-width:0}.editor-row>praxis-presentation-choice-control.presentation-choice-field{flex:1 1 100%;min-width:0}praxis-field-shell[data-field-name=label] .mdc-floating-label,praxis-field-shell[data-field-name=placeholder] .mdc-floating-label{font-weight:600}.editor-row>praxis-field-shell[data-field-name=prefix],.editor-row>praxis-field-shell[data-field-name=suffix]{flex:0 0 200px;min-width:160px}.editor-row>praxis-field-shell[data-field-name=prefixIcon],.editor-row>praxis-field-shell[data-field-name=suffixIcon],.editor-row>praxis-field-shell[data-field-name=prefixIconColor],.editor-row>praxis-field-shell[data-field-name=suffixIconColor]{flex:1 1 auto;min-width:160px}.editor-item{display:flex;align-items:flex-start;gap:4px;min-width:0;width:100%}.editor-item>ng-container,.editor-item>praxis-field-shell{flex:1;min-width:0}.actions-container{display:flex;align-items:center;gap:0;margin-top:12px;flex-shrink:0}.decorator-btn{position:static;color:var(--md-sys-color-primary, #3f51b5);opacity:.6;transform:scale(.8);width:32px;height:32px;line-height:32px;padding:0}.decorator-btn:hover{opacity:1}.icon-picker-btn,.hint-btn,.hint-btn.has-icon-picker{right:auto}.editor-row .mat-mdc-form-field-subscript-wrapper mat-hint{display:none!important}.editor-row .mat-mdc-form-field-subscript-wrapper mat-error{display:block!important}praxis-field-shell[data-field-name=showCharCount] .mdc-switch{transform:scale(1.05)}praxis-field-shell[data-field-name=showCharCount] .mat-mdc-slide-toggle{font-weight:600}praxis-field-shell[data-field-name=controlType] .mat-mdc-form-field-hint{display:flex;align-items:center;gap:8px;background:var(--md-sys-color-tertiary-container);border:1px solid var(--md-sys-color-tertiary);color:var(--md-sys-color-on-tertiary-container);padding:8px 10px;border-radius:6px;line-height:1.3;white-space:normal}praxis-field-shell[data-field-name=controlType] .mat-mdc-form-field-hint:before{content:\"\\26a0\\fe0f\";display:inline-block}praxis-field-shell[data-field-name=controlType] .mat-mdc-form-field-subscript-wrapper{margin-top:6px;margin-bottom:10px;min-height:auto}praxis-field-shell[data-field-name=controlType] .mat-mdc-form-field{margin-bottom:16px}@media(max-width:520px){.editor-row,.editor-row.row-inline{gap:8px}.editor-item{flex-direction:column;gap:2px}.editor-row.row-inline>praxis-field-shell,.editor-row.row-2>praxis-field-shell,.editor-row.row-3>praxis-field-shell{flex:1 1 100%;min-width:0}.gradient-stops-preview{align-items:flex-start;flex-direction:column}.gradient-stops-preview__stars{font-size:27px}.actions-container{width:100%;justify-content:flex-end;margin-top:0}.editor-row.row-gradient-stops .editor-item{align-items:flex-start;flex-direction:row;gap:4px}.editor-row.row-gradient-stops .actions-container{width:auto;margin-top:12px}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i3$2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i3$2.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: DynamicFieldLoaderDirective, selector: "[dynamicFieldLoader]", inputs: ["fields", "formGroup", "enableExternalControlBinding", "itemTemplate", "debugTrace", "debugTraceLabel", "readonlyMode", "disabledMode", "presentationMode", "visible", "canvasMode"], outputs: ["componentsCreated", "fieldCreated", "fieldDestroyed", "renderError", "canvasMouseEnter", "canvasMouseLeave", "canvasClick", "canvasFocus"] }, { kind: "ngmodule", type: MatExpansionModule }, { kind: "directive", type: i5$1.MatAccordion, selector: "mat-accordion", inputs: ["hideToggle", "displayMode", "togglePosition"], exportAs: ["matAccordion"] }, { kind: "component", type: i5$1.MatExpansionPanel, selector: "mat-expansion-panel", inputs: ["hideToggle", "togglePosition"], outputs: ["afterExpand", "afterCollapse"], exportAs: ["matExpansionPanel"] }, { kind: "component", type: i5$1.MatExpansionPanelHeader, selector: "mat-expansion-panel-header", inputs: ["expandedHeight", "collapsedHeight", "tabIndex"] }, { kind: "directive", type: i5$1.MatExpansionPanelTitle, selector: "mat-panel-title" }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i2$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i1$2.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatDialogModule }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i3$1.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "directive", type: PraxisIconDirective, selector: "mat-icon[praxisIcon]", inputs: ["praxisIcon"] }, { kind: "component", type: PresentationRulesBuilderComponent, selector: "praxis-presentation-rules-builder" }, { kind: "component", type: PresentationChoiceControlComponent, selector: "praxis-presentation-choice-control", inputs: ["field"] }, { kind: "component", type: ColorPaletteEditorComponent, selector: "praxis-color-palette-editor", inputs: ["formControl"] }], encapsulation: i0.ViewEncapsulation.None });
2903
+ `, isInline: true, styles: [":host{display:block}.editor-surface{padding:8px 0 2px;border-radius:12px;background:transparent}.section-accordion{display:block}.section-card{margin:6px 0 8px;border-radius:12px;border:1px solid var(--md-sys-color-outline-variant);overflow:hidden}.section-card.mat-expanded{border-color:var(--md-sys-color-primary);box-shadow:0 2px 10px #0000000d}.section-card .mat-expansion-panel-body{background:var(--md-sys-color-surface-container-low)}.section-header{background:var(--md-sys-color-surface-container-low);border-bottom:1px solid var(--md-sys-color-outline-variant);padding:2px 8px;min-height:32px;display:flex;align-items:center;color:var(--md-sys-color-on-surface)}.section-header .mat-expansion-panel-header-title,.section-header .mat-expansion-panel-header-description{display:flex;align-items:center;color:var(--md-sys-color-on-surface)}.section-title{display:inline-flex;align-items:center;gap:6px;font-weight:600;letter-spacing:.2px;color:var(--md-sys-color-on-surface);font-size:.9rem;line-height:1.2}.section-icon{color:var( --md-sys-color-primary, var(--md-sys-color-primary, #3f51b5) );opacity:.9}.editor-row{display:flex;gap:12px;align-items:flex-start;flex-wrap:wrap;margin-bottom:8px}.editor-row>praxis-field-shell{flex:1 1 100%;min-width:280px}.editor-row praxis-field-shell .mat-mdc-form-field{width:100%;margin-bottom:8px}.editor-row.row-inline>praxis-field-shell{flex:0 0 auto}.editor-row.row-2>praxis-field-shell{flex:1 1 calc(50% - 12px);min-width:260px}.editor-row.row-3>praxis-field-shell{flex:1 1 calc(33.333% - 12px);min-width:220px}.editor-row.row-gradient-stops>praxis-field-shell{flex:1 1 100%;min-width:0;width:100%}.editor-row.row-gradient-stops .editor-item>pdx-color-input{flex:1 1 auto;min-width:0}.gradient-stops-preview{display:flex;flex:1 1 100%;align-items:center;justify-content:space-between;gap:16px;min-width:0;padding:14px 16px;border:1px solid var(--md-sys-color-outline-variant);border-radius:12px;background:var(--md-sys-color-surface-container);color:var(--md-sys-color-on-surface)}.gradient-stops-preview__copy{display:grid;gap:3px;min-width:0}.gradient-stops-preview__copy strong{font:var(--md-sys-typescale-label-large-font, inherit);font-weight:650}.gradient-stops-preview__copy span{color:var(--md-sys-color-on-surface-variant);font:var(--md-sys-typescale-body-small-font, inherit);line-height:1.35}.gradient-stops-preview__stars{flex:0 0 auto;color:transparent;background-clip:text;-webkit-background-clip:text;font-size:30px;line-height:1;letter-spacing:2px}.gradient-stops-preview__radial{position:relative;flex:0 0 auto;width:76px;height:76px;border-radius:50%;box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--md-sys-color-outline-variant) 70%,transparent)}.gradient-stops-preview__radial:before,.gradient-stops-preview__radial:after,.gradient-stops-preview__radial>span{content:\"\";position:absolute;inset:12px;border:1px dashed color-mix(in srgb,var(--md-sys-color-on-surface) 35%,transparent);border-radius:50%}.gradient-stops-preview__radial:after{inset:25px}.gradient-stops-preview__radial>span{inset:35px;border:0;background:var(--md-sys-color-on-surface)}@media(forced-colors:active){.gradient-stops-preview{border-color:CanvasText}.gradient-stops-preview__stars,.gradient-stops-preview__radial{color:CanvasText;background:none}}.editor-row>praxis-field-shell[data-field-name=controlType],.editor-row>praxis-field-shell[data-field-name=label],.editor-row>praxis-field-shell[data-field-name=placeholder]{flex:1 1 calc(50% - 12px);min-width:300px}.editor-row>praxis-presentation-rules-builder.presentation-rules-field{flex:1 1 100%;min-width:0}.editor-row>praxis-color-palette-editor.color-palette-field{flex:1 1 100%;min-width:0}.editor-row>praxis-presentation-choice-control.presentation-choice-field{flex:1 1 100%;min-width:0}praxis-field-shell[data-field-name=label] .mdc-floating-label,praxis-field-shell[data-field-name=placeholder] .mdc-floating-label{font-weight:600}.editor-row>praxis-field-shell[data-field-name=prefix],.editor-row>praxis-field-shell[data-field-name=suffix]{flex:0 0 200px;min-width:160px}.editor-row>praxis-field-shell[data-field-name=prefixIcon],.editor-row>praxis-field-shell[data-field-name=suffixIcon],.editor-row>praxis-field-shell[data-field-name=prefixIconColor],.editor-row>praxis-field-shell[data-field-name=suffixIconColor]{flex:1 1 auto;min-width:160px}.editor-item{display:flex;align-items:flex-start;gap:4px;min-width:0;width:100%}.editor-item>ng-container,.editor-item>praxis-field-shell{flex:1;min-width:0}.actions-container{display:flex;align-items:center;gap:0;margin-top:12px;flex-shrink:0}.decorator-btn{position:static;color:var(--md-sys-color-primary, #3f51b5);opacity:.6;transform:scale(.8);width:32px;height:32px;line-height:32px;padding:0}.decorator-btn:hover{opacity:1}.icon-picker-btn,.hint-btn,.hint-btn.has-icon-picker{right:auto}.editor-row .mat-mdc-form-field-subscript-wrapper mat-hint{display:none!important}.editor-row .mat-mdc-form-field-subscript-wrapper mat-error{display:block!important}praxis-field-shell[data-field-name=showCharCount] .mdc-switch{transform:scale(1.05)}praxis-field-shell[data-field-name=showCharCount] .mat-mdc-slide-toggle{font-weight:600}praxis-field-shell[data-field-name=controlType] .mat-mdc-form-field-hint{display:flex;align-items:center;gap:8px;background:var(--md-sys-color-tertiary-container);border:1px solid var(--md-sys-color-tertiary);color:var(--md-sys-color-on-tertiary-container);padding:8px 10px;border-radius:6px;line-height:1.3;white-space:normal}praxis-field-shell[data-field-name=controlType] .mat-mdc-form-field-hint:before{content:\"\\26a0\\fe0f\";display:inline-block}praxis-field-shell[data-field-name=controlType] .mat-mdc-form-field-subscript-wrapper{margin-top:6px;margin-bottom:10px;min-height:auto}praxis-field-shell[data-field-name=controlType] .mat-mdc-form-field{margin-bottom:16px}@media(max-width:520px){.editor-row,.editor-row.row-inline{gap:8px}.editor-item{flex-direction:column;gap:2px}.editor-row.row-inline>praxis-field-shell,.editor-row.row-2>praxis-field-shell,.editor-row.row-3>praxis-field-shell{flex:1 1 100%;min-width:0}.gradient-stops-preview{align-items:flex-start;flex-direction:column}.gradient-stops-preview__stars{font-size:27px}.actions-container{width:100%;justify-content:flex-end;margin-top:0}.editor-row.row-gradient-stops .editor-item{align-items:flex-start;flex-direction:row;gap:4px}.editor-row.row-gradient-stops .actions-container{width:auto;margin-top:12px}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i3$2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i3$2.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: DynamicFieldLoaderDirective, selector: "[dynamicFieldLoader]", inputs: ["fields", "formGroup", "enableExternalControlBinding", "itemTemplate", "debugTrace", "debugTraceLabel", "readonlyMode", "disabledMode", "presentationMode", "visible", "canvasMode"], outputs: ["componentsCreated", "fieldCreated", "fieldDestroyed", "renderError", "canvasMouseEnter", "canvasMouseLeave", "canvasClick", "canvasFocus"] }, { kind: "ngmodule", type: MatExpansionModule }, { kind: "directive", type: i5$1.MatAccordion, selector: "mat-accordion", inputs: ["hideToggle", "displayMode", "togglePosition"], exportAs: ["matAccordion"] }, { kind: "component", type: i5$1.MatExpansionPanel, selector: "mat-expansion-panel", inputs: ["hideToggle", "togglePosition"], outputs: ["afterExpand", "afterCollapse"], exportAs: ["matExpansionPanel"] }, { kind: "component", type: i5$1.MatExpansionPanelHeader, selector: "mat-expansion-panel-header", inputs: ["expandedHeight", "collapsedHeight", "tabIndex"] }, { kind: "directive", type: i5$1.MatExpansionPanelTitle, selector: "mat-panel-title" }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i2$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i1$2.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatDialogModule }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i3$1.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "directive", type: PraxisIconDirective, selector: "mat-icon[praxisIcon]", inputs: ["praxisIcon"] }, { kind: "component", type: PresentationRulesBuilderComponent, selector: "praxis-presentation-rules-builder" }, { kind: "component", type: PresentationChoiceControlComponent, selector: "praxis-presentation-choice-control", inputs: ["field"] }, { kind: "component", type: ColorPaletteEditorComponent, selector: "praxis-color-palette-editor", inputs: ["formControl"] }], encapsulation: i0.ViewEncapsulation.None });
2878
2904
  }
2879
2905
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: DynamicEditorRendererComponent, decorators: [{
2880
2906
  type: Component,
@@ -3041,7 +3067,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
3041
3067
  }
3042
3068
  </mat-accordion>
3043
3069
  </div>
3044
- `, styles: [":host{display:block}.editor-surface{padding:8px 0 2px;border-radius:12px;background:transparent}.section-accordion{display:block}.section-card{margin:6px 0 8px;border-radius:12px;border:1px solid var(--md-sys-color-outline-variant);overflow:hidden}.section-card.mat-expanded{border-color:var(--md-sys-color-primary);box-shadow:0 2px 10px #0000000d}.section-card .mat-expansion-panel-body{background:var(--md-sys-color-surface-container-low)}.section-header{background:var(--md-sys-color-surface-container-low);border-bottom:1px solid var(--md-sys-color-outline-variant, var(--sicoob-stroke-medium));padding:2px 8px;min-height:32px;display:flex;align-items:center;color:var(--md-sys-color-on-surface)}.section-header .mat-expansion-panel-header-title,.section-header .mat-expansion-panel-header-description{display:flex;align-items:center;color:var(--md-sys-color-on-surface)}.section-title{display:inline-flex;align-items:center;gap:6px;font-weight:600;letter-spacing:.2px;color:var(--md-sys-color-on-surface);font-size:.9rem;line-height:1.2}.section-icon{color:var( --md-sys-color-primary, var(--md-sys-color-primary, #3f51b5) );opacity:.9}.editor-row{display:flex;gap:12px;align-items:flex-start;flex-wrap:wrap;margin-bottom:8px}.editor-row>praxis-field-shell{flex:1 1 100%;min-width:280px}.editor-row praxis-field-shell .mat-mdc-form-field{width:100%;margin-bottom:8px}.editor-row.row-inline>praxis-field-shell{flex:0 0 auto}.editor-row.row-2>praxis-field-shell{flex:1 1 calc(50% - 12px);min-width:260px}.editor-row.row-3>praxis-field-shell{flex:1 1 calc(33.333% - 12px);min-width:220px}.editor-row.row-gradient-stops>praxis-field-shell{flex:1 1 100%;min-width:0;width:100%}.editor-row.row-gradient-stops .editor-item>pdx-color-input{flex:1 1 auto;min-width:0}.gradient-stops-preview{display:flex;flex:1 1 100%;align-items:center;justify-content:space-between;gap:16px;min-width:0;padding:14px 16px;border:1px solid var(--md-sys-color-outline-variant);border-radius:12px;background:var(--md-sys-color-surface-container);color:var(--md-sys-color-on-surface)}.gradient-stops-preview__copy{display:grid;gap:3px;min-width:0}.gradient-stops-preview__copy strong{font:var(--md-sys-typescale-label-large-font, inherit);font-weight:650}.gradient-stops-preview__copy span{color:var(--md-sys-color-on-surface-variant);font:var(--md-sys-typescale-body-small-font, inherit);line-height:1.35}.gradient-stops-preview__stars{flex:0 0 auto;color:transparent;background-clip:text;-webkit-background-clip:text;font-size:30px;line-height:1;letter-spacing:2px}.gradient-stops-preview__radial{position:relative;flex:0 0 auto;width:76px;height:76px;border-radius:50%;box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--md-sys-color-outline-variant) 70%,transparent)}.gradient-stops-preview__radial:before,.gradient-stops-preview__radial:after,.gradient-stops-preview__radial>span{content:\"\";position:absolute;inset:12px;border:1px dashed color-mix(in srgb,var(--md-sys-color-on-surface) 35%,transparent);border-radius:50%}.gradient-stops-preview__radial:after{inset:25px}.gradient-stops-preview__radial>span{inset:35px;border:0;background:var(--md-sys-color-on-surface)}@media(forced-colors:active){.gradient-stops-preview{border-color:CanvasText}.gradient-stops-preview__stars,.gradient-stops-preview__radial{color:CanvasText;background:none}}.editor-row>praxis-field-shell[data-field-name=controlType],.editor-row>praxis-field-shell[data-field-name=label],.editor-row>praxis-field-shell[data-field-name=placeholder]{flex:1 1 calc(50% - 12px);min-width:300px}.editor-row>praxis-presentation-rules-builder.presentation-rules-field{flex:1 1 100%;min-width:0}.editor-row>praxis-color-palette-editor.color-palette-field{flex:1 1 100%;min-width:0}.editor-row>praxis-presentation-choice-control.presentation-choice-field{flex:1 1 100%;min-width:0}praxis-field-shell[data-field-name=label] .mdc-floating-label,praxis-field-shell[data-field-name=placeholder] .mdc-floating-label{font-weight:600}.editor-row>praxis-field-shell[data-field-name=prefix],.editor-row>praxis-field-shell[data-field-name=suffix]{flex:0 0 200px;min-width:160px}.editor-row>praxis-field-shell[data-field-name=prefixIcon],.editor-row>praxis-field-shell[data-field-name=suffixIcon],.editor-row>praxis-field-shell[data-field-name=prefixIconColor],.editor-row>praxis-field-shell[data-field-name=suffixIconColor]{flex:1 1 auto;min-width:160px}.editor-item{display:flex;align-items:flex-start;gap:4px;min-width:0;width:100%}.editor-item>ng-container,.editor-item>praxis-field-shell{flex:1;min-width:0}.actions-container{display:flex;align-items:center;gap:0;margin-top:12px;flex-shrink:0}.decorator-btn{position:static;color:var(--md-sys-color-primary, #3f51b5);opacity:.6;transform:scale(.8);width:32px;height:32px;line-height:32px;padding:0}.decorator-btn:hover{opacity:1}.icon-picker-btn,.hint-btn,.hint-btn.has-icon-picker{right:auto}.editor-row .mat-mdc-form-field-subscript-wrapper mat-hint{display:none!important}.editor-row .mat-mdc-form-field-subscript-wrapper mat-error{display:block!important}praxis-field-shell[data-field-name=showCharCount] .mdc-switch{transform:scale(1.05)}praxis-field-shell[data-field-name=showCharCount] .mat-mdc-slide-toggle{font-weight:600}praxis-field-shell[data-field-name=controlType] .mat-mdc-form-field-hint{display:flex;align-items:center;gap:8px;background:var(--md-sys-color-tertiary-container);border:1px solid var(--md-sys-color-tertiary);color:var(--md-sys-color-on-tertiary-container);padding:8px 10px;border-radius:6px;line-height:1.3;white-space:normal}praxis-field-shell[data-field-name=controlType] .mat-mdc-form-field-hint:before{content:\"\\26a0\\fe0f\";display:inline-block}praxis-field-shell[data-field-name=controlType] .mat-mdc-form-field-subscript-wrapper{margin-top:6px;margin-bottom:10px;min-height:auto}praxis-field-shell[data-field-name=controlType] .mat-mdc-form-field{margin-bottom:16px}@media(max-width:520px){.editor-row,.editor-row.row-inline{gap:8px}.editor-item{flex-direction:column;gap:2px}.editor-row.row-inline>praxis-field-shell,.editor-row.row-2>praxis-field-shell,.editor-row.row-3>praxis-field-shell{flex:1 1 100%;min-width:0}.gradient-stops-preview{align-items:flex-start;flex-direction:column}.gradient-stops-preview__stars{font-size:27px}.actions-container{width:100%;justify-content:flex-end;margin-top:0}.editor-row.row-gradient-stops .editor-item{align-items:flex-start;flex-direction:row;gap:4px}.editor-row.row-gradient-stops .actions-container{width:auto;margin-top:12px}}\n"] }]
3070
+ `, styles: [":host{display:block}.editor-surface{padding:8px 0 2px;border-radius:12px;background:transparent}.section-accordion{display:block}.section-card{margin:6px 0 8px;border-radius:12px;border:1px solid var(--md-sys-color-outline-variant);overflow:hidden}.section-card.mat-expanded{border-color:var(--md-sys-color-primary);box-shadow:0 2px 10px #0000000d}.section-card .mat-expansion-panel-body{background:var(--md-sys-color-surface-container-low)}.section-header{background:var(--md-sys-color-surface-container-low);border-bottom:1px solid var(--md-sys-color-outline-variant);padding:2px 8px;min-height:32px;display:flex;align-items:center;color:var(--md-sys-color-on-surface)}.section-header .mat-expansion-panel-header-title,.section-header .mat-expansion-panel-header-description{display:flex;align-items:center;color:var(--md-sys-color-on-surface)}.section-title{display:inline-flex;align-items:center;gap:6px;font-weight:600;letter-spacing:.2px;color:var(--md-sys-color-on-surface);font-size:.9rem;line-height:1.2}.section-icon{color:var( --md-sys-color-primary, var(--md-sys-color-primary, #3f51b5) );opacity:.9}.editor-row{display:flex;gap:12px;align-items:flex-start;flex-wrap:wrap;margin-bottom:8px}.editor-row>praxis-field-shell{flex:1 1 100%;min-width:280px}.editor-row praxis-field-shell .mat-mdc-form-field{width:100%;margin-bottom:8px}.editor-row.row-inline>praxis-field-shell{flex:0 0 auto}.editor-row.row-2>praxis-field-shell{flex:1 1 calc(50% - 12px);min-width:260px}.editor-row.row-3>praxis-field-shell{flex:1 1 calc(33.333% - 12px);min-width:220px}.editor-row.row-gradient-stops>praxis-field-shell{flex:1 1 100%;min-width:0;width:100%}.editor-row.row-gradient-stops .editor-item>pdx-color-input{flex:1 1 auto;min-width:0}.gradient-stops-preview{display:flex;flex:1 1 100%;align-items:center;justify-content:space-between;gap:16px;min-width:0;padding:14px 16px;border:1px solid var(--md-sys-color-outline-variant);border-radius:12px;background:var(--md-sys-color-surface-container);color:var(--md-sys-color-on-surface)}.gradient-stops-preview__copy{display:grid;gap:3px;min-width:0}.gradient-stops-preview__copy strong{font:var(--md-sys-typescale-label-large-font, inherit);font-weight:650}.gradient-stops-preview__copy span{color:var(--md-sys-color-on-surface-variant);font:var(--md-sys-typescale-body-small-font, inherit);line-height:1.35}.gradient-stops-preview__stars{flex:0 0 auto;color:transparent;background-clip:text;-webkit-background-clip:text;font-size:30px;line-height:1;letter-spacing:2px}.gradient-stops-preview__radial{position:relative;flex:0 0 auto;width:76px;height:76px;border-radius:50%;box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--md-sys-color-outline-variant) 70%,transparent)}.gradient-stops-preview__radial:before,.gradient-stops-preview__radial:after,.gradient-stops-preview__radial>span{content:\"\";position:absolute;inset:12px;border:1px dashed color-mix(in srgb,var(--md-sys-color-on-surface) 35%,transparent);border-radius:50%}.gradient-stops-preview__radial:after{inset:25px}.gradient-stops-preview__radial>span{inset:35px;border:0;background:var(--md-sys-color-on-surface)}@media(forced-colors:active){.gradient-stops-preview{border-color:CanvasText}.gradient-stops-preview__stars,.gradient-stops-preview__radial{color:CanvasText;background:none}}.editor-row>praxis-field-shell[data-field-name=controlType],.editor-row>praxis-field-shell[data-field-name=label],.editor-row>praxis-field-shell[data-field-name=placeholder]{flex:1 1 calc(50% - 12px);min-width:300px}.editor-row>praxis-presentation-rules-builder.presentation-rules-field{flex:1 1 100%;min-width:0}.editor-row>praxis-color-palette-editor.color-palette-field{flex:1 1 100%;min-width:0}.editor-row>praxis-presentation-choice-control.presentation-choice-field{flex:1 1 100%;min-width:0}praxis-field-shell[data-field-name=label] .mdc-floating-label,praxis-field-shell[data-field-name=placeholder] .mdc-floating-label{font-weight:600}.editor-row>praxis-field-shell[data-field-name=prefix],.editor-row>praxis-field-shell[data-field-name=suffix]{flex:0 0 200px;min-width:160px}.editor-row>praxis-field-shell[data-field-name=prefixIcon],.editor-row>praxis-field-shell[data-field-name=suffixIcon],.editor-row>praxis-field-shell[data-field-name=prefixIconColor],.editor-row>praxis-field-shell[data-field-name=suffixIconColor]{flex:1 1 auto;min-width:160px}.editor-item{display:flex;align-items:flex-start;gap:4px;min-width:0;width:100%}.editor-item>ng-container,.editor-item>praxis-field-shell{flex:1;min-width:0}.actions-container{display:flex;align-items:center;gap:0;margin-top:12px;flex-shrink:0}.decorator-btn{position:static;color:var(--md-sys-color-primary, #3f51b5);opacity:.6;transform:scale(.8);width:32px;height:32px;line-height:32px;padding:0}.decorator-btn:hover{opacity:1}.icon-picker-btn,.hint-btn,.hint-btn.has-icon-picker{right:auto}.editor-row .mat-mdc-form-field-subscript-wrapper mat-hint{display:none!important}.editor-row .mat-mdc-form-field-subscript-wrapper mat-error{display:block!important}praxis-field-shell[data-field-name=showCharCount] .mdc-switch{transform:scale(1.05)}praxis-field-shell[data-field-name=showCharCount] .mat-mdc-slide-toggle{font-weight:600}praxis-field-shell[data-field-name=controlType] .mat-mdc-form-field-hint{display:flex;align-items:center;gap:8px;background:var(--md-sys-color-tertiary-container);border:1px solid var(--md-sys-color-tertiary);color:var(--md-sys-color-on-tertiary-container);padding:8px 10px;border-radius:6px;line-height:1.3;white-space:normal}praxis-field-shell[data-field-name=controlType] .mat-mdc-form-field-hint:before{content:\"\\26a0\\fe0f\";display:inline-block}praxis-field-shell[data-field-name=controlType] .mat-mdc-form-field-subscript-wrapper{margin-top:6px;margin-bottom:10px;min-height:auto}praxis-field-shell[data-field-name=controlType] .mat-mdc-form-field{margin-bottom:16px}@media(max-width:520px){.editor-row,.editor-row.row-inline{gap:8px}.editor-item{flex-direction:column;gap:2px}.editor-row.row-inline>praxis-field-shell,.editor-row.row-2>praxis-field-shell,.editor-row.row-3>praxis-field-shell{flex:1 1 100%;min-width:0}.gradient-stops-preview{align-items:flex-start;flex-direction:column}.gradient-stops-preview__stars{font-size:27px}.actions-container{width:100%;justify-content:flex-end;margin-top:0}.editor-row.row-gradient-stops .editor-item{align-items:flex-start;flex-direction:row;gap:4px}.editor-row.row-gradient-stops .actions-container{width:auto;margin-top:12px}}\n"] }]
3045
3071
  }], ctorParameters: () => [{ type: i1$1.MatDialog }, { type: i2$2.IconPickerService }, { type: i2$2.PraxisI18nService, decorators: [{
3046
3072
  type: Optional
3047
3073
  }] }], propDecorators: { properties: [{
@@ -4222,6 +4248,20 @@ const selectProperties = [
4222
4248
  { value: 'danger', text: 'Danger' },
4223
4249
  ],
4224
4250
  },
4251
+ {
4252
+ name: 'actions.showQuickView',
4253
+ label: 'Quick View: exibir botão',
4254
+ editorType: 'checkbox',
4255
+ group: 'Ações',
4256
+ hint: 'Controla o botão de visualização rápida quando um único item está selecionado e há vínculo governado (resourceReference / optionSource.detail).',
4257
+ },
4258
+ {
4259
+ name: 'quickViewActionLabel',
4260
+ label: 'Quick View: label da ação',
4261
+ editorType: 'text',
4262
+ group: 'Ações',
4263
+ hint: 'Rótulo acessível customizado para o botão de visualização rápida do item selecionado.',
4264
+ },
4225
4265
  // 7) Aparência
4226
4266
  {
4227
4267
  name: 'materialDesign.appearance',
@@ -4482,7 +4522,7 @@ const entityLookupProperties = [
4482
4522
  { value: 'compact', text: 'compact' },
4483
4523
  { value: 'token', text: 'token' },
4484
4524
  ],
4485
- hint: 'Layout preferencial do valor selecionado publicado pela option-source.',
4525
+ hint: 'Layout preferencial do valor selecionado publicado pela option-source (default compact/inline em formularios; card para revisoes detalhadas).',
4486
4526
  },
4487
4527
  {
4488
4528
  name: 'optionSource.display.resultLayout',
@@ -4595,6 +4635,13 @@ const entityLookupProperties = [
4595
4635
  group: 'Display canonico',
4596
4636
  hint: 'Limite preferencial de badges antes de overflow.',
4597
4637
  },
4638
+ {
4639
+ name: 'optionSource.display.actions.showQuickView',
4640
+ label: 'Display: acao visualizacao rapida',
4641
+ editorType: 'checkbox',
4642
+ group: 'Display canonico',
4643
+ hint: 'Exibe ou oculta o gatilho de Quick View na projecao canonica de display do optionSource.',
4644
+ },
4598
4645
  {
4599
4646
  name: 'selectedLayout',
4600
4647
  label: 'Layout selecionado',
@@ -4606,7 +4653,7 @@ const entityLookupProperties = [
4606
4653
  { value: 'compact', text: 'compact' },
4607
4654
  { value: 'token', text: 'token' },
4608
4655
  ],
4609
- hint: 'Layout do item selecionado no trigger ou card principal.',
4656
+ hint: 'Layout do item selecionado no trigger (default compact/inline; card opt-in para superficie expandida).',
4610
4657
  },
4611
4658
  {
4612
4659
  name: 'resultLayout',
@@ -4923,6 +4970,13 @@ const entityLookupProperties = [
4923
4970
  ],
4924
4971
  hint: 'Use surface como contrato canonico de plataforma; route/href ficam como fallback legado.',
4925
4972
  },
4973
+ {
4974
+ name: 'optionSource.detail.resourceKey',
4975
+ label: 'ResourceKey de detalhe',
4976
+ editorType: 'text',
4977
+ group: 'Navegacao',
4978
+ hint: 'Chave canonica do recurso destino publicada pelo backend (ex.: human-resources.departamentos).',
4979
+ },
4926
4980
  {
4927
4981
  name: 'optionSource.detail.surfaceId',
4928
4982
  label: 'Surface de detalhe',
@@ -4930,6 +4984,13 @@ const entityLookupProperties = [
4930
4984
  group: 'Navegacao',
4931
4985
  hint: 'Id da surface item-level preferida. Ex.: view, detail.',
4932
4986
  },
4987
+ {
4988
+ name: 'optionSource.detail.previewSurfaceId',
4989
+ label: 'Surface de preview (Quick View)',
4990
+ editorType: 'text',
4991
+ group: 'Navegacao',
4992
+ hint: 'Surface read-only opcional dedicada ao card de Quick View; quando ausente, usa surfaceId.',
4993
+ },
4933
4994
  {
4934
4995
  name: 'optionSource.detail.presentation',
4935
4996
  label: 'Apresentacao da surface',
@@ -5060,6 +5121,13 @@ const entityLookupProperties = [
5060
5121
  editorType: 'text',
5061
5122
  group: 'Acoes do lookup',
5062
5123
  },
5124
+ {
5125
+ name: 'quickViewActionLabel',
5126
+ label: 'Label visualizacao rapida',
5127
+ editorType: 'text',
5128
+ group: 'Acoes do lookup',
5129
+ hint: 'Rotulo customizado para o botao/acao de Quick View do registro selecionado.',
5130
+ },
5063
5131
  {
5064
5132
  name: 'changeActionLabel',
5065
5133
  label: 'Label trocar',
@@ -5096,12 +5164,19 @@ const entityLookupProperties = [
5096
5164
  editorType: 'checkbox',
5097
5165
  group: 'Acoes do lookup',
5098
5166
  },
5167
+ {
5168
+ name: 'actions.showQuickView',
5169
+ label: 'Acao: visualizacao rapida',
5170
+ editorType: 'checkbox',
5171
+ group: 'Acoes do lookup',
5172
+ hint: 'Exibe ou oculta o gatilho de Quick View quando houver target governado (resourceReference ou optionSource.detail).',
5173
+ },
5099
5174
  {
5100
5175
  name: 'actions.showChange',
5101
5176
  label: 'Acao: trocar',
5102
5177
  editorType: 'checkbox',
5103
5178
  group: 'Acoes do lookup',
5104
- hint: 'Override explicito. Sem configuracao, formularios completos exibem a acao e controles inline usam o proprio trigger para trocar a entidade.',
5179
+ hint: 'Override explicito. Sem configuracao, apenas selectedLayout="card" exibe a acao Trocar; nos modos compact/inline/token e inlineEntityLookup o proprio trigger troca a entidade.',
5105
5180
  },
5106
5181
  {
5107
5182
  name: 'actions.showCopyCode',
@@ -5546,6 +5621,46 @@ const numberProperties = [
5546
5621
  { name: 'step', label: 'Step (incremento)', editorType: 'number', group: 'Formato/Comportamento', hint: 'Incremento em unidades; combine com min/max (ex.: 0.01).' },
5547
5622
  { name: 'numberFormat.decimalPlaces', label: 'Casas decimais', editorType: 'number', group: 'Formato/Comportamento', row: 'formato.decimais', inline: true, hint: 'Precisão exibida/aceita (ex.: 2).' },
5548
5623
  { 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.' },
5624
+ { 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.' },
5625
+ {
5626
+ name: 'numericFormat',
5627
+ label: 'Formato numérico canônico',
5628
+ editorType: 'select',
5629
+ group: 'Formato/Comportamento',
5630
+ options: [
5631
+ { value: 'decimal', text: 'Decimal' },
5632
+ { value: 'integer', text: 'Inteiro' },
5633
+ { value: 'currency', text: 'Moeda' },
5634
+ { value: 'percent', text: 'Percentual (Percent Studio)' },
5635
+ { value: 'scientific', text: 'Científico' },
5636
+ ],
5637
+ hint: 'Formato semântico do campo numérico vindo do contrato x-ui.',
5638
+ },
5639
+ {
5640
+ name: 'showPercentStudio',
5641
+ label: 'Exibir barra Percent Studio no formulário',
5642
+ editorType: 'checkbox',
5643
+ group: 'Formato/Comportamento',
5644
+ hint: 'Mostra o trilho interativo, presets de quartis e steppers rápidos abaixo do pdx-number-input quando em modo percentual.',
5645
+ },
5646
+ {
5647
+ name: 'inlineVisualStyle',
5648
+ label: 'Estilo visual inline (percentual)',
5649
+ editorType: 'select',
5650
+ group: 'Formato/Comportamento',
5651
+ options: [
5652
+ { value: 'compact', text: 'Compacto (Pílula líquida + popover)' },
5653
+ { value: 'graphic', text: 'Gráfico expandido (Card com donut + slider)' },
5654
+ ],
5655
+ hint: 'Define a apresentação de filtros numéricos percentuais em superfícies inline.',
5656
+ },
5657
+ {
5658
+ name: 'percentPresets',
5659
+ label: 'Presets rápidos de percentual',
5660
+ editorType: 'json',
5661
+ group: 'Formato/Comportamento',
5662
+ hint: 'Lista de atalhos rápidos no Percent Studio (ex.: [0, 25, 50, 75, 100] ou [{"label": "Meta", "value": 12.5}]).',
5663
+ },
5549
5664
  {
5550
5665
  name: 'inputMode',
5551
5666
  label: 'Modo de entrada',
@@ -5694,6 +5809,16 @@ const dateProperties = [
5694
5809
  { value: 'year', text: 'Ano' },
5695
5810
  { value: 'multi-year', text: 'Décadas' },
5696
5811
  ] },
5812
+ { name: 'defaultGranularity', label: 'Granularidade de seleção (inline)', editorType: 'select', group: 'Formato/Comportamento', options: [
5813
+ { value: 'day', text: 'Dia' },
5814
+ { value: 'month', text: 'Mês/Ano (1º dia do mês)' },
5815
+ { value: 'year', text: 'Ano (01/01 do ano)' },
5816
+ ], hint: 'No inlineDate, permite selecionar diretamente o ano ou mês/ano sem navegar pelos dias.' },
5817
+ { name: 'inlineChipDisplay', label: 'Exibição no chip (inline)', editorType: 'select', group: 'Formato/Comportamento', options: [
5818
+ { value: 'value', text: 'Apenas valor' },
5819
+ { value: 'label-value', text: 'Rótulo + valor (ex.: Data: Mai/2026)' },
5820
+ ], hint: 'Define como o valor preenchido é exibido na pílula fechada do inlineDate.' },
5821
+ { name: 'showShortcuts', label: 'Atalhos rápidos (inline)', editorType: 'checkbox', group: 'Formato/Comportamento', hint: 'Exibe barra de atalhos rápidos contextual (Hoje/Ontem, Este mês/Mês ant., Ano atual/Ano ant.) no inlineDate.' },
5697
5822
  { name: 'startAt', label: 'Abrir em', editorType: 'date', group: 'Formato/Comportamento' },
5698
5823
  { name: 'touchUi', label: 'Touch UI', editorType: 'checkbox', group: 'Formato/Comportamento', row: 'fmt.inline1', inline: true },
5699
5824
  { name: 'closeOnSelect', label: 'Fechar ao selecionar', editorType: 'checkbox', group: 'Formato/Comportamento', row: 'fmt.inline1', inline: true },
@@ -5823,6 +5948,19 @@ const dateRangeProperties = [
5823
5948
  { value: 'multi-year', text: 'Décadas' },
5824
5949
  ], hint: 'Visão mostrada ao abrir o calendário (padrão: mês).'
5825
5950
  },
5951
+ {
5952
+ name: 'defaultGranularity', label: 'Granularidade inicial (inline)', editorType: 'select', group: 'Formato/Comportamento', options: [
5953
+ { value: 'day', text: 'Dia (calendário)' },
5954
+ { value: 'month', text: 'Mês/Ano (competência)' },
5955
+ { value: 'year', text: 'Ano (exercício)' },
5956
+ ], hint: 'Define a aba inicial do seletor rápido de período no inlineDateRange (Dia, Mês/Ano ou Ano).'
5957
+ },
5958
+ {
5959
+ name: 'inlineChipDisplay', label: 'Exibição no chip (inline)', editorType: 'select', group: 'Formato/Comportamento', options: [
5960
+ { value: 'value', text: 'Apenas valor (ex.: Q2/2026)' },
5961
+ { value: 'label-value', text: 'Rótulo + valor (ex.: Período: Q2/2026)' },
5962
+ ], hint: 'Define como o período selecionado é exibido na pílula fechada do inlineDateRange.'
5963
+ },
5826
5964
  { name: 'startAt', label: 'Abrir em', editorType: 'date', group: 'Formato/Comportamento', hint: 'Data inicial do calendário (YYYY-MM-DD) quando não há valor selecionado.' },
5827
5965
  { name: 'touchUi', label: 'Touch UI', editorType: 'checkbox', group: 'Formato/Comportamento', row: 'fmt.inline1', inline: true, hint: 'Abre o datepicker em modo tela cheia para toque.' },
5828
5966
  // Validação
@@ -7695,6 +7833,7 @@ const rangeSliderProperties = [
7695
7833
  { value: 'compact', text: 'Compacto' },
7696
7834
  { value: 'storage', text: 'Armazenamento' },
7697
7835
  ], hint: 'Formatter declarativo para labels e aria-valuetext.' },
7836
+ { 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%).' },
7698
7837
  { name: 'showTicks', label: 'Ticks', editorType: 'select', group: 'Formato/Comportamento', options: [
7699
7838
  { value: false, text: 'Sem ticks' },
7700
7839
  { value: true, text: 'Com ticks' },
@@ -8684,6 +8823,58 @@ const fieldPresentationProperties = [
8684
8823
  hint: 'Builder guiado para regras Json Logic ordenadas que alteram somente a apresentação. Regras posteriores vencem.',
8685
8824
  hintKey: 'presentationFields.presentationRules.hint',
8686
8825
  },
8826
+ {
8827
+ name: 'quickViewVisible',
8828
+ label: 'Exibir no resumo Quick View',
8829
+ labelKey: 'presentationFields.quickViewVisible.label',
8830
+ editorType: 'checkbox',
8831
+ group: 'Apresentação',
8832
+ groupKey: 'presentationFields.group',
8833
+ hint: 'Controla x-ui.quickViewVisible quando este recurso é exibido em um card de Quick View.',
8834
+ hintKey: 'presentationFields.quickViewVisible.hint',
8835
+ },
8836
+ {
8837
+ name: 'quickViewOrder',
8838
+ label: 'Ordem no resumo Quick View',
8839
+ labelKey: 'presentationFields.quickViewOrder.label',
8840
+ editorType: 'number',
8841
+ group: 'Apresentação',
8842
+ groupKey: 'presentationFields.group',
8843
+ hint: 'Prioridade numérica (x-ui.quickViewOrder) deste campo na síntese do card Quick View.',
8844
+ hintKey: 'presentationFields.quickViewOrder.hint',
8845
+ },
8846
+ {
8847
+ name: 'quickView.previewSurfaceId',
8848
+ label: 'Quick View: surface de preview',
8849
+ labelKey: 'presentationFields.quickViewPreviewSurfaceId.label',
8850
+ editorType: 'text',
8851
+ group: 'Apresentação',
8852
+ groupKey: 'presentationFields.group',
8853
+ hint: 'Surface read-only opcional dedicada ao card de Quick View deste vínculo (preserva o recordOpen canônico do servidor).',
8854
+ hintKey: 'presentationFields.quickViewPreviewSurfaceId.hint',
8855
+ },
8856
+ {
8857
+ name: 'quickView.detailPresentation',
8858
+ label: 'Quick View: abertura de detalhe',
8859
+ labelKey: 'presentationFields.quickViewDetailPresentation.label',
8860
+ editorType: 'select',
8861
+ group: 'Apresentação',
8862
+ groupKey: 'presentationFields.group',
8863
+ options: [
8864
+ {
8865
+ value: 'drawer',
8866
+ text: 'Gaveta lateral (drawer)',
8867
+ textKey: 'presentationFields.quickViewDetailPresentation.option.drawer',
8868
+ },
8869
+ {
8870
+ value: 'modal',
8871
+ text: 'Modal (dialog)',
8872
+ textKey: 'presentationFields.quickViewDetailPresentation.option.modal',
8873
+ },
8874
+ ],
8875
+ hint: 'Define se o CTA primário do Quick View abre a superfície completa em drawer ou modal.',
8876
+ hintKey: 'presentationFields.quickViewDetailPresentation.hint',
8877
+ },
8687
8878
  ];
8688
8879
 
8689
8880
  /** Garante que minLength <= maxLength quando ambos definidos. */
@@ -10709,6 +10900,93 @@ function mergeEditorProperties(base, contextual) {
10709
10900
  }
10710
10901
  return merged;
10711
10902
  }
10903
+ const inlinePhoneEditorProperties = mergeEditorProperties(inputProperties, phoneInputProperties);
10904
+ const inlinePeriodRangeEditorProperties = mergeEditorProperties(rangeSliderProperties, [
10905
+ {
10906
+ name: 'granularity',
10907
+ label: 'Granularidade do período',
10908
+ editorType: 'select',
10909
+ group: 'Formato/Comportamento',
10910
+ options: [
10911
+ { value: 'month', text: 'Mês (1-12 ou YYYY-MM)' },
10912
+ { value: 'year', text: 'Ano (YYYY)' },
10913
+ ],
10914
+ hint: 'Define se o seletor opera em competência mensal ou exercício anual.',
10915
+ },
10916
+ {
10917
+ name: 'defaultGranularity',
10918
+ label: 'Granularidade inicial',
10919
+ editorType: 'select',
10920
+ group: 'Formato/Comportamento',
10921
+ options: [
10922
+ { value: 'month', text: 'Mês' },
10923
+ { value: 'year', text: 'Ano' },
10924
+ ],
10925
+ hint: 'Granularidade inicial aberta no painel.',
10926
+ },
10927
+ {
10928
+ name: 'selectionGrid',
10929
+ label: 'Exibir grade de seleção (Jan-Dez / Anos)',
10930
+ editorType: 'checkbox',
10931
+ group: 'Formato/Comportamento',
10932
+ hint: 'Mostra botões clicáveis de meses ou anos para seleção de único item ou faixa.',
10933
+ },
10934
+ {
10935
+ name: 'showSlider',
10936
+ label: 'Exibir slider complementar',
10937
+ editorType: 'checkbox',
10938
+ group: 'Formato/Comportamento',
10939
+ hint: 'Mostra barra deslizante sincronizada com a grade de meses/anos.',
10940
+ },
10941
+ {
10942
+ name: 'inlineChipDisplay',
10943
+ label: 'Exibição no chip (inline)',
10944
+ editorType: 'select',
10945
+ group: 'Formato/Comportamento',
10946
+ options: [
10947
+ { value: 'badge', text: 'Rótulo + badge (ex.: Mês: Jan – Jun)' },
10948
+ { value: 'value', text: 'Apenas valor (ex.: Jan – Jun)' },
10949
+ { value: 'label-value', text: 'Rótulo + valor' },
10950
+ ],
10951
+ hint: 'Define como o intervalo selecionado aparece na pílula fechada.',
10952
+ },
10953
+ {
10954
+ name: 'minYear',
10955
+ label: 'Ano mínimo',
10956
+ editorType: 'number',
10957
+ group: 'Validação',
10958
+ row: 'val.yearminmax',
10959
+ inline: true,
10960
+ hint: 'Ano inicial exibido na grade de anos (ex.: 2020).',
10961
+ },
10962
+ {
10963
+ name: 'maxYear',
10964
+ label: 'Ano máximo',
10965
+ editorType: 'number',
10966
+ group: 'Validação',
10967
+ row: 'val.yearminmax',
10968
+ inline: true,
10969
+ hint: 'Ano final exibido na grade de anos (ex.: 2030).',
10970
+ },
10971
+ {
10972
+ name: 'minPeriod',
10973
+ label: 'Período mínimo',
10974
+ editorType: 'text',
10975
+ group: 'Validação',
10976
+ row: 'val.periodminmax',
10977
+ inline: true,
10978
+ hint: 'Limite inferior de competência (ex.: 2020-01 ou 1).',
10979
+ },
10980
+ {
10981
+ name: 'maxPeriod',
10982
+ label: 'Período máximo',
10983
+ editorType: 'text',
10984
+ group: 'Validação',
10985
+ row: 'val.periodminmax',
10986
+ inline: true,
10987
+ hint: 'Limite superior de competência (ex.: 2027-12 ou 12).',
10988
+ },
10989
+ ]);
10712
10990
  function hasCanonicalDocumentSemantics(metadata) {
10713
10991
  if (!metadata || typeof metadata !== 'object' || Array.isArray(metadata)) {
10714
10992
  return false;
@@ -10724,12 +11002,12 @@ const INLINE_FILTER_EDITOR_PROPERTIES_BY_CONTROL_TYPE = Object.freeze({
10724
11002
  [INLINE_FILTER_CONTROL_TYPES.AUTOCOMPLETE]: selectProperties,
10725
11003
  [INLINE_FILTER_CONTROL_TYPES.MULTI_SELECT]: selectProperties,
10726
11004
  [INLINE_FILTER_CONTROL_TYPES.INPUT]: inputProperties,
10727
- [INLINE_FILTER_CONTROL_TYPES.PHONE]: inputProperties,
11005
+ [INLINE_FILTER_CONTROL_TYPES.PHONE]: inlinePhoneEditorProperties,
10728
11006
  [INLINE_FILTER_CONTROL_TYPES.TOGGLE]: toggleProperties,
10729
11007
  [INLINE_FILTER_CONTROL_TYPES.RANGE]: rangeSliderProperties,
10730
- [INLINE_FILTER_CONTROL_TYPES.PERIOD_RANGE]: rangeSliderProperties,
10731
- [INLINE_FILTER_CONTROL_TYPES.YEAR_RANGE]: rangeSliderProperties,
10732
- [INLINE_FILTER_CONTROL_TYPES.MONTH_RANGE]: rangeSliderProperties,
11008
+ [INLINE_FILTER_CONTROL_TYPES.PERIOD_RANGE]: inlinePeriodRangeEditorProperties,
11009
+ [INLINE_FILTER_CONTROL_TYPES.YEAR_RANGE]: inlinePeriodRangeEditorProperties,
11010
+ [INLINE_FILTER_CONTROL_TYPES.MONTH_RANGE]: inlinePeriodRangeEditorProperties,
10733
11011
  [INLINE_FILTER_CONTROL_TYPES.DATE]: dateProperties,
10734
11012
  [INLINE_FILTER_CONTROL_TYPES.DATE_RANGE]: dateRangeProperties,
10735
11013
  [INLINE_FILTER_CONTROL_TYPES.TIME]: timePickerProperties,
@@ -10771,7 +11049,11 @@ class FieldMetadataEditorComponent {
10771
11049
  normalizedProps = [];
10772
11050
  renderedProps = [];
10773
11051
  initialSnapshot = null;
11052
+ initialControlType;
11053
+ initialSeed;
10774
11054
  baselineReady = false;
11055
+ formRebuild$ = new Subject();
11056
+ hostApplyPatch;
10775
11057
  // Lista de tipos suportados no editor (curada para os tipos com config disponível)
10776
11058
  typeOptions = [
10777
11059
  { value: FieldControlType.INPUT, label: 'Texto (Input)' },
@@ -10824,6 +11106,9 @@ class FieldMetadataEditorComponent {
10824
11106
  isValid$ = new BehaviorSubject(true);
10825
11107
  isBusy$ = new BehaviorSubject(false);
10826
11108
  destroyRef = inject(DestroyRef);
11109
+ resourceDiscovery = inject(ResourceDiscoveryService, { optional: true });
11110
+ i18n = inject(PraxisI18nService, { optional: true });
11111
+ discoveredResourceOptions = [];
10827
11112
  lastCurrencyPreset = null;
10828
11113
  constructor(registry, normalizer, factory, ctxValidators) {
10829
11114
  this.registry = registry;
@@ -10838,10 +11123,7 @@ class FieldMetadataEditorComponent {
10838
11123
  if (data.seed)
10839
11124
  this.seed = data.seed;
10840
11125
  // Bridge: allow host to receive patches even if saved$/applied$ are missed
10841
- try {
10842
- this.__hostApplyPatch = data.hostBridge?.applyPatch;
10843
- }
10844
- catch { }
11126
+ this.hostApplyPatch = data.hostBridge?.applyPatch;
10845
11127
  }
10846
11128
  // Normalize legacy avatar seed (root imageSrc/imageAlt -> extra.* for editor binding)
10847
11129
  try {
@@ -10863,14 +11145,214 @@ class FieldMetadataEditorComponent {
10863
11145
  }
10864
11146
  catch { }
10865
11147
  }
11148
+ catalogLookupRequested = false;
11149
+ resourceSchemaFieldsByPath = new Map();
11150
+ resourceSchemaRequestIdByScope = {
11151
+ root: 0,
11152
+ optionSource: 0,
11153
+ };
10866
11154
  ngOnInit() {
10867
- try {
10868
- const hasBridge = typeof this.__hostApplyPatch === 'function';
10869
- (console.log || console.debug)('[FieldMetadataEditor] ngOnInit', { controlType: this.controlType, hasHostBridge: hasBridge });
10870
- }
10871
- catch { }
10872
11155
  this.rebuildEditorForType(this.controlType);
10873
11156
  }
11157
+ loadCatalogResourceOptionsIfNeeded() {
11158
+ const hasResourcePathProp = this.normalizedProps.some((prop) => prop?.name === 'resourcePath' ||
11159
+ prop?.name === 'optionSource.resourcePath');
11160
+ if (!hasResourcePathProp) {
11161
+ return;
11162
+ }
11163
+ const optionSourceTypeCtrl = this.form?.get('optionSource.type');
11164
+ if (optionSourceTypeCtrl) {
11165
+ optionSourceTypeCtrl.valueChanges
11166
+ .pipe(takeUntil(this.formRebuild$), takeUntilDestroyed(this.destroyRef))
11167
+ .subscribe(() => {
11168
+ if (this.discoveredResourceOptions.length > 0 && this.normalizedProps.length > 0) {
11169
+ this.renderedProps = this.buildRenderedProps(this.normalizedProps);
11170
+ }
11171
+ });
11172
+ }
11173
+ this.bindResourcePathSchemaSync('resourcePath', 'root');
11174
+ this.bindResourcePathSchemaSync('optionSource.resourcePath', 'optionSource');
11175
+ if (this.catalogLookupRequested || !this.resourceDiscovery) {
11176
+ return;
11177
+ }
11178
+ this.catalogLookupRequested = true;
11179
+ const endpointKey = this.resolveDiscoveryEndpointKey();
11180
+ this.resourceDiscovery
11181
+ .getDiscoveredResources({
11182
+ intent: 'any',
11183
+ ...(endpointKey ? { endpointKey } : {}),
11184
+ })
11185
+ .pipe(catchError(() => of([])), takeUntilDestroyed(this.destroyRef))
11186
+ .subscribe((options) => {
11187
+ this.discoveredResourceOptions = options;
11188
+ this.seedCatalogResponseFieldsCache(options);
11189
+ if (options.length > 0 && this.normalizedProps.length > 0) {
11190
+ this.renderedProps = this.buildRenderedProps(this.normalizedProps);
11191
+ }
11192
+ });
11193
+ }
11194
+ resolveDiscoveryEndpointKey() {
11195
+ const raw = this.form?.get('endpointKey')?.value ??
11196
+ this.form?.get('apiEndpointKey')?.value ??
11197
+ this.seed?.endpointKey ??
11198
+ this.seed?.apiEndpointKey;
11199
+ const trimmed = String(raw ?? '').trim();
11200
+ return trimmed || undefined;
11201
+ }
11202
+ bindResourcePathSchemaSync(controlPath, scope) {
11203
+ const ctrl = this.form?.get(controlPath);
11204
+ if (!ctrl) {
11205
+ return;
11206
+ }
11207
+ let lastPath = String(ctrl.value ?? '').trim();
11208
+ if (lastPath) {
11209
+ this.resolveAndApplyResourceSchemaFields(lastPath, scope, false);
11210
+ }
11211
+ ctrl.valueChanges
11212
+ .pipe(takeUntil(this.formRebuild$), takeUntilDestroyed(this.destroyRef))
11213
+ .subscribe((nextValue) => {
11214
+ const nextPath = String(nextValue ?? '').trim();
11215
+ if (nextPath === lastPath) {
11216
+ return;
11217
+ }
11218
+ lastPath = nextPath;
11219
+ if (!nextPath) {
11220
+ if (this.normalizedProps.length > 0) {
11221
+ this.renderedProps = this.buildRenderedProps(this.normalizedProps);
11222
+ }
11223
+ return;
11224
+ }
11225
+ this.resolveAndApplyResourceSchemaFields(nextPath, scope, this.baselineReady);
11226
+ });
11227
+ }
11228
+ seedCatalogResponseFieldsCache(options) {
11229
+ for (const option of options) {
11230
+ if (!option?.responseFields?.length) {
11231
+ continue;
11232
+ }
11233
+ const mappedMetas = mapCatalogFieldsToMetadata(option.responseFields);
11234
+ const metaByName = new Map(mappedMetas.map((meta) => [meta.name, meta]));
11235
+ const definitions = option.responseFields
11236
+ .filter((field) => Boolean(field?.name && String(field.name).trim()))
11237
+ .map((field) => {
11238
+ const name = String(field.name).trim();
11239
+ const rawLabel = String(field.label ?? '').trim();
11240
+ const label = rawLabel && rawLabel !== name
11241
+ ? rawLabel
11242
+ : humanizeCatalogFieldName(name);
11243
+ const mapped = metaByName.get(name);
11244
+ return {
11245
+ name,
11246
+ label,
11247
+ type: String(field.type ?? field.controlType ?? 'string').trim() ||
11248
+ 'string',
11249
+ controlType: String(field.controlType ?? mapped?.controlType ?? '').trim() ||
11250
+ undefined,
11251
+ description: String(field.description ?? '').trim() || undefined,
11252
+ ...(typeof field.format === 'string' && field.format.trim()
11253
+ ? { format: field.format.trim() }
11254
+ : {}),
11255
+ ...(typeof field.required === 'boolean'
11256
+ ? { required: field.required }
11257
+ : {}),
11258
+ ...(typeof field.minimum === 'number'
11259
+ ? { minimum: field.minimum }
11260
+ : {}),
11261
+ ...(typeof field.maximum === 'number'
11262
+ ? { maximum: field.maximum }
11263
+ : {}),
11264
+ ...(typeof field.minLength === 'number'
11265
+ ? { minLength: field.minLength }
11266
+ : {}),
11267
+ ...(typeof field.maxLength === 'number'
11268
+ ? { maxLength: field.maxLength }
11269
+ : {}),
11270
+ ...(typeof field.pattern === 'string' && field.pattern.trim()
11271
+ ? { pattern: field.pattern.trim() }
11272
+ : {}),
11273
+ ...(Array.isArray(field.enumValues)
11274
+ ? { enumValues: field.enumValues }
11275
+ : {}),
11276
+ ...(mapped?.options?.length
11277
+ ? {
11278
+ options: mapped.options.map((opt) => {
11279
+ const optLabel = String(opt.label ?? opt.text ?? opt.value);
11280
+ return {
11281
+ key: String(opt.value),
11282
+ value: String(opt.value),
11283
+ label: optLabel,
11284
+ text: opt.text ?? optLabel,
11285
+ };
11286
+ }),
11287
+ }
11288
+ : {}),
11289
+ };
11290
+ });
11291
+ if (!definitions.length) {
11292
+ continue;
11293
+ }
11294
+ const candidateKeys = new Set();
11295
+ for (const candidate of [option.value, option.path, option.resourceKey]) {
11296
+ const raw = String(candidate ?? '').trim();
11297
+ if (!raw)
11298
+ continue;
11299
+ const normalized = this.normalizeResourceCacheKey(raw);
11300
+ if (normalized)
11301
+ candidateKeys.add(normalized);
11302
+ const stripped = raw.replace(/^\/+|\/+$/g, '');
11303
+ if (stripped)
11304
+ candidateKeys.add(stripped);
11305
+ const withoutApi = stripped.replace(/^api\/+/i, '');
11306
+ if (withoutApi)
11307
+ candidateKeys.add(withoutApi);
11308
+ }
11309
+ for (const cacheKey of candidateKeys) {
11310
+ if (!this.resourceSchemaFieldsByPath.has(cacheKey)) {
11311
+ this.resourceSchemaFieldsByPath.set(cacheKey, definitions);
11312
+ }
11313
+ }
11314
+ }
11315
+ }
11316
+ resolveAndApplyResourceSchemaFields(resourcePath, scope, autoSuggestDefaults) {
11317
+ const trimmed = String(resourcePath ?? '').trim();
11318
+ if (!trimmed) {
11319
+ return;
11320
+ }
11321
+ const cached = this.getCachedFieldsForResource(trimmed);
11322
+ if (cached.length > 0) {
11323
+ if (autoSuggestDefaults) {
11324
+ this.applyAutoSuggestedDependentKeys(scope, cached);
11325
+ }
11326
+ if (this.normalizedProps.length > 0) {
11327
+ this.renderedProps = this.buildRenderedProps(this.normalizedProps);
11328
+ }
11329
+ }
11330
+ if (typeof this.resourceDiscovery?.getResourceSchemaFields !== 'function') {
11331
+ return;
11332
+ }
11333
+ const requestId = ++this.resourceSchemaRequestIdByScope[scope];
11334
+ const endpointKey = this.resolveDiscoveryEndpointKey();
11335
+ const request$ = endpointKey
11336
+ ? this.resourceDiscovery.getResourceSchemaFields(trimmed, { endpointKey })
11337
+ : this.resourceDiscovery.getResourceSchemaFields(trimmed);
11338
+ request$
11339
+ .pipe(catchError(() => of([])), takeUntilDestroyed(this.destroyRef))
11340
+ .subscribe((fields) => {
11341
+ if (requestId !== this.resourceSchemaRequestIdByScope[scope]) {
11342
+ return;
11343
+ }
11344
+ const validFields = (fields || []).filter((field) => Boolean(field?.name && String(field.name).trim()));
11345
+ if (validFields.length > 0) {
11346
+ this.setCachedFieldsForResource(trimmed, validFields);
11347
+ if (autoSuggestDefaults) {
11348
+ this.applyAutoSuggestedDependentKeys(scope, validFields);
11349
+ }
11350
+ }
11351
+ if (this.normalizedProps.length > 0) {
11352
+ this.renderedProps = this.buildRenderedProps(this.normalizedProps);
11353
+ }
11354
+ });
11355
+ }
10874
11356
  resolveCanonicalEditorProperties(controlType, controlTypeToken) {
10875
11357
  const inlineProps = resolveInlineFilterEditorProperties(controlTypeToken, this.seed);
10876
11358
  if (inlineProps?.length)
@@ -10965,6 +11447,13 @@ class FieldMetadataEditorComponent {
10965
11447
  return undefined;
10966
11448
  }
10967
11449
  rebuildEditorForType(ct) {
11450
+ this.formRebuild$.next();
11451
+ if (this.initialControlType === undefined) {
11452
+ this.initialControlType = ct;
11453
+ }
11454
+ if (this.initialSeed === undefined) {
11455
+ this.initialSeed = this.cloneSnapshot(this.seed ?? {});
11456
+ }
10968
11457
  this.controlType = ct;
10969
11458
  let props = this.registry.getProperties(this.controlType);
10970
11459
  const controlTypeToken = normalizeControlTypeToken(this.controlType);
@@ -10985,23 +11474,6 @@ class FieldMetadataEditorComponent {
10985
11474
  }
10986
11475
  }
10987
11476
  }
10988
- try {
10989
- (console.log || console.debug)('[FieldMetadataEditor] init', {
10990
- controlType: this.controlType,
10991
- seedPreview: {
10992
- resourcePath: this.seed?.resourcePath,
10993
- optionLabelKey: this.seed?.optionLabelKey,
10994
- optionValueKey: this.seed?.optionValueKey,
10995
- filterCriteria: typeof this.seed?.filterCriteria === 'string'
10996
- ? this.seed?.filterCriteria
10997
- : this.seed?.filterCriteria != null
10998
- ? JSON.stringify(this.seed?.filterCriteria)
10999
- : undefined,
11000
- },
11001
- propsBefore: props.map((p) => p.name),
11002
- });
11003
- }
11004
- catch { }
11005
11477
  // Fallback de registro mínimo para facilitar execução inicial
11006
11478
  if (!props.length && canonicalProps?.length) {
11007
11479
  this.registry.register(this.controlType, canonicalProps);
@@ -11174,12 +11646,6 @@ class FieldMetadataEditorComponent {
11174
11646
  props = treeSelectProperties;
11175
11647
  }
11176
11648
  }
11177
- try {
11178
- (console.log || console.debug)('[FieldMetadataEditor] props after registry resolution', {
11179
- resolvedProps: props.map((p) => p.name),
11180
- });
11181
- }
11182
- catch { }
11183
11649
  // Inject controlType selector into 'Geral' group so users can change field type
11184
11650
  try {
11185
11651
  const typeProp = {
@@ -11198,28 +11664,6 @@ class FieldMetadataEditorComponent {
11198
11664
  }
11199
11665
  catch { }
11200
11666
  this.normalizedProps = this.normalizer.normalize(props, this.seed);
11201
- try {
11202
- console.debug('[FieldMetadataEditor] normalized props', {
11203
- propNames: this.normalizedProps.map((p) => p.name),
11204
- defaults: this.normalizedProps
11205
- .filter((p) => ['resourcePath', 'filterCriteria', 'optionLabelKey', 'optionValueKey'].includes(p.name))
11206
- .map((p) => ({ name: p.name, defaultValue: p.defaultValue })),
11207
- optionsDefault: (() => {
11208
- const opt = this.normalizedProps.find((p) => p.name === 'options');
11209
- if (!opt)
11210
- return undefined;
11211
- const v = opt.defaultValue;
11212
- return {
11213
- type: v == null ? 'null' : typeof v,
11214
- preview: typeof v === 'string' ? String(v).slice(0, 80) : undefined,
11215
- lengthOrCount: Array.isArray(v)
11216
- ? v.length
11217
- : (typeof v === 'string' ? v.length : undefined),
11218
- };
11219
- })(),
11220
- });
11221
- }
11222
- catch { }
11223
11667
  // Backward-compatible bridge: if a config still uses providerOptions, hydrate from seed.options.
11224
11668
  try {
11225
11669
  const hasProviderOptions = this.normalizedProps.some((p) => p.name === 'providerOptions');
@@ -11232,10 +11676,6 @@ class FieldMetadataEditorComponent {
11232
11676
  return String(seedOptions);
11233
11677
  } })();
11234
11678
  this.normalizedProps = this.normalizedProps.map((p) => p.name === 'providerOptions' && p.defaultValue === undefined ? { ...p, defaultValue: v } : p);
11235
- (console.log || console.debug)('[FieldMetadataEditor] providerOptions default injected from seed.options', {
11236
- type: typeof seedOptions,
11237
- lengthOrCount: Array.isArray(seedOptions) ? seedOptions.length : (typeof seedOptions === 'string' ? seedOptions.length : undefined),
11238
- });
11239
11679
  }
11240
11680
  }
11241
11681
  catch { }
@@ -11282,61 +11722,19 @@ class FieldMetadataEditorComponent {
11282
11722
  this.form.patchValue(subset, { emitEvent: false });
11283
11723
  }
11284
11724
  catch { }
11285
- // Diagnostics for Rating seeding after applying seed → form
11286
- try {
11287
- const ctU = String(this.controlType || '').toUpperCase();
11288
- if (ctU === String(FieldControlType.RATING)) {
11289
- const pick = (k) => this.form.get(k)?.value;
11290
- console.debug('[FieldMetadataEditor][RATING] seeded form values', {
11291
- max: pick('max'),
11292
- allowHalf: pick('allowHalf'),
11293
- selection: pick('selection'),
11294
- size: pick('size'),
11295
- icon: pick('icon'),
11296
- emptyIcon: pick('emptyIcon'),
11297
- svgIcon: pick('svgIcon'),
11298
- svgIconOutline: pick('svgIconOutline'),
11299
- ratingColor: pick('ratingColor'),
11300
- outlineColor: pick('outlineColor'),
11301
- readonly: pick('readonly'),
11302
- tabIndex: pick('tabIndex'),
11303
- ariaLabel: pick('ariaLabel'),
11304
- });
11305
- }
11306
- }
11307
- catch { }
11308
- }
11309
- catch { }
11310
- // Diagnostics: dump initial values for Rating editor
11311
- try {
11312
- const ctU = String(this.controlType || '').toUpperCase();
11313
- if (ctU === String(FieldControlType.RATING)) {
11314
- const pick = (k) => this.form.get(k)?.value;
11315
- console.debug('[FieldMetadataEditor][RATING] initial form values', {
11316
- max: pick('max'),
11317
- allowHalf: pick('allowHalf'),
11318
- selection: pick('selection'),
11319
- size: pick('size'),
11320
- icon: pick('icon'),
11321
- emptyIcon: pick('emptyIcon'),
11322
- svgIcon: pick('svgIcon'),
11323
- svgIconOutline: pick('svgIconOutline'),
11324
- ratingColor: pick('ratingColor'),
11325
- outlineColor: pick('outlineColor'),
11326
- readonly: pick('readonly'),
11327
- tabIndex: pick('tabIndex'),
11328
- ariaLabel: pick('ariaLabel'),
11329
- });
11330
- }
11331
11725
  }
11332
11726
  catch { }
11333
11727
  // Capture baseline after seed is applied to avoid missing first user change
11334
11728
  try {
11335
- this.initialSnapshot = this.form.getRawValue();
11729
+ if (this.initialSnapshot == null) {
11730
+ this.initialSnapshot = this.form.getRawValue();
11731
+ }
11336
11732
  this.baselineReady = true;
11337
- if (this.isDirty$.value)
11733
+ const controlTypeChanged = this.initialControlType !== undefined &&
11734
+ this.controlType !== this.initialControlType;
11735
+ if (!controlTypeChanged && this.isDirty$.value) {
11338
11736
  this.isDirty$.next(false);
11339
- (console.log || console.debug)('[FieldMetadataEditor] baseline captured (immediate)');
11737
+ }
11340
11738
  }
11341
11739
  catch {
11342
11740
  this.baselineReady = true;
@@ -11344,12 +11742,12 @@ class FieldMetadataEditorComponent {
11344
11742
  // track validity/dirty with snapshot-based detection (avoid false dirty on init)
11345
11743
  this.isValid$.next(this.form.valid);
11346
11744
  this.form.statusChanges
11347
- .pipe(takeUntilDestroyed(this.destroyRef))
11745
+ .pipe(takeUntil(this.formRebuild$), takeUntilDestroyed(this.destroyRef))
11348
11746
  .subscribe(() => this.isValid$.next(this.form.valid));
11349
11747
  this.setupGradientStopValidationSync();
11350
11748
  this.setupCurrencyPresetSync();
11351
11749
  this.form.valueChanges
11352
- .pipe(takeUntilDestroyed(this.destroyRef))
11750
+ .pipe(takeUntil(this.formRebuild$), takeUntilDestroyed(this.destroyRef))
11353
11751
  .subscribe((val) => {
11354
11752
  if (!this.baselineReady) {
11355
11753
  // Ignore initial churn until baseline is captured
@@ -11359,12 +11757,10 @@ class FieldMetadataEditorComponent {
11359
11757
  this.initialSnapshot = val;
11360
11758
  if (this.isDirty$.value)
11361
11759
  this.isDirty$.next(false);
11362
- try {
11363
- (console.log || console.debug)('[FieldMetadataEditor] baseline set from first stable value');
11364
- }
11365
- catch { }
11366
11760
  return;
11367
11761
  }
11762
+ const controlTypeChanged = this.initialControlType !== undefined &&
11763
+ this.controlType !== this.initialControlType;
11368
11764
  let same = false;
11369
11765
  try {
11370
11766
  same = JSON.stringify(val) === JSON.stringify(this.initialSnapshot);
@@ -11372,15 +11768,15 @@ class FieldMetadataEditorComponent {
11372
11768
  catch {
11373
11769
  same = false;
11374
11770
  }
11375
- const dirty = !same;
11771
+ const dirty = controlTypeChanged || !same;
11376
11772
  if (dirty !== this.isDirty$.value) {
11377
- try {
11378
- (console.log || console.debug)('[FieldMetadataEditor] dirty ->', dirty);
11379
- }
11380
- catch { }
11381
11773
  this.isDirty$.next(dirty);
11382
11774
  }
11383
11775
  });
11776
+ if (this.discoveredResourceOptions.length > 0) {
11777
+ this.renderedProps = this.buildRenderedProps(this.normalizedProps);
11778
+ }
11779
+ this.loadCatalogResourceOptionsIfNeeded();
11384
11780
  }
11385
11781
  setupGradientStopValidationSync() {
11386
11782
  const rows = new Map();
@@ -11396,7 +11792,7 @@ class FieldMetadataEditorComponent {
11396
11792
  if (!source)
11397
11793
  continue;
11398
11794
  source.valueChanges
11399
- .pipe(takeUntilDestroyed(this.destroyRef))
11795
+ .pipe(takeUntil(this.formRebuild$), takeUntilDestroyed(this.destroyRef))
11400
11796
  .subscribe(() => {
11401
11797
  for (const targetField of fieldNames) {
11402
11798
  if (targetField === sourceField)
@@ -11448,7 +11844,7 @@ class FieldMetadataEditorComponent {
11448
11844
  VES: { locale: 'es-VE', decimalPlaces: 2, currencyPosition: 'before' },
11449
11845
  };
11450
11846
  currencyControl.valueChanges
11451
- .pipe(takeUntilDestroyed(this.destroyRef))
11847
+ .pipe(takeUntil(this.formRebuild$), takeUntilDestroyed(this.destroyRef))
11452
11848
  .subscribe((value) => {
11453
11849
  const code = String(value || '').toUpperCase();
11454
11850
  const preset = presets[code];
@@ -11499,13 +11895,7 @@ class FieldMetadataEditorComponent {
11499
11895
  // Sempre incluir controlType no patch quando presente
11500
11896
  patch.controlType = this.controlType;
11501
11897
  try {
11502
- console.debug('[FieldMetadataEditor] apply() patch', patch);
11503
- }
11504
- catch { }
11505
- try {
11506
- const fn = this.__hostApplyPatch;
11507
- if (typeof fn === 'function')
11508
- fn(patch);
11898
+ this.hostApplyPatch?.(patch);
11509
11899
  }
11510
11900
  catch { }
11511
11901
  this.applied.emit(patch);
@@ -11526,10 +11916,6 @@ class FieldMetadataEditorComponent {
11526
11916
  delete patch.providerOptions;
11527
11917
  }
11528
11918
  patch.controlType = this.controlType;
11529
- try {
11530
- console.debug('[FieldMetadataEditor] getSettingsValue() patch', patch);
11531
- }
11532
- catch { }
11533
11919
  return patch;
11534
11920
  }
11535
11921
  /**
@@ -11639,6 +12025,18 @@ class FieldMetadataEditorComponent {
11639
12025
  this.normalizeJsonObjectProperty(patch, 'valuePresentation');
11640
12026
  this.normalizeJsonObjectProperty(patch, 'presentation');
11641
12027
  this.normalizeJsonArrayProperty(patch, 'presentationRules');
12028
+ const seedRecordOpen = this.seed?.quickView?.recordOpen;
12029
+ if (patch?.quickView &&
12030
+ typeof patch.quickView === 'object' &&
12031
+ !Array.isArray(patch.quickView) &&
12032
+ seedRecordOpen &&
12033
+ typeof seedRecordOpen === 'object' &&
12034
+ patch.quickView.recordOpen === undefined) {
12035
+ patch.quickView = {
12036
+ recordOpen: this.cloneSnapshot(seedRecordOpen),
12037
+ ...patch.quickView,
12038
+ };
12039
+ }
11642
12040
  }
11643
12041
  normalizeEntityLookupPatchValues(patch) {
11644
12042
  const optionSource = patch?.optionSource;
@@ -11811,6 +12209,12 @@ class FieldMetadataEditorComponent {
11811
12209
  }
11812
12210
  }
11813
12211
  reset() {
12212
+ if (this.initialSeed !== undefined) {
12213
+ this.seed = this.cloneSnapshot(this.initialSeed);
12214
+ }
12215
+ if (this.initialControlType !== undefined && this.controlType !== this.initialControlType) {
12216
+ this.rebuildEditorForType(this.initialControlType);
12217
+ }
11814
12218
  const snapshot = this.cloneSnapshot(this.initialSnapshot ?? this.form.getRawValue());
11815
12219
  this.form.reset(snapshot);
11816
12220
  this.form.markAsPristine();
@@ -11828,30 +12232,414 @@ class FieldMetadataEditorComponent {
11828
12232
  if (value != null)
11829
12233
  this.onControlTypeChange(value);
11830
12234
  }
11831
- withSharedFieldMetadataProperties(props) {
11832
- const propNames = new Set(props.map((prop) => prop.name));
11833
- const sharedProps = [...fieldPresentationProperties, ...fieldSubmitProperties];
11834
- const missingSharedProps = sharedProps.filter((prop) => !propNames.has(prop.name));
11835
- return missingSharedProps.length ? [...props, ...missingSharedProps] : props;
11836
- }
11837
- get isArrayEditor() {
11838
- return this.controlType === FieldControlType.ARRAY_INPUT;
12235
+ tx(key, fallback) {
12236
+ const namespacedKey = key.startsWith('praxis.metadataEditor.')
12237
+ ? key
12238
+ : `praxis.metadataEditor.${key}`;
12239
+ return this.i18n?.t(namespacedKey, undefined, fallback) ?? fallback;
11839
12240
  }
11840
- buildRenderedProps(props) {
11841
- if (!this.isArrayEditor) {
11842
- return props;
12241
+ buildCurrentSemanticFieldSnapshot() {
12242
+ const currentRaw = this.form?.getRawValue?.() ?? {};
12243
+ const merged = {
12244
+ ...(this.seed || {}),
12245
+ };
12246
+ for (const [k, v] of Object.entries(currentRaw)) {
12247
+ if (v !== undefined && v !== null && v !== '') {
12248
+ merged[k] = v;
12249
+ }
11843
12250
  }
11844
- return (props || []).filter((prop) => prop?.name !== 'array.itemSchema.fields');
12251
+ merged['name'] = String(currentRaw?.name ?? this.seed?.name ?? '').trim();
12252
+ merged['controlType'] = this.controlType;
12253
+ return merged;
11845
12254
  }
11846
- cloneSnapshot(value) {
11847
- return this.cloneMetadataValue(value, new WeakMap());
12255
+ canCalibrateFromSemantics() {
12256
+ const snapshot = this.buildCurrentSemanticFieldSnapshot();
12257
+ return hasSemanticFieldCalibrationDiff(snapshot);
11848
12258
  }
11849
- cloneMetadataValue(value, seen) {
11850
- if (!value || typeof value !== 'object') {
11851
- return value;
11852
- }
11853
- if (seen.has(value)) {
11854
- return seen.get(value);
12259
+ calibrateFromSemantics() {
12260
+ const mergedField = this.buildCurrentSemanticFieldSnapshot();
12261
+ if (!hasSemanticFieldCalibrationDiff(mergedField)) {
12262
+ return;
12263
+ }
12264
+ const calibrated = calibrateFieldMetadataFromSemantics(mergedField);
12265
+ this.seed = {
12266
+ ...(this.seed || {}),
12267
+ ...calibrated,
12268
+ };
12269
+ const nextControlType = (calibrated.controlType ?? this.controlType);
12270
+ if (nextControlType && nextControlType !== this.controlType) {
12271
+ this.rebuildEditorForType(nextControlType);
12272
+ }
12273
+ const getByPath = (obj, path) => {
12274
+ if (!obj || typeof obj !== 'object' || !path)
12275
+ return undefined;
12276
+ return path.split('.').reduce((acc, part) => (acc == null ? undefined : acc[part]), obj);
12277
+ };
12278
+ const formPatch = {};
12279
+ for (const prop of this.normalizedProps || []) {
12280
+ const propName = String(prop?.name ?? '').trim();
12281
+ if (!propName)
12282
+ continue;
12283
+ const rawVal = getByPath(calibrated, propName);
12284
+ if (rawVal === undefined)
12285
+ continue;
12286
+ const controlVal = this.toEditorControlValue(prop, rawVal);
12287
+ if (controlVal !== undefined && controlVal !== null) {
12288
+ try {
12289
+ this.factory.setValueByPath(this.form, propName, controlVal);
12290
+ }
12291
+ catch { }
12292
+ if (!propName.includes('.') && this.form?.get(propName)) {
12293
+ formPatch[propName] = controlVal;
12294
+ }
12295
+ }
12296
+ }
12297
+ if (calibrated.options !== undefined &&
12298
+ !this.form?.get('options') &&
12299
+ this.form?.get('providerOptions')) {
12300
+ const prop = this.normalizedProps?.find((p) => p?.name === 'providerOptions');
12301
+ formPatch['providerOptions'] = this.toEditorControlValue(prop, calibrated.options);
12302
+ }
12303
+ if (Object.keys(formPatch).length > 0) {
12304
+ this.form.patchValue(formPatch, { emitEvent: true });
12305
+ }
12306
+ this.isDirty$.next(true);
12307
+ }
12308
+ withSharedFieldMetadataProperties(props) {
12309
+ const propNames = new Set(props.map((prop) => prop.name));
12310
+ const sharedProps = [...fieldPresentationProperties, ...fieldSubmitProperties];
12311
+ const missingSharedProps = sharedProps.filter((prop) => !propNames.has(prop.name));
12312
+ return missingSharedProps.length ? [...props, ...missingSharedProps] : props;
12313
+ }
12314
+ get isArrayEditor() {
12315
+ return this.controlType === FieldControlType.ARRAY_INPUT;
12316
+ }
12317
+ static ROOT_DEPENDENT_FIELD_KEY_PROPS = new Set([
12318
+ 'optionValueKey',
12319
+ 'optionLabelKey',
12320
+ 'lookupIdKey',
12321
+ 'lookupLabelKey',
12322
+ 'lookupSubtitleKey',
12323
+ 'parentIdKey',
12324
+ ]);
12325
+ static OPTION_SOURCE_DEPENDENT_FIELD_KEY_PROPS = new Set([
12326
+ 'optionSource.valuePropertyPath',
12327
+ 'optionSource.labelPropertyPath',
12328
+ 'optionSource.codePropertyPath',
12329
+ 'optionSource.statusPropertyPath',
12330
+ 'optionSource.subtitlePropertyPath',
12331
+ 'optionSource.descriptionPropertyPath',
12332
+ ]);
12333
+ buildRenderedProps(props) {
12334
+ const base = !this.isArrayEditor
12335
+ ? props || []
12336
+ : (props || []).filter((prop) => prop?.name !== 'array.itemSchema.fields');
12337
+ if (!this.discoveredResourceOptions.length &&
12338
+ this.resourceSchemaFieldsByPath.size === 0) {
12339
+ return base;
12340
+ }
12341
+ return base.map((prop) => {
12342
+ const resourceEnriched = this.enrichResourcePathProperty(prop);
12343
+ return this.enrichDependentFieldKeyProperty(resourceEnriched);
12344
+ });
12345
+ }
12346
+ enrichResourcePathProperty(prop) {
12347
+ if (!prop ||
12348
+ (prop.name !== 'resourcePath' && prop.name !== 'optionSource.resourcePath')) {
12349
+ return prop;
12350
+ }
12351
+ const optionSourceType = String(this.form?.get('optionSource.type')?.value ??
12352
+ this.seed?.optionSource?.type ??
12353
+ '')
12354
+ .trim()
12355
+ .toLowerCase();
12356
+ const useStatsIntent = prop.name === 'optionSource.resourcePath' && optionSourceType === 'stats';
12357
+ const filteredResources = this.discoveredResourceOptions.filter((item) => useStatsIntent
12358
+ ? item.capabilities?.stats !== false
12359
+ : item.capabilities?.options !== false);
12360
+ if (!filteredResources.length) {
12361
+ return prop;
12362
+ }
12363
+ const currentValue = String(this.form?.get(prop.name)?.value ?? prop.defaultValue ?? '').trim();
12364
+ const matchedCatalogOption = currentValue
12365
+ ? this.resourceDiscovery?.findCatalogResourceOption?.(currentValue, filteredResources)
12366
+ : null;
12367
+ const readOnlyLabel = this.i18n?.t('quickConnect.readOnly', undefined, 'Somente leitura', 'resourceDiscovery') ?? 'Somente leitura';
12368
+ const options = filteredResources.map((item) => {
12369
+ const readOnlySuffix = item.readOnly ? readOnlyLabel : null;
12370
+ const hasDistinctPath = Boolean(item.path && item.path !== item.label);
12371
+ const code = hasDistinctPath ? item.path : null;
12372
+ const badge = [item.group, readOnlySuffix]
12373
+ .filter((part) => Boolean(part))
12374
+ .join(' · ') || null;
12375
+ const description = String(item.description ?? '').trim() || null;
12376
+ return {
12377
+ value: item === matchedCatalogOption && currentValue ? currentValue : item.value,
12378
+ text: item.label,
12379
+ icon: item.icon ?? 'api',
12380
+ code,
12381
+ badge,
12382
+ description,
12383
+ searchTerms: [
12384
+ ...item.searchTerms,
12385
+ ...(readOnlySuffix ? [readOnlySuffix] : []),
12386
+ ],
12387
+ };
12388
+ });
12389
+ if (currentValue && !matchedCatalogOption) {
12390
+ options.unshift({
12391
+ value: currentValue,
12392
+ text: currentValue,
12393
+ icon: 'link',
12394
+ code: null,
12395
+ badge: null,
12396
+ description: null,
12397
+ searchTerms: [currentValue],
12398
+ });
12399
+ }
12400
+ return {
12401
+ ...prop,
12402
+ editorType: 'select',
12403
+ searchable: true,
12404
+ searchPlaceholderKey: 'fieldResource.searchPlaceholder',
12405
+ searchPlaceholder: 'Buscar por nome, domínio ou caminho',
12406
+ options,
12407
+ };
12408
+ }
12409
+ enrichDependentFieldKeyProperty(prop) {
12410
+ const propName = String(prop?.name ?? '').trim();
12411
+ const isRootDependent = FieldMetadataEditorComponent.ROOT_DEPENDENT_FIELD_KEY_PROPS.has(propName);
12412
+ const isOptionSourceDependent = FieldMetadataEditorComponent.OPTION_SOURCE_DEPENDENT_FIELD_KEY_PROPS.has(propName);
12413
+ if (!isRootDependent && !isOptionSourceDependent) {
12414
+ return prop;
12415
+ }
12416
+ const resourceControlPath = isOptionSourceDependent
12417
+ ? 'optionSource.resourcePath'
12418
+ : 'resourcePath';
12419
+ const resourcePath = String(this.form?.get(resourceControlPath)?.value ??
12420
+ (isOptionSourceDependent
12421
+ ? this.seed?.optionSource?.resourcePath
12422
+ : this.seed?.resourcePath) ??
12423
+ '').trim();
12424
+ if (!resourcePath) {
12425
+ return prop;
12426
+ }
12427
+ const fields = this.getCachedFieldsForResource(resourcePath);
12428
+ if (!fields.length) {
12429
+ return prop;
12430
+ }
12431
+ const currentValue = String(this.form?.get(propName)?.value ?? prop.defaultValue ?? '').trim();
12432
+ const options = fields.map((field) => {
12433
+ const fieldName = String(field.name ?? '').trim();
12434
+ const fieldLabel = String(field.label ?? '').trim();
12435
+ const hasDistinctLabel = Boolean(fieldLabel && fieldLabel !== fieldName);
12436
+ const typeHint = String(field.type || field.controlType || '').trim();
12437
+ const description = String(field.description ?? '').trim() || null;
12438
+ return {
12439
+ value: fieldName,
12440
+ text: hasDistinctLabel ? fieldLabel : fieldName,
12441
+ code: hasDistinctLabel ? fieldName : null,
12442
+ badge: typeHint || null,
12443
+ description,
12444
+ searchTerms: [
12445
+ fieldName,
12446
+ fieldLabel,
12447
+ typeHint,
12448
+ description ?? '',
12449
+ ].filter(Boolean),
12450
+ };
12451
+ });
12452
+ if (currentValue &&
12453
+ !options.some((option) => option.value === currentValue)) {
12454
+ options.unshift({
12455
+ value: currentValue,
12456
+ text: currentValue,
12457
+ code: null,
12458
+ badge: null,
12459
+ description: null,
12460
+ searchTerms: [currentValue],
12461
+ });
12462
+ }
12463
+ return {
12464
+ ...prop,
12465
+ editorType: 'select',
12466
+ searchable: true,
12467
+ searchPlaceholderKey: 'fieldResource.fieldSearchPlaceholder',
12468
+ searchPlaceholder: 'Buscar campo por nome ou rótulo',
12469
+ options,
12470
+ };
12471
+ }
12472
+ normalizeResourceCacheKey(resourcePath) {
12473
+ const raw = String(resourcePath ?? '').trim();
12474
+ if (!raw) {
12475
+ return '';
12476
+ }
12477
+ if (this.discoveredResourceOptions.length > 0) {
12478
+ const matched = this.resourceDiscovery?.findCatalogResourceOption?.(raw, this.discoveredResourceOptions);
12479
+ if (matched?.value) {
12480
+ return matched.value.trim().replace(/^\/+|\/+$/g, '');
12481
+ }
12482
+ }
12483
+ return raw.replace(/^\/+|\/+$/g, '').replace(/^api\/+/i, '');
12484
+ }
12485
+ getCachedFieldsForResource(resourcePath) {
12486
+ const key = this.normalizeResourceCacheKey(resourcePath);
12487
+ if (!key) {
12488
+ return [];
12489
+ }
12490
+ const direct = this.resourceSchemaFieldsByPath.get(key);
12491
+ if (direct?.length) {
12492
+ return direct;
12493
+ }
12494
+ const rawKey = String(resourcePath ?? '').trim().replace(/^\/+|\/+$/g, '');
12495
+ return this.resourceSchemaFieldsByPath.get(rawKey) ?? [];
12496
+ }
12497
+ setCachedFieldsForResource(resourcePath, fields) {
12498
+ const key = this.normalizeResourceCacheKey(resourcePath);
12499
+ if (key) {
12500
+ this.resourceSchemaFieldsByPath.set(key, fields);
12501
+ }
12502
+ const rawKey = String(resourcePath ?? '').trim().replace(/^\/+|\/+$/g, '');
12503
+ if (rawKey) {
12504
+ this.resourceSchemaFieldsByPath.set(rawKey, fields);
12505
+ }
12506
+ }
12507
+ applyAutoSuggestedDependentKeys(scope, fields) {
12508
+ if (!this.form || !fields.length) {
12509
+ return;
12510
+ }
12511
+ const fieldNameSet = new Set(fields.map((field) => String(field.name ?? '').trim()).filter(Boolean));
12512
+ const suggestions = this.selectSuggestedFieldKeys(fields);
12513
+ const targets = scope === 'optionSource'
12514
+ ? [
12515
+ {
12516
+ path: 'optionSource.valuePropertyPath',
12517
+ suggested: suggestions.valueKey,
12518
+ },
12519
+ {
12520
+ path: 'optionSource.labelPropertyPath',
12521
+ suggested: suggestions.labelKey,
12522
+ },
12523
+ {
12524
+ path: 'optionSource.codePropertyPath',
12525
+ suggested: suggestions.codeKey,
12526
+ },
12527
+ {
12528
+ path: 'optionSource.statusPropertyPath',
12529
+ suggested: suggestions.statusKey,
12530
+ },
12531
+ ]
12532
+ : [
12533
+ { path: 'optionValueKey', suggested: suggestions.valueKey },
12534
+ { path: 'optionLabelKey', suggested: suggestions.labelKey },
12535
+ { path: 'lookupIdKey', suggested: suggestions.valueKey },
12536
+ { path: 'lookupLabelKey', suggested: suggestions.labelKey },
12537
+ { path: 'lookupSubtitleKey', suggested: suggestions.subtitleKey },
12538
+ ];
12539
+ for (const target of targets) {
12540
+ const ctrl = this.form.get(target.path);
12541
+ if (!ctrl) {
12542
+ continue;
12543
+ }
12544
+ const current = String(ctrl.value ?? '').trim();
12545
+ if (!current || !fieldNameSet.has(current)) {
12546
+ const nextValue = target.suggested ?? '';
12547
+ if (current !== nextValue) {
12548
+ ctrl.setValue(nextValue);
12549
+ }
12550
+ }
12551
+ }
12552
+ }
12553
+ selectSuggestedFieldKeys(fields) {
12554
+ const names = fields
12555
+ .map((field) => String(field.name ?? '').trim())
12556
+ .filter(Boolean);
12557
+ const findExact = (candidates, exclude = new Set()) => {
12558
+ for (const candidate of candidates) {
12559
+ const lower = candidate.toLowerCase();
12560
+ const found = names.find((name) => !exclude.has(name) && name.toLowerCase() === lower);
12561
+ if (found) {
12562
+ return found;
12563
+ }
12564
+ }
12565
+ return null;
12566
+ };
12567
+ const valueKey = findExact([
12568
+ 'id',
12569
+ 'codigo',
12570
+ 'code',
12571
+ 'uuid',
12572
+ 'key',
12573
+ 'valor',
12574
+ 'value',
12575
+ 'matricula',
12576
+ 'sigla',
12577
+ ]) ??
12578
+ names.find((name) => /(?:Id|Codigo|Code)$/.test(name)) ??
12579
+ names[0] ??
12580
+ null;
12581
+ const usedForValue = new Set(valueKey ? [valueKey] : []);
12582
+ const labelKey = findExact([
12583
+ 'nome',
12584
+ 'name',
12585
+ 'label',
12586
+ 'descricao',
12587
+ 'description',
12588
+ 'titulo',
12589
+ 'title',
12590
+ 'razaosocial',
12591
+ 'nomecompleto',
12592
+ 'rotulo',
12593
+ 'text',
12594
+ ], usedForValue) ??
12595
+ fields
12596
+ .find((field) => !usedForValue.has(String(field.name ?? '').trim()) &&
12597
+ String(field.type ?? 'string').toLowerCase() === 'string')
12598
+ ?.name?.trim() ??
12599
+ names.find((name) => !usedForValue.has(name)) ??
12600
+ valueKey;
12601
+ const codeKey = findExact([
12602
+ 'codigo',
12603
+ 'code',
12604
+ 'sigla',
12605
+ 'chave',
12606
+ 'matricula',
12607
+ 'referencia',
12608
+ ]);
12609
+ const statusKey = findExact([
12610
+ 'status',
12611
+ 'situacao',
12612
+ 'situacaofuncional',
12613
+ 'ativo',
12614
+ 'active',
12615
+ 'estado',
12616
+ 'enabled',
12617
+ ]);
12618
+ const usedForPrimary = new Set([valueKey, labelKey].filter((item) => Boolean(item)));
12619
+ const subtitleKey = findExact([
12620
+ 'descricao',
12621
+ 'description',
12622
+ 'subtitulo',
12623
+ 'subtitle',
12624
+ 'resumo',
12625
+ 'summary',
12626
+ 'email',
12627
+ 'cargo',
12628
+ 'departamento',
12629
+ 'sigla',
12630
+ 'codigo',
12631
+ ], usedForPrimary);
12632
+ return { valueKey, labelKey, codeKey, statusKey, subtitleKey };
12633
+ }
12634
+ cloneSnapshot(value) {
12635
+ return this.cloneMetadataValue(value, new WeakMap());
12636
+ }
12637
+ cloneMetadataValue(value, seen) {
12638
+ if (!value || typeof value !== 'object') {
12639
+ return value;
12640
+ }
12641
+ if (seen.has(value)) {
12642
+ return seen.get(value);
11855
12643
  }
11856
12644
  if (value instanceof Date) {
11857
12645
  return new Date(value.getTime());
@@ -11954,10 +12742,25 @@ class FieldMetadataEditorComponent {
11954
12742
  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 });
11955
12743
  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: [
11956
12744
  providePraxisMetadataEditorI18n(),
12745
+ providePraxisI18nConfig(RESOURCE_DISCOVERY_I18N_CONFIG),
11957
12746
  ...providePraxisDynamicFieldsI18n(),
11958
12747
  ], ngImport: i0, template: `
11959
12748
  @if (form) {
11960
12749
  <div class="p-3">
12750
+ @if (canCalibrateFromSemantics()) {
12751
+ <div class="d-flex justify-content-end mb-2">
12752
+ <button
12753
+ mat-stroked-button
12754
+ type="button"
12755
+ data-testid="field-metadata-smart-calibrate-btn"
12756
+ (click)="calibrateFromSemantics()"
12757
+ [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.')"
12758
+ >
12759
+ <mat-icon aria-hidden="true">auto_fix_high</mat-icon>
12760
+ {{ tx('smartCalibrate.action', 'Calibrar pelo schema') }}
12761
+ </button>
12762
+ </div>
12763
+ }
11961
12764
  <praxis-dynamic-editor-renderer
11962
12765
  [properties]="renderedProps"
11963
12766
  [form]="form"
@@ -11970,7 +12773,7 @@ class FieldMetadataEditorComponent {
11970
12773
  }
11971
12774
  </div>
11972
12775
  }
11973
- `, 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"] }] });
12776
+ `, 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"] }] });
11974
12777
  }
11975
12778
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: FieldMetadataEditorComponent, decorators: [{
11976
12779
  type: Component,
@@ -11979,16 +12782,34 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
11979
12782
  standalone: true,
11980
12783
  imports: [
11981
12784
  ReactiveFormsModule,
12785
+ MatButtonModule,
12786
+ MatIconModule,
12787
+ MatTooltipModule,
11982
12788
  DynamicEditorRendererComponent,
11983
12789
  ArrayItemSchemaFieldsEditorComponent
11984
12790
  ],
11985
12791
  providers: [
11986
12792
  providePraxisMetadataEditorI18n(),
12793
+ providePraxisI18nConfig(RESOURCE_DISCOVERY_I18N_CONFIG),
11987
12794
  ...providePraxisDynamicFieldsI18n(),
11988
12795
  ],
11989
12796
  template: `
11990
12797
  @if (form) {
11991
12798
  <div class="p-3">
12799
+ @if (canCalibrateFromSemantics()) {
12800
+ <div class="d-flex justify-content-end mb-2">
12801
+ <button
12802
+ mat-stroked-button
12803
+ type="button"
12804
+ data-testid="field-metadata-smart-calibrate-btn"
12805
+ (click)="calibrateFromSemantics()"
12806
+ [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.')"
12807
+ >
12808
+ <mat-icon aria-hidden="true">auto_fix_high</mat-icon>
12809
+ {{ tx('smartCalibrate.action', 'Calibrar pelo schema') }}
12810
+ </button>
12811
+ </div>
12812
+ }
11992
12813
  <praxis-dynamic-editor-renderer
11993
12814
  [properties]="renderedProps"
11994
12815
  [form]="form"
@@ -12174,16 +12995,132 @@ class CascadeRulesService {
12174
12995
  dependencyLoadOnChange: null,
12175
12996
  };
12176
12997
  }
12177
- /** Suggest a basic mapping dep->dep and a default valuePath */
12998
+ /** Fill missing mappings and value paths without replacing authored values. */
12178
12999
  suggest(rule, inferredValueKey) {
12179
- const map = {};
12180
- (rule.dependencyFields || []).forEach((dep) => (map[dep] = dep));
13000
+ const map = { ...rule.dependencyFilterMap };
13001
+ (rule.dependencyFields || []).forEach((dep) => {
13002
+ if (!Object.prototype.hasOwnProperty.call(map, dep))
13003
+ map[dep] = dep;
13004
+ });
12181
13005
  return {
12182
13006
  ...rule,
12183
13007
  dependencyFilterMap: Object.keys(map).length ? map : rule.dependencyFilterMap,
12184
13008
  dependencyValuePath: rule.dependencyValuePath || inferredValueKey || 'id',
12185
13009
  };
12186
13010
  }
13011
+ /**
13012
+ * Validates the cascade dependency graph directly from an array of FieldMetadata/FieldDefinition.
13013
+ */
13014
+ validateFieldsCascadeGraph(fields) {
13015
+ const rules = new Map();
13016
+ for (const field of fields || []) {
13017
+ const vm = this.hydrateRule(field);
13018
+ if (vm) {
13019
+ rules.set(field.name, vm);
13020
+ }
13021
+ }
13022
+ return this.validateCascadeGraph(rules, fields);
13023
+ }
13024
+ /**
13025
+ * Detects self-references, direct cycles (A -> B -> A) and indirect cycles (A -> B -> C -> A),
13026
+ * including graphs where fields have multiple parent dependencies.
13027
+ */
13028
+ validateCascadeGraph(rules, fields = []) {
13029
+ const ruleEntries = rules instanceof Map ? Array.from(rules.entries()) : Object.entries(rules || {});
13030
+ const labelMap = this.getFieldLabelMap([...fields]);
13031
+ const formatNode = (name) => {
13032
+ const label = labelMap.get(name);
13033
+ return label && label !== name ? `${label} (${name})` : name;
13034
+ };
13035
+ const adjacency = new Map();
13036
+ for (const [target, rule] of ruleEntries) {
13037
+ if (!rule || rule.enableDependencyCascade === false)
13038
+ continue;
13039
+ const deps = (rule.dependencyFields || [])
13040
+ .filter((dep) => typeof dep === 'string')
13041
+ .map((dep) => dep.trim())
13042
+ .filter(Boolean);
13043
+ if (deps.length > 0) {
13044
+ adjacency.set(target, Array.from(new Set(deps)));
13045
+ }
13046
+ }
13047
+ const cycles = [];
13048
+ const fieldsInCycles = new Set();
13049
+ const recordedCycleSignatures = new Set();
13050
+ const canonicalizeCycle = (cyclePath) => {
13051
+ const loop = cyclePath.slice(0, -1);
13052
+ if (loop.length <= 1)
13053
+ return loop.join('->');
13054
+ let minIdx = 0;
13055
+ for (let i = 1; i < loop.length; i++) {
13056
+ if (loop[i].localeCompare(loop[minIdx]) < 0) {
13057
+ minIdx = i;
13058
+ }
13059
+ }
13060
+ const rotated = [...loop.slice(minIdx), ...loop.slice(0, minIdx)];
13061
+ return rotated.join('->');
13062
+ };
13063
+ const recordCycle = (rawCyclePath) => {
13064
+ const signature = canonicalizeCycle(rawCyclePath);
13065
+ if (recordedCycleSignatures.has(signature))
13066
+ return;
13067
+ recordedCycleSignatures.add(signature);
13068
+ const loop = rawCyclePath.slice(0, -1);
13069
+ loop.forEach((f) => fieldsInCycles.add(f));
13070
+ const isSelfReference = loop.length === 1;
13071
+ const formattedPath = rawCyclePath.map(formatNode).join(' → ');
13072
+ const lastParent = rawCyclePath[rawCyclePath.length - 2];
13073
+ const closingField = rawCyclePath[rawCyclePath.length - 1];
13074
+ const message = isSelfReference
13075
+ ? `Autorreferência inválida: o campo ${formatNode(closingField)} não pode depender de si mesmo. Remova "${closingField}" da lista de campos pais.`
13076
+ : `Dependência circular detectada: ${formattedPath}. Para corrigir, remova a dependência entre ${formatNode(lastParent)} e ${formatNode(closingField)} em um dos campos do ciclo.`;
13077
+ cycles.push({
13078
+ path: rawCyclePath,
13079
+ formattedPath,
13080
+ isSelfReference,
13081
+ message,
13082
+ });
13083
+ };
13084
+ const sortedNodes = Array.from(adjacency.keys()).sort((a, b) => a.localeCompare(b));
13085
+ const globallyVisited = new Set();
13086
+ for (const startNode of sortedNodes) {
13087
+ const stack = [];
13088
+ const onStack = new Set();
13089
+ const dfs = (current) => {
13090
+ stack.push(current);
13091
+ onStack.add(current);
13092
+ const neighbors = adjacency.get(current) || [];
13093
+ for (const dep of neighbors) {
13094
+ if (dep === current) {
13095
+ recordCycle([current, current]);
13096
+ continue;
13097
+ }
13098
+ if (onStack.has(dep)) {
13099
+ const cycleStartIndex = stack.indexOf(dep);
13100
+ if (cycleStartIndex >= 0) {
13101
+ const cyclePath = [...stack.slice(cycleStartIndex), dep];
13102
+ recordCycle(cyclePath);
13103
+ }
13104
+ continue;
13105
+ }
13106
+ if (!globallyVisited.has(dep) && adjacency.has(dep)) {
13107
+ dfs(dep);
13108
+ }
13109
+ }
13110
+ stack.pop();
13111
+ onStack.delete(current);
13112
+ globallyVisited.add(current);
13113
+ };
13114
+ if (!globallyVisited.has(startNode)) {
13115
+ dfs(startNode);
13116
+ }
13117
+ }
13118
+ return {
13119
+ valid: cycles.length === 0,
13120
+ cycles,
13121
+ fieldsInCycles,
13122
+ };
13123
+ }
12187
13124
  normalizeDependencyFields(value) {
12188
13125
  if (Array.isArray(value)) {
12189
13126
  const deps = value
@@ -12212,6 +13149,7 @@ class CascadeManagerTabComponent {
12212
13149
  connections; // optional signal for conflicts
12213
13150
  apply = new EventEmitter();
12214
13151
  cancel = new EventEmitter();
13152
+ validChange = new EventEmitter();
12215
13153
  searchTerm = '';
12216
13154
  searchTimer = null;
12217
13155
  searchValue = '';
@@ -12226,10 +13164,18 @@ class CascadeManagerTabComponent {
12226
13164
  editLoadOn = 'respectLoadOn';
12227
13165
  editDebounce = 150;
12228
13166
  editValuePath = '';
12229
- editFilterMapJson = '';
12230
13167
  importMode = false;
12231
13168
  importJson = '';
13169
+ feedbackMessage = '';
13170
+ feedbackTone = 'info';
12232
13171
  editMapRows = [];
13172
+ editInitialFilterMap = {};
13173
+ editInitialValuePath = 'id';
13174
+ lastEmittedValid = null;
13175
+ setFeedback(message, tone = 'info') {
13176
+ this.feedbackMessage = message;
13177
+ this.feedbackTone = tone;
13178
+ }
12233
13179
  constructor(svc) {
12234
13180
  this.svc = svc;
12235
13181
  }
@@ -12246,20 +13192,17 @@ class CascadeManagerTabComponent {
12246
13192
  this.selectedFieldSig.set(this.fields[0].name);
12247
13193
  this.startEdit(this.fields[0].name);
12248
13194
  }
13195
+ this.syncValidityState();
12249
13196
  }
12250
13197
  ngOnChanges(changes) {
12251
13198
  if (changes['fields'] && !changes['fields'].firstChange) {
12252
- // Re-hydrate rules from incoming fields while preserving existing ones when possible
13199
+ // Re-hydrate rules from incoming fields; incoming fields own the canonical state
12253
13200
  const incoming = this.fields || [];
12254
13201
  const next = new Map();
12255
- // keep existing for fields still present
12256
13202
  for (const f of incoming) {
12257
- const existing = this.rules().get(f.name);
12258
13203
  const hydrated = this.svc.hydrateRule(f);
12259
13204
  if (hydrated)
12260
13205
  next.set(f.name, hydrated);
12261
- else if (existing)
12262
- next.set(f.name, existing);
12263
13206
  }
12264
13207
  this.rules.set(next);
12265
13208
  // preserve selection if still valid
@@ -12267,29 +13210,114 @@ class CascadeManagerTabComponent {
12267
13210
  if (sel && !incoming.some((f) => f.name === sel)) {
12268
13211
  this.selectedFieldSig.set(incoming[0]?.name ?? null);
12269
13212
  }
13213
+ const activeSel = this.selectedField();
13214
+ if (activeSel) {
13215
+ this.startEdit(activeSel);
13216
+ }
13217
+ this.syncValidityState();
12270
13218
  }
12271
13219
  }
12272
- _logCount = 0;
12273
13220
  filteredFields() {
12274
13221
  const q = (this.searchValue || '').toLowerCase();
12275
13222
  const labelMap = this.svc.getFieldLabelMap(this.fields);
12276
13223
  const result = this.fields
12277
13224
  .filter((f) => !q || f.name.toLowerCase().includes(q) || (labelMap.get(f.name) || '').toLowerCase().includes(q))
12278
- .sort((a, b) => (labelMap.get(a.name) || a.name).localeCompare(labelMap.get(b.name) || b.name));
13225
+ .sort((a, b) => {
13226
+ const labelCmp = (labelMap.get(a.name) || a.name).localeCompare(labelMap.get(b.name) || b.name);
13227
+ return labelCmp !== 0 ? labelCmp : a.name.localeCompare(b.name);
13228
+ });
12279
13229
  return result;
12280
13230
  }
12281
13231
  labelFor(name) {
12282
13232
  return this.svc.getFieldLabelMap(this.fields).get(name) || name;
12283
13233
  }
13234
+ /**
13235
+ * Disambiguates fields that share the same display label (e.g. "Cargo" for both `cargoId` and `cargoNome`).
13236
+ */
13237
+ disambiguatedLabelFor(name) {
13238
+ const baseLabel = this.labelFor(name);
13239
+ const duplicateCount = this.fields.filter((f) => (f.label || f.name).trim().toLowerCase() === baseLabel.trim().toLowerCase()).length;
13240
+ if (duplicateCount <= 1) {
13241
+ return baseLabel;
13242
+ }
13243
+ const roleHint = this.hasRemoteOptionSource(name) || /id$/i.test(name)
13244
+ ? 'Selecionável · ID'
13245
+ : 'Texto / Nome';
13246
+ return `${baseLabel} — ${roleHint}`;
13247
+ }
13248
+ hasRemoteOptionSource(name) {
13249
+ const f = this.fields.find((item) => item.name === name);
13250
+ if (!f)
13251
+ return false;
13252
+ return Boolean(f.resourcePath ||
13253
+ f.endpoint ||
13254
+ f.optionSource?.resourcePath ||
13255
+ f.optionSource?.key ||
13256
+ f.controlType === 'async-select' ||
13257
+ f.controlType === 'entity-lookup' ||
13258
+ f.controlType === 'select' ||
13259
+ f.controlType === 'multi-select');
13260
+ }
13261
+ fieldRoleBadge(name) {
13262
+ const f = this.fields.find((item) => item.name === name);
13263
+ if (!f)
13264
+ return 'Campo';
13265
+ const hasEndpoint = Boolean(f.resourcePath || f.endpoint || f.optionSource?.resourcePath || f.optionSource?.key);
13266
+ if (hasEndpoint) {
13267
+ return 'Opções remotas';
13268
+ }
13269
+ if (f.controlType === 'select' || f.controlType === 'async-select' || f.controlType === 'multi-select' || /id$/i.test(name)) {
13270
+ return 'Selecionável';
13271
+ }
13272
+ return 'Texto / Exibição';
13273
+ }
13274
+ fieldRoleSummary(name) {
13275
+ const f = this.fields.find((item) => item.name === name);
13276
+ if (!f)
13277
+ return 'Campo do formulário';
13278
+ const resourcePath = f.resourcePath || f.optionSource?.resourcePath || f.endpoint;
13279
+ if (resourcePath) {
13280
+ return `Carrega opções de "${resourcePath}"`;
13281
+ }
13282
+ return `${this.fieldRoleBadge(name)} (${f.controlType || 'input'})`;
13283
+ }
13284
+ formatParentList(deps) {
13285
+ if (!deps || !deps.length)
13286
+ return '—';
13287
+ return deps.map((d) => `${this.disambiguatedLabelFor(d)} (${d})`).join(', ');
13288
+ }
13289
+ formatRelationshipSummary(targetField, deps) {
13290
+ if (!deps || !deps.length) {
13291
+ return `Nenhum campo pai selecionado para ${this.disambiguatedLabelFor(targetField)} (${targetField}).`;
13292
+ }
13293
+ const parents = deps.map((d) => `${this.disambiguatedLabelFor(d)} (${d})`).join(' + ');
13294
+ return `${parents} → filtra opções de ${this.disambiguatedLabelFor(targetField)} (${targetField})`;
13295
+ }
13296
+ formatLoadOnLabel(policy) {
13297
+ switch (policy) {
13298
+ case 'immediate':
13299
+ return 'Imediato ao mudar o pai (immediate)';
13300
+ case 'manual':
13301
+ return 'Manual / sob demanda (manual)';
13302
+ default:
13303
+ return 'Seguir configuração do campo (respectLoadOn)';
13304
+ }
13305
+ }
13306
+ formatMergeStrategyLabel(strategy) {
13307
+ return strategy === 'replace'
13308
+ ? 'Substituir filtros (replace)'
13309
+ : 'Mesclar filtros (merge)';
13310
+ }
12284
13311
  selectedField() {
12285
13312
  return this.selectedFieldSig();
12286
13313
  }
12287
13314
  selectField(name) {
12288
13315
  this.selectedFieldSig.set(name);
12289
13316
  this.startEdit(name);
13317
+ this.syncValidityState();
12290
13318
  }
12291
13319
  hasRule(name) {
12292
- return this.rules().has(name);
13320
+ return (this.rules().get(name)?.dependencyFields?.length ?? 0) > 0;
12293
13321
  }
12294
13322
  ruleFor(name) {
12295
13323
  return this.rules().get(name) || null;
@@ -12305,6 +13333,52 @@ class CascadeManagerTabComponent {
12305
13333
  return false;
12306
13334
  return this.connections.some((c) => c.toFieldName === name && (c.toInputPath || '').includes('filterCriteria'));
12307
13335
  }
13336
+ /**
13337
+ * Computes graph validation combining saved rules with any active draft in the inline editor.
13338
+ */
13339
+ validateCurrentGraph(includeDraft = true) {
13340
+ const effectiveRules = new Map(this.rules());
13341
+ const sel = this.selectedField();
13342
+ if (includeDraft && this.editing() && sel) {
13343
+ const draftDeps = (this.editDepList || []).filter(Boolean);
13344
+ if (draftDeps.length > 0) {
13345
+ effectiveRules.set(sel, {
13346
+ targetField: sel,
13347
+ dependencyFields: draftDeps,
13348
+ enableDependencyCascade: this.editEnable,
13349
+ resetOnDependentChange: this.editReset,
13350
+ dependencyFilterMap: {},
13351
+ dependencyValuePath: this.editValuePath || 'id',
13352
+ dependencyMergeStrategy: this.editMerge,
13353
+ dependencyDebounceMs: this.editDebounce ?? 150,
13354
+ dependencyLoadOnChange: this.editLoadOn,
13355
+ });
13356
+ }
13357
+ else {
13358
+ effectiveRules.delete(sel);
13359
+ }
13360
+ }
13361
+ return this.svc.validateCascadeGraph(effectiveRules, this.fields);
13362
+ }
13363
+ activeCycleDiagnostics() {
13364
+ return this.validateCurrentGraph(true).cycles;
13365
+ }
13366
+ isFieldInCycle(name) {
13367
+ return this.validateCurrentGraph(true).fieldsInCycles.has(name);
13368
+ }
13369
+ hasBlockingErrors() {
13370
+ return !this.validateCurrentGraph(true).valid;
13371
+ }
13372
+ syncValidityState() {
13373
+ const isValid = this.validateCurrentGraph(true).valid;
13374
+ if (this.lastEmittedValid !== isValid) {
13375
+ this.lastEmittedValid = isValid;
13376
+ this.validChange.emit(isValid);
13377
+ }
13378
+ }
13379
+ onDraftControlChange() {
13380
+ this.syncValidityState();
13381
+ }
12308
13382
  startEdit(name) {
12309
13383
  const existing = this.ruleFor(name);
12310
13384
  const base = existing || {
@@ -12327,35 +13401,69 @@ class CascadeManagerTabComponent {
12327
13401
  this.editLoadOn = suggested.dependencyLoadOnChange || 'respectLoadOn';
12328
13402
  this.editDebounce = suggested.dependencyDebounceMs ?? 150;
12329
13403
  this.editValuePath = (typeof suggested.dependencyValuePath === 'string' ? suggested.dependencyValuePath : 'id') || '';
12330
- try {
12331
- this.editFilterMapJson = suggested.dependencyFilterMap ? JSON.stringify(suggested.dependencyFilterMap, null, 2) : '';
12332
- }
12333
- catch {
12334
- this.editFilterMapJson = '';
12335
- }
13404
+ this.editInitialFilterMap = structuredClone(suggested.dependencyFilterMap || {});
13405
+ this.editInitialValuePath = structuredClone(suggested.dependencyValuePath || 'id');
13406
+ this.editMapRows = [];
12336
13407
  // Build mapping rows from dependencies
12337
13408
  this.buildMapRowsFromDeps();
12338
13409
  this.editing.set(true);
13410
+ this.syncValidityState();
12339
13411
  }
12340
13412
  edit(name) { this.startEdit(name); }
12341
- cancelEdit() { this.editing.set(false); }
13413
+ cancelEdit() {
13414
+ const sel = this.selectedField();
13415
+ if (sel) {
13416
+ // Restore draft from committed rule before closing inline edit
13417
+ const existing = this.ruleFor(sel);
13418
+ this.editDepList = existing ? [...existing.dependencyFields] : [];
13419
+ this.editEnable = existing ? existing.enableDependencyCascade : true;
13420
+ }
13421
+ this.editing.set(false);
13422
+ this.syncValidityState();
13423
+ }
12342
13424
  saveEdit(name) {
12343
- // Build rule from editor
13425
+ // Build rule from editor (preserving self-reference prevention)
12344
13426
  const deps = this.editDepList.filter((s) => !!s && s !== name);
12345
- // Build map from rows; fallback to JSON when provided
12346
- let map = {};
12347
- this.editMapRows.forEach((r) => { if (!r.key && r.dep)
12348
- r.key = r.dep; if (r.dep)
12349
- map[r.dep] = r.valuePath ? { key: r.key || r.dep, valuePath: r.valuePath } : (r.key || r.dep); });
12350
- const raw = (this.editFilterMapJson || '').trim();
12351
- if (raw) {
12352
- try {
12353
- const fromJson = JSON.parse(raw);
12354
- map = { ...map, ...fromJson };
13427
+ this.editDepList = [...deps];
13428
+ // The rows own the draft. The opening snapshot only preserves entry shape;
13429
+ // merging the original JSON after the rows would silently undo visual edits.
13430
+ const map = structuredClone(this.editInitialFilterMap);
13431
+ const dependenciesRemoved = (this.ruleFor(name)?.dependencyFields || [])
13432
+ .some((dep) => !deps.includes(dep));
13433
+ if (dependenciesRemoved) {
13434
+ for (const dep of Object.keys(map)) {
13435
+ if (!deps.includes(dep))
13436
+ delete map[dep];
12355
13437
  }
12356
- catch {
12357
- alert('Mapeamento de filtros inválido (JSON)');
12358
- return;
13438
+ }
13439
+ for (const row of this.editMapRows) {
13440
+ if (!deps.includes(row.dep))
13441
+ continue;
13442
+ const initial = this.editInitialFilterMap[row.dep];
13443
+ const initialPath = this.initialRowValuePath(row.dep);
13444
+ if (typeof initial === 'object' && initial !== null) {
13445
+ const entry = { ...initial, key: row.key || row.dep };
13446
+ if (row.valuePath !== initialPath) {
13447
+ if (row.valuePath)
13448
+ entry.valuePath = row.valuePath;
13449
+ else
13450
+ delete entry.valuePath;
13451
+ }
13452
+ map[row.dep] = entry;
13453
+ }
13454
+ else {
13455
+ map[row.dep] = row.valuePath && row.valuePath !== initialPath
13456
+ ? { key: row.key || row.dep, valuePath: row.valuePath }
13457
+ : row.key || row.dep;
13458
+ }
13459
+ }
13460
+ const valuePaths = this.buildValuePathOverrides(deps);
13461
+ // Removed dependencies must lose their extraction overrides as well as their
13462
+ // filter mappings, otherwise re-adding one resurrects stale extraction state.
13463
+ if (dependenciesRemoved && typeof valuePaths === 'object') {
13464
+ for (const dep of Object.keys(valuePaths)) {
13465
+ if (!deps.includes(dep))
13466
+ delete valuePaths[dep];
12359
13467
  }
12360
13468
  }
12361
13469
  const rule = {
@@ -12364,25 +13472,67 @@ class CascadeManagerTabComponent {
12364
13472
  enableDependencyCascade: this.editEnable,
12365
13473
  resetOnDependentChange: this.editReset,
12366
13474
  dependencyFilterMap: map,
12367
- dependencyValuePath: this.buildValuePathOverrides(deps),
13475
+ dependencyValuePath: valuePaths,
12368
13476
  dependencyMergeStrategy: this.editMerge,
12369
13477
  dependencyDebounceMs: this.editDebounce ?? 150,
12370
13478
  dependencyLoadOnChange: this.editLoadOn,
12371
13479
  };
12372
13480
  const next = new Map(this.rules());
12373
13481
  next.set(name, rule);
13482
+ const validation = this.svc.validateCascadeGraph(next, this.fields);
13483
+ if (validation.fieldsInCycles.has(name)) {
13484
+ const fieldCycle = validation.cycles.find((c) => c.path.includes(name)) || validation.cycles[0];
13485
+ this.setFeedback(fieldCycle.message, 'error');
13486
+ this.syncValidityState();
13487
+ return;
13488
+ }
13489
+ if (!validation.valid) {
13490
+ this.setFeedback(validation.cycles[0].message, 'error');
13491
+ }
13492
+ else {
13493
+ this.setFeedback('');
13494
+ }
12374
13495
  this.rules.set(next);
12375
13496
  this.emitPatch();
12376
13497
  this.editing.set(false);
13498
+ this.syncValidityState();
13499
+ }
13500
+ /**
13501
+ * Removes cyclic dependencies from the given field's draft (and saved rule if present).
13502
+ */
13503
+ breakCycleForField(name) {
13504
+ const currentValidation = this.validateCurrentGraph(true);
13505
+ if (!currentValidation.fieldsInCycles.has(name)) {
13506
+ return;
13507
+ }
13508
+ const cyclicTargets = new Set([name]);
13509
+ for (const cycle of currentValidation.cycles) {
13510
+ for (let i = 0; i < cycle.path.length - 1; i++) {
13511
+ if (cycle.path[i] === name) {
13512
+ cyclicTargets.add(cycle.path[i + 1]);
13513
+ }
13514
+ }
13515
+ }
13516
+ this.editDepList = (this.editDepList || []).filter((dep) => !cyclicTargets.has(dep));
13517
+ this.onDepListChange();
13518
+ if (this.rules().has(name)) {
13519
+ this.saveEdit(name);
13520
+ }
12377
13521
  }
12378
13522
  remove(name) {
12379
13523
  const next = new Map(this.rules());
12380
13524
  next.delete(name);
12381
13525
  this.rules.set(next);
13526
+ if (this.selectedField() === name) {
13527
+ this.editDepList = [];
13528
+ this.editMapRows = [];
13529
+ }
12382
13530
  const patch = {
12383
13531
  [name]: this.svc.clearPatch(),
12384
13532
  };
13533
+ this.setFeedback('');
12385
13534
  this.apply.emit(patch);
13535
+ this.syncValidityState();
12386
13536
  }
12387
13537
  quickSet(name, action) {
12388
13538
  const current = this.ruleFor(name);
@@ -12397,6 +13547,7 @@ class CascadeManagerTabComponent {
12397
13547
  map.set(name, next);
12398
13548
  this.rules.set(map);
12399
13549
  this.emitPatch();
13550
+ this.syncValidityState();
12400
13551
  }
12401
13552
  emitPatch() {
12402
13553
  const out = {};
@@ -12436,7 +13587,7 @@ class CascadeManagerTabComponent {
12436
13587
  navigator.clipboard?.writeText(json);
12437
13588
  }
12438
13589
  catch { }
12439
- alert('Regras exportadas para a área de transferência.');
13590
+ this.setFeedback('Regras exportadas para a área de transferência.', 'info');
12440
13591
  }
12441
13592
  applyImport() {
12442
13593
  let obj;
@@ -12444,9 +13595,10 @@ class CascadeManagerTabComponent {
12444
13595
  obj = JSON.parse(this.importJson || '{}');
12445
13596
  }
12446
13597
  catch {
12447
- alert('JSON inválido');
13598
+ this.setFeedback('JSON inválido na importação de regras.', 'error');
12448
13599
  return;
12449
13600
  }
13601
+ this.setFeedback('');
12450
13602
  const next = new Map(this.rules());
12451
13603
  Object.keys(obj || {}).forEach((field) => {
12452
13604
  const patch = obj[field] || {};
@@ -12463,14 +13615,26 @@ class CascadeManagerTabComponent {
12463
13615
  };
12464
13616
  next.set(field, vm);
12465
13617
  });
13618
+ const validation = this.svc.validateCascadeGraph(next, this.fields);
12466
13619
  this.rules.set(next);
13620
+ const sel = this.selectedField();
13621
+ if (sel && next.has(sel)) {
13622
+ this.startEdit(sel);
13623
+ }
13624
+ if (!validation.valid) {
13625
+ this.setFeedback(validation.cycles[0].message, 'error');
13626
+ this.syncValidityState();
13627
+ return;
13628
+ }
12467
13629
  this.emitPatch();
12468
13630
  this.importMode = false;
13631
+ this.syncValidityState();
12469
13632
  }
12470
13633
  startTemplate(name, tpl) {
12471
13634
  const hasR = this.hasField('rotaId');
12472
13635
  const hasD = this.hasField('destinoReqId');
12473
- const deps = tpl === 'rota-destino' ? (hasR ? ['rotaId'] : []) : (hasR && hasD ? ['rotaId', 'destinoReqId'] : []);
13636
+ const deps = (tpl === 'rota-destino' ? (hasR ? ['rotaId'] : []) : (hasR && hasD ? ['rotaId', 'destinoReqId'] : []))
13637
+ .filter((dep) => dep !== name);
12474
13638
  this.editDepList = [...deps];
12475
13639
  this.editEnable = true;
12476
13640
  this.editReset = true;
@@ -12478,40 +13642,73 @@ class CascadeManagerTabComponent {
12478
13642
  this.editLoadOn = 'respectLoadOn';
12479
13643
  this.editDebounce = 150;
12480
13644
  this.editValuePath = 'id';
13645
+ this.editInitialFilterMap = {};
13646
+ this.editInitialValuePath = 'id';
13647
+ this.editMapRows = [];
12481
13648
  this.buildMapRowsFromDeps();
12482
13649
  this.editing.set(true);
13650
+ this.syncValidityState();
12483
13651
  }
12484
13652
  buildMapRowsFromDeps() {
12485
13653
  const deps = this.editDepList;
12486
- const existing = (() => { try {
12487
- return JSON.parse(this.editFilterMapJson || '{}');
12488
- }
12489
- catch {
12490
- return {};
12491
- } })();
13654
+ const current = new Map(this.editMapRows.map((row) => [row.dep, row]));
12492
13655
  this.editMapRows = deps.map((dep) => {
12493
- const entry = existing[dep];
12494
- if (!entry)
12495
- return { dep, key: dep };
12496
- if (typeof entry === 'string')
12497
- return { dep, key: entry };
12498
- return { dep, key: entry.key || dep, valuePath: entry.valuePath };
13656
+ const edited = current.get(dep);
13657
+ if (edited)
13658
+ return edited;
13659
+ const entry = this.editInitialFilterMap[dep];
13660
+ const key = typeof entry === 'string' ? entry : entry?.key || dep;
13661
+ return { dep, key, valuePath: this.initialRowValuePath(dep) };
12499
13662
  });
12500
13663
  }
13664
+ initialRowValuePath(dep) {
13665
+ const entry = this.editInitialFilterMap[dep];
13666
+ if (typeof entry === 'object' && entry?.valuePath)
13667
+ return entry.valuePath;
13668
+ return typeof this.editInitialValuePath === 'object'
13669
+ ? this.editInitialValuePath[dep]
13670
+ : undefined;
13671
+ }
12501
13672
  buildValuePathOverrides(deps) {
12502
- // If any row defines a valuePath, return a record; else return default string
12503
- const record = {};
12504
- this.editMapRows.forEach((r) => { if (r.valuePath)
12505
- record[r.dep] = r.valuePath; });
12506
- const hasAny = Object.keys(record).length > 0;
12507
- return hasAny ? record : (this.editValuePath || 'id');
13673
+ const initialDefault = typeof this.editInitialValuePath === 'string'
13674
+ ? this.editInitialValuePath
13675
+ : 'id';
13676
+ const defaultChanged = this.editValuePath !== initialDefault;
13677
+ const pathsChanged = this.editMapRows.some((row) => row.valuePath !== this.initialRowValuePath(row.dep));
13678
+ if (!defaultChanged && !pathsChanged)
13679
+ return structuredClone(this.editInitialValuePath);
13680
+ const wasRecord = typeof this.editInitialValuePath === 'object';
13681
+ if (!pathsChanged && !wasRecord)
13682
+ return this.editValuePath || 'id';
13683
+ // A specific extraction path must not erase the default/overrides used by
13684
+ // other dependencies. Unchanged documents keep their original shape above.
13685
+ const record = typeof this.editInitialValuePath === 'object'
13686
+ ? { ...this.editInitialValuePath }
13687
+ : {};
13688
+ for (const dep of deps) {
13689
+ const row = this.editMapRows.find((candidate) => candidate.dep === dep);
13690
+ if (row && row.valuePath !== this.initialRowValuePath(dep)) {
13691
+ record[dep] = row.valuePath || this.editValuePath || 'id';
13692
+ }
13693
+ else if (typeof this.editInitialValuePath === 'object') {
13694
+ const existing = this.editInitialValuePath[dep];
13695
+ if (existing !== undefined)
13696
+ record[dep] = existing;
13697
+ else if (defaultChanged)
13698
+ record[dep] = this.editValuePath || 'id';
13699
+ }
13700
+ else {
13701
+ record[dep] = this.editValuePath || 'id';
13702
+ }
13703
+ }
13704
+ return record;
12508
13705
  }
12509
13706
  buildPreview() {
12510
13707
  try {
12511
- const deps = this.editDepList;
12512
13708
  const frag = {};
12513
13709
  this.editMapRows.forEach((r) => {
12514
- const example = r.valuePath ? 123 : 123; // simple placeholder id
13710
+ const extractedProp = r.valuePath || this.editValuePath || 'id';
13711
+ const example = `<valor de ${r.dep}.${extractedProp}>`;
12515
13712
  const parts = (r.key || r.dep).split('.');
12516
13713
  let ref = frag;
12517
13714
  for (let i = 0; i < parts.length - 1; i++) {
@@ -12536,10 +13733,14 @@ class CascadeManagerTabComponent {
12536
13733
  const labelMap = this.svc.getFieldLabelMap(this.fields);
12537
13734
  return this.fields
12538
13735
  .filter((f) => f.name !== current)
12539
- .sort((a, b) => (labelMap.get(a.name) || a.name).localeCompare(labelMap.get(b.name) || b.name));
13736
+ .sort((a, b) => {
13737
+ const labelCmp = (labelMap.get(a.name) || a.name).localeCompare(labelMap.get(b.name) || b.name);
13738
+ return labelCmp !== 0 ? labelCmp : a.name.localeCompare(b.name);
13739
+ });
12540
13740
  }
12541
13741
  onDepListChange() {
12542
13742
  this.buildMapRowsFromDeps();
13743
+ this.syncValidityState();
12543
13744
  }
12544
13745
  getKeySuggestions(dep) {
12545
13746
  const suggestions = [dep, `${dep}.id`, `${dep}.value`];
@@ -12559,7 +13760,7 @@ class CascadeManagerTabComponent {
12559
13760
  return [...new Set(suggestions)];
12560
13761
  }
12561
13762
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: CascadeManagerTabComponent, deps: [{ token: CascadeRulesService }], target: i0.ɵɵFactoryTarget.Component });
12562
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: CascadeManagerTabComponent, isStandalone: true, selector: "praxis-cascade-manager-tab", inputs: { fields: "fields", connections: "connections" }, outputs: { apply: "apply", cancel: "cancel" }, usesOnChanges: true, ngImport: i0, template: `
13763
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: CascadeManagerTabComponent, isStandalone: true, selector: "praxis-cascade-manager-tab", inputs: { fields: "fields", connections: "connections" }, outputs: { apply: "apply", cancel: "cancel", validChange: "validChange" }, usesOnChanges: true, ngImport: i0, template: `
12563
13764
  <div class="cm-root">
12564
13765
  <!-- Left: Fields list -->
12565
13766
  <div class="cm-left">
@@ -12569,14 +13770,29 @@ class CascadeManagerTabComponent {
12569
13770
  </mat-form-field>
12570
13771
  @if (fields.length <= 50) {
12571
13772
  <div>
12572
- @for (f of filteredFields(); track f) {
12573
- <div (click)="selectField(f.name)" class="field-item" [class.selected]="f.name===selectedField()" tabindex="0" (keydown)="onFieldKeydown($event, f.name)">
12574
- <div>
12575
- <div>{{ labelFor(f.name) }}</div>
12576
- <small style="opacity:.7">{{ f.name }}</small>
13773
+ @for (f of filteredFields(); track f.name) {
13774
+ <div
13775
+ (click)="selectField(f.name)"
13776
+ class="field-item"
13777
+ [class.selected]="f.name===selectedField()"
13778
+ [class.has-cycle]="isFieldInCycle(f.name)"
13779
+ tabindex="0"
13780
+ (keydown)="onFieldKeydown($event, f.name)"
13781
+ >
13782
+ <div class="field-item-main">
13783
+ <div class="field-item-title">{{ disambiguatedLabelFor(f.name) }}</div>
13784
+ <div class="field-item-meta">
13785
+ <span>{{ f.name }}</span>
13786
+ <span class="field-role-pill" [class.field-role-pill--remote]="hasRemoteOptionSource(f.name)">
13787
+ {{ fieldRoleBadge(f.name) }}
13788
+ </span>
13789
+ </div>
12577
13790
  </div>
12578
- @if (hasRule(f.name)) {
12579
- <div class="rule-badges">
13791
+ <div class="rule-badges">
13792
+ @if (isFieldInCycle(f.name)) {
13793
+ <span class="rule-badge b-conflict">Ciclo</span>
13794
+ }
13795
+ @if (hasRule(f.name)) {
12580
13796
  @if (isCascadeEnabled(f.name)) {
12581
13797
  <span class="rule-badge b-active">Ativo</span>
12582
13798
  }
@@ -12584,22 +13800,38 @@ class CascadeManagerTabComponent {
12584
13800
  <span class="rule-badge b-manual">Manual</span>
12585
13801
  }
12586
13802
  @if (loadOnChange(f.name)==='immediate') {
12587
- <span class="rule-badge b-immediate">Immediate</span>
13803
+ <span class="rule-badge b-immediate">Imediato</span>
12588
13804
  }
12589
- </div>
12590
- }
13805
+ }
13806
+ </div>
12591
13807
  </div>
12592
13808
  }
12593
13809
  </div>
12594
13810
  } @else {
12595
- <cdk-virtual-scroll-viewport itemSize="44" style="height: calc(100vh - 240px)">
12596
- <div *cdkVirtualFor="let f of filteredFields()" (click)="selectField(f.name)" class="field-item" [class.selected]="f.name===selectedField()" tabindex="0" (keydown)="onFieldKeydown($event, f.name)">
12597
- <div>
12598
- <div>{{ labelFor(f.name) }}</div>
12599
- <small style="opacity:.7">{{ f.name }}</small>
13811
+ <cdk-virtual-scroll-viewport itemSize="48" style="height: calc(100vh - 240px)">
13812
+ <div
13813
+ *cdkVirtualFor="let f of filteredFields()"
13814
+ (click)="selectField(f.name)"
13815
+ class="field-item"
13816
+ [class.selected]="f.name===selectedField()"
13817
+ [class.has-cycle]="isFieldInCycle(f.name)"
13818
+ tabindex="0"
13819
+ (keydown)="onFieldKeydown($event, f.name)"
13820
+ >
13821
+ <div class="field-item-main">
13822
+ <div class="field-item-title">{{ disambiguatedLabelFor(f.name) }}</div>
13823
+ <div class="field-item-meta">
13824
+ <span>{{ f.name }}</span>
13825
+ <span class="field-role-pill" [class.field-role-pill--remote]="hasRemoteOptionSource(f.name)">
13826
+ {{ fieldRoleBadge(f.name) }}
13827
+ </span>
13828
+ </div>
12600
13829
  </div>
12601
- @if (hasRule(f.name)) {
12602
- <div class="rule-badges">
13830
+ <div class="rule-badges">
13831
+ @if (isFieldInCycle(f.name)) {
13832
+ <span class="rule-badge b-conflict">Ciclo</span>
13833
+ }
13834
+ @if (hasRule(f.name)) {
12603
13835
  @if (isCascadeEnabled(f.name)) {
12604
13836
  <span class="rule-badge b-active">Ativo</span>
12605
13837
  }
@@ -12607,27 +13839,27 @@ class CascadeManagerTabComponent {
12607
13839
  <span class="rule-badge b-manual">Manual</span>
12608
13840
  }
12609
13841
  @if (loadOnChange(f.name)==='immediate') {
12610
- <span class="rule-badge b-immediate">Immediate</span>
13842
+ <span class="rule-badge b-immediate">Imediato</span>
12611
13843
  }
12612
- </div>
12613
- }
13844
+ }
13845
+ </div>
12614
13846
  </div>
12615
13847
  </cdk-virtual-scroll-viewport>
12616
13848
  }
12617
13849
  </div>
12618
-
13850
+
12619
13851
  <!-- Right: Rules for selected field -->
12620
13852
  @if (selectedField(); as sel) {
12621
13853
  <div class="cm-right">
12622
13854
  <div class="cm-header">
12623
13855
  <div>
12624
- <h3 style="margin:0">{{ labelFor(sel) }}</h3>
12625
- <div style="opacity:.7">{{ sel }}</div>
13856
+ <h3 style="margin:0">{{ disambiguatedLabelFor(sel) }}</h3>
13857
+ <div style="opacity:.75; font-size:0.85rem">{{ sel }} · {{ fieldRoleSummary(sel) }}</div>
12626
13858
  </div>
12627
13859
  <div>
12628
13860
  <button mat-stroked-button color="primary" (click)="exportRules()" style="margin-right:8px">Exportar</button>
12629
13861
  <button mat-stroked-button (click)="toggleImport()" style="margin-right:8px">Importar</button>
12630
- <button mat-flat-button color="primary" [matMenuTriggerFor]="addMenu" [disabled]="editing()">Adicionar</button>
13862
+ <button mat-flat-button color="primary" [matMenuTriggerFor]="addMenu">Modelos</button>
12631
13863
  <mat-menu #addMenu="matMenu">
12632
13864
  <button mat-menu-item (click)="startEdit(sel)"><mat-icon [praxisIcon]="'add'"></mat-icon><span>Em branco</span></button>
12633
13865
  <button mat-menu-item (click)="startTemplate(sel, 'rota-destino')" [disabled]="!hasField('rotaId') || !hasField('destinoReqId')"><mat-icon [praxisIcon]="'bolt'"></mat-icon><span>Rota → Destino (id)</span></button>
@@ -12635,6 +13867,45 @@ class CascadeManagerTabComponent {
12635
13867
  </mat-menu>
12636
13868
  </div>
12637
13869
  </div>
13870
+
13871
+ @if (activeCycleDiagnostics().length > 0) {
13872
+ <div class="cm-cycle-alert" role="alert" data-testid="cascade-cycle-diagnostic">
13873
+ <mat-icon [praxisIcon]="'error'"></mat-icon>
13874
+ <div class="cm-cycle-alert-body">
13875
+ <strong>Dependência circular detectada — não é possível aplicar enquanto houver ciclo</strong>
13876
+ @for (cycle of activeCycleDiagnostics(); track cycle.formattedPath) {
13877
+ <div>{{ cycle.message }}</div>
13878
+ }
13879
+ @if (isFieldInCycle(sel)) {
13880
+ <div style="margin-top: 6px;">
13881
+ <button
13882
+ mat-stroked-button
13883
+ type="button"
13884
+ data-testid="cascade-break-cycle-button"
13885
+ (click)="breakCycleForField(sel)"
13886
+ >
13887
+ Remover dependência circular deste campo
13888
+ </button>
13889
+ </div>
13890
+ }
13891
+ </div>
13892
+ </div>
13893
+ }
13894
+
13895
+ @if (feedbackMessage) {
13896
+ <div class="cm-feedback" [class.cm-feedback--error]="feedbackTone === 'error'" role="status">{{ feedbackMessage }}</div>
13897
+ }
13898
+
13899
+ @if (!hasRemoteOptionSource(sel)) {
13900
+ <div class="cm-source-notice" role="note" data-testid="cascade-no-remote-source-notice">
13901
+ <mat-icon [praxisIcon]="'info'"></mat-icon>
13902
+ <div>
13903
+ Este campo ({{ fieldRoleBadge(sel) }}) não declara fonte de opções remota (<code class="kbd">resourcePath</code> / <code class="kbd">optionSource</code>).
13904
+ Campos de texto ou nomes resolvidos exibem valores derivados; para recarregar opções em cascata, selecione o campo selecionável (ex.: ID) correspondente.
13905
+ </div>
13906
+ </div>
13907
+ }
13908
+
12638
13909
  <!-- Conflict banner (connections) -->
12639
13910
  @if (hasConflict(sel)) {
12640
13911
  <div class="banner">
@@ -12647,43 +13918,56 @@ class CascadeManagerTabComponent {
12647
13918
  <button mat-stroked-button color="accent" (click)="quickSet(sel,'manual')">Definir manual</button>
12648
13919
  </div>
12649
13920
  }
13921
+
12650
13922
  @if (importMode) {
12651
13923
  <div class="import-panel">
12652
13924
  <div style="display:flex; gap:8px; align-items:center; margin-bottom:8px"><mat-icon [praxisIcon]="'upload'"></mat-icon><strong>Importar regras (JSON)</strong></div>
12653
- <textarea matInput rows="6" class="full" [(ngModel)]="importJson" placeholder='{ "campo": { "dependencyFields": ["dep"], ... } }'></textarea>
13925
+ <textarea rows="6" class="import-textarea" [(ngModel)]="importJson" placeholder='{ "campo": { "dependencyFields": ["dep"], ... } }'></textarea>
12654
13926
  <div style="display:flex; gap:8px; justify-content:flex-end; padding-top:8px">
12655
13927
  <button mat-stroked-button (click)="toggleImport()">Cancelar</button>
12656
13928
  <button mat-flat-button color="primary" (click)="applyImport()">Aplicar</button>
12657
13929
  </div>
12658
13930
  </div>
12659
13931
  }
12660
- @if (!ruleFor(sel)) {
13932
+
13933
+ @if (!hasRule(sel)) {
12661
13934
  <div>
12662
- <div class="rules-empty">Nenhuma cascata configurada. Clique em “Adicionar” para montar a dependência.</div>
13935
+ <div class="rules-empty" data-testid="cascade-empty-guidance">
13936
+ Nenhuma cascata salva para <strong>{{ disambiguatedLabelFor(sel) }}</strong>.
13937
+ Selecione abaixo em <strong>“Este campo depende de (campos pais)”</strong> quais campos controlam as opções deste campo e clique em <strong>Salvar</strong>.
13938
+ </div>
12663
13939
  </div>
12664
13940
  }
12665
- @if (ruleFor(sel); as rule) {
13941
+ @if (hasRule(sel) && ruleFor(sel); as rule) {
12666
13942
  <div>
12667
13943
  <div class="rule-row">
12668
13944
  <div>
12669
- <div><strong>Dependentes:</strong> {{ rule.dependencyFields.join(', ') || '—' }}</div>
12670
- <div style="opacity:.7">Load on change: {{ rule.dependencyLoadOnChange || 'respectLoadOn' }} | Merge: {{ rule.dependencyMergeStrategy || 'merge' }}</div>
13945
+ <div><strong>Depende de (campos pais):</strong> {{ formatParentList(rule.dependencyFields) }}</div>
13946
+ <div class="rule-summary-direction">
13947
+ <strong>Fluxo:</strong> {{ formatRelationshipSummary(sel, rule.dependencyFields) }}
13948
+ </div>
13949
+ <div class="rule-summary-policies">
13950
+ Quando recarregar: {{ formatLoadOnLabel(rule.dependencyLoadOnChange) }} | Como combinar filtros: {{ formatMergeStrategyLabel(rule.dependencyMergeStrategy) }}
13951
+ </div>
12671
13952
  </div>
12672
13953
  <div class="rule-actions">
12673
- <button mat-icon-button matTooltip="Editar" (click)="startEdit(sel)"><mat-icon [praxisIcon]="'edit'"></mat-icon></button>
12674
- <button mat-icon-button matTooltip="Duplicar" disabled><mat-icon [praxisIcon]="'content_copy'"></mat-icon></button>
12675
- <button mat-icon-button matTooltip="Remover" (click)="remove(sel)"><mat-icon [praxisIcon]="'delete'"></mat-icon></button>
13954
+ <button mat-icon-button type="button" aria-label="Editar regra de cascata" matTooltip="Editar" (click)="startEdit(sel)"><mat-icon [praxisIcon]="'edit'"></mat-icon></button>
13955
+ <button mat-icon-button type="button" aria-label="Duplicar regra de cascata" matTooltip="Duplicar" disabled><mat-icon [praxisIcon]="'content_copy'"></mat-icon></button>
13956
+ <button mat-icon-button type="button" aria-label="Remover regra de cascata" matTooltip="Remover" (click)="remove(sel)"><mat-icon [praxisIcon]="'delete'"></mat-icon></button>
12676
13957
  </div>
12677
13958
  </div>
12678
13959
  </div>
12679
13960
  }
13961
+
12680
13962
  <!-- Editor com seções (sem acordeão duplo) -->
12681
13963
  <div class="panel-surface">
12682
- <div class="panel-header" style="padding: 12px 16px; border-bottom: 1px solid var(--md-sys-color-outline-variant);">
13964
+ <div class="panel-header" style="padding: 8px 4px; border-bottom: 1px solid var(--pfx-settings-panel-border, var(--md-sys-color-outline-variant));">
12683
13965
  <mat-icon [praxisIcon]="'rule'"></mat-icon>
12684
13966
  <div>
12685
13967
  <div>Cascata do campo</div>
12686
- <small style="color: var(--md-sys-color-on-surface-variant);">Defina dependentes, quando recarregar e como mapear filtros</small>
13968
+ <small style="color: var(--md-sys-color-on-surface-variant);">
13969
+ Defina de quais campos pais este campo depende, quando recarregar opções e qual filtro enviar ao serviço
13970
+ </small>
12687
13971
  </div>
12688
13972
  </div>
12689
13973
  <div class="panel-stack">
@@ -12693,13 +13977,16 @@ class CascadeManagerTabComponent {
12693
13977
  <mat-panel-title class="panel-header"><mat-icon [praxisIcon]="'tune'"></mat-icon> Básico</mat-panel-title>
12694
13978
  </mat-expansion-panel-header>
12695
13979
  <div class="form-grid">
13980
+ <p class="section-caption">
13981
+ Quando o usuário alterar qualquer um dos campos pais escolhidos abaixo, as opções disponíveis em <strong>{{ disambiguatedLabelFor(sel) }}</strong> serão filtradas.
13982
+ </p>
12696
13983
  <div class="form-row">
12697
13984
  <mat-form-field appearance="outline" class="full">
12698
- <mat-label>Campos dependentes</mat-label>
13985
+ <mat-label>Este campo depende de (campos pais)</mat-label>
12699
13986
  <mat-select multiple [(ngModel)]="editDepList" (ngModelChange)="onDepListChange()">
12700
- @for (f of availableDependencyFields(); track f) {
13987
+ @for (f of availableDependencyFields(); track f.name) {
12701
13988
  <mat-option [value]="f.name">
12702
- {{ labelFor(f.name) }} ({{ f.name }})
13989
+ {{ disambiguatedLabelFor(f.name) }} ({{ f.name }}) · {{ fieldRoleBadge(f.name) }}
12703
13990
  </mat-option>
12704
13991
  }
12705
13992
  </mat-select>
@@ -12708,42 +13995,54 @@ class CascadeManagerTabComponent {
12708
13995
  matSuffix
12709
13996
  class="help-icon-button"
12710
13997
  type="button"
12711
- [matTooltip]="'Selecione os campos que controlam este valor'"
13998
+ [matTooltip]="'Selecione os campos pais (origem) cujo valor filtrará as opções deste campo (destino)'"
12712
13999
  matTooltipPosition="above"
12713
14000
  >
12714
14001
  <mat-icon [praxisIcon]="'help_outline'"></mat-icon>
12715
14002
  </button>
12716
14003
  </mat-form-field>
12717
14004
  </div>
14005
+ @if (editDepList.length > 0) {
14006
+ <div class="section-caption" data-testid="cascade-draft-direction-summary">
14007
+ <strong>Resumo da relação:</strong> {{ formatRelationshipSummary(sel, editDepList) }}
14008
+ </div>
14009
+ }
12718
14010
  <div class="form-row">
12719
- <mat-checkbox [(ngModel)]="editEnable">Ativar cascata nativa</mat-checkbox>
12720
- <mat-checkbox [(ngModel)]="editReset">Resetar ao mudar</mat-checkbox>
14011
+ <mat-checkbox [(ngModel)]="editEnable" (ngModelChange)="onDraftControlChange()">
14012
+ Ativar cascata automática neste campo
14013
+ </mat-checkbox>
14014
+ <mat-checkbox [(ngModel)]="editReset" (ngModelChange)="onDraftControlChange()">
14015
+ Limpar seleção deste campo quando o campo pai mudar
14016
+ </mat-checkbox>
12721
14017
  </div>
12722
14018
  </div>
12723
14019
  </mat-expansion-panel>
12724
14020
  <!-- Execução -->
12725
14021
  <mat-expansion-panel class="stack-panel">
12726
14022
  <mat-expansion-panel-header>
12727
- <mat-panel-title class="panel-header"><mat-icon [praxisIcon]="'schedule'"></mat-icon> Execução</mat-panel-title>
14023
+ <mat-panel-title class="panel-header"><mat-icon [praxisIcon]="'schedule'"></mat-icon> Quando recarregar e combinar filtros</mat-panel-title>
12728
14024
  </mat-expansion-panel-header>
14025
+ <p class="section-caption">
14026
+ Controle o momento em que a consulta de opções é disparada e como o filtro do campo pai convive com outros filtros do campo.
14027
+ </p>
12729
14028
  <div class="form-row">
12730
14029
  <mat-form-field appearance="outline">
12731
- <mat-label>Carregar ao mudar</mat-label>
14030
+ <mat-label>Quando recarregar opções</mat-label>
12732
14031
  <mat-select [(ngModel)]="editLoadOn">
12733
- <mat-option value="respectLoadOn">Respeitar loadOn</mat-option>
12734
- <mat-option value="immediate">Imediato</mat-option>
12735
- <mat-option value="manual">Manual</mat-option>
14032
+ <mat-option value="respectLoadOn">Seguir configuração do campo (respectLoadOn)</mat-option>
14033
+ <mat-option value="immediate">Recarregar imediatamente ao mudar o pai (immediate)</mat-option>
14034
+ <mat-option value="manual">Somente sob comando manual / abertura (manual)</mat-option>
12736
14035
  </mat-select>
12737
14036
  </mat-form-field>
12738
14037
  <mat-form-field appearance="outline">
12739
- <mat-label>Merge de filtros</mat-label>
14038
+ <mat-label>Como combinar filtros</mat-label>
12740
14039
  <mat-select [(ngModel)]="editMerge">
12741
- <mat-option value="merge">merge (padrão)</mat-option>
12742
- <mat-option value="replace">replace</mat-option>
14040
+ <mat-option value="merge">Mesclar com filtros existentes (merge — padrão)</mat-option>
14041
+ <mat-option value="replace">Substituir filtros anteriores (replace)</mat-option>
12743
14042
  </mat-select>
12744
14043
  </mat-form-field>
12745
14044
  <mat-form-field appearance="outline">
12746
- <mat-label>Atraso (ms)</mat-label>
14045
+ <mat-label>Atraso após mudança (ms)</mat-label>
12747
14046
  <input matInput type="number" [(ngModel)]="editDebounce">
12748
14047
  </mat-form-field>
12749
14048
  </div>
@@ -12751,18 +14050,21 @@ class CascadeManagerTabComponent {
12751
14050
  <!-- Extração de valor -->
12752
14051
  <mat-expansion-panel class="stack-panel">
12753
14052
  <mat-expansion-panel-header>
12754
- <mat-panel-title class="panel-header"><mat-icon [praxisIcon]="'call_split'"></mat-icon> Extração de valor</mat-panel-title>
14053
+ <mat-panel-title class="panel-header"><mat-icon [praxisIcon]="'call_split'"></mat-icon> Qual propriedade ler do campo pai</mat-panel-title>
12755
14054
  </mat-expansion-panel-header>
14055
+ <p class="section-caption">
14056
+ Se o campo pai guardar um objeto completo (ex.: <code class="kbd">&#123; id: 10, nome: "TI" &#125;</code>), informe qual propriedade deve ser extraída para envio no filtro (padrão: <code class="kbd">id</code>).
14057
+ </p>
12756
14058
  <div class="form-row">
12757
14059
  <mat-form-field appearance="outline" class="full">
12758
- <mat-label>ValuePath padrão</mat-label>
14060
+ <mat-label>Propriedade padrão lida do campo pai (ValuePath)</mat-label>
12759
14061
  <input matInput [(ngModel)]="editValuePath" placeholder="id">
12760
14062
  <button
12761
14063
  mat-icon-button
12762
14064
  matSuffix
12763
14065
  class="help-icon-button"
12764
14066
  type="button"
12765
- [matTooltip]="'Preenchido automaticamente a partir de optionValueKey() ou \\'id\\''"
14067
+ [matTooltip]="'Preenchido automaticamente a partir de optionValueKey do campo pai ou \\'id\\''"
12766
14068
  matTooltipPosition="above"
12767
14069
  >
12768
14070
  <mat-icon [praxisIcon]="'help_outline'"></mat-icon>
@@ -12773,24 +14075,30 @@ class CascadeManagerTabComponent {
12773
14075
  <!-- Mapeamento de filtros (tabela) -->
12774
14076
  <mat-expansion-panel class="stack-panel">
12775
14077
  <mat-expansion-panel-header>
12776
- <mat-panel-title class="panel-header"><mat-icon [praxisIcon]="'filter_alt'"></mat-icon> Mapeamento de filtros</mat-panel-title>
14078
+ <mat-panel-title class="panel-header"><mat-icon [praxisIcon]="'filter_alt'"></mat-icon> Mapeamento de filtros enviados ao serviço</mat-panel-title>
12777
14079
  </mat-expansion-panel-header>
12778
14080
  @if (editMapRows.length) {
14081
+ <p class="section-caption">
14082
+ Para cada campo pai, defina o nome do parâmetro esperado pelo serviço de opções deste campo e, se necessário, a propriedade específica a extrair.
14083
+ </p>
12779
14084
  <table class="map-table">
12780
14085
  <thead>
12781
14086
  <tr>
12782
- <th>Dependente</th>
12783
- <th>Chave (dot‑path)</th>
12784
- <th>ValuePath (opcional)</th>
14087
+ <th>Campo pai (origem)</th>
14088
+ <th>Parâmetro enviado ao filtro (dot‑path)</th>
14089
+ <th>Propriedade lida do pai (opcional)</th>
12785
14090
  </tr>
12786
14091
  </thead>
12787
14092
  <tbody>
12788
- @for (row of editMapRows; track row; let i = $index) {
14093
+ @for (row of editMapRows; track row.dep; let i = $index) {
12789
14094
  <tr>
12790
- <td>{{ row.dep }}</td>
14095
+ <td>
14096
+ <div><strong>{{ disambiguatedLabelFor(row.dep) }}</strong></div>
14097
+ <small style="opacity:.75">{{ row.dep }}</small>
14098
+ </td>
12791
14099
  <td>
12792
14100
  <mat-form-field appearance="outline" class="full">
12793
- <mat-label>Chave</mat-label>
14101
+ <mat-label>Parâmetro no filtro</mat-label>
12794
14102
  <input matInput [(ngModel)]="row.key" [matAutocomplete]="autoKey" [placeholder]="row.dep">
12795
14103
  <mat-autocomplete #autoKey="matAutocomplete">
12796
14104
  @for (opt of getKeySuggestions(row.dep); track opt) {
@@ -12802,7 +14110,7 @@ class CascadeManagerTabComponent {
12802
14110
  matSuffix
12803
14111
  class="help-icon-button"
12804
14112
  type="button"
12805
- [matTooltip]="'Use dot-notation (e.g. ' + row.dep + '.id) for nested objects'"
14113
+ [matTooltip]="'Use notação com ponto (ex.: ' + row.dep + '.id) quando o backend esperar filtro aninhado'"
12806
14114
  matTooltipPosition="above"
12807
14115
  >
12808
14116
  <mat-icon [praxisIcon]="'help_outline'"></mat-icon>
@@ -12811,7 +14119,7 @@ class CascadeManagerTabComponent {
12811
14119
  </td>
12812
14120
  <td>
12813
14121
  <mat-form-field appearance="outline" class="full">
12814
- <mat-label>ValuePath</mat-label>
14122
+ <mat-label>Propriedade do pai (ValuePath)</mat-label>
12815
14123
  <input matInput [(ngModel)]="row.valuePath" [matAutocomplete]="autoValue" [placeholder]="editValuePath||'id'">
12816
14124
  <mat-autocomplete #autoValue="matAutocomplete">
12817
14125
  @for (opt of getValuePathSuggestions(row.dep); track opt) {
@@ -12823,7 +14131,7 @@ class CascadeManagerTabComponent {
12823
14131
  matSuffix
12824
14132
  class="help-icon-button"
12825
14133
  type="button"
12826
- [matTooltip]="'Opcional'"
14134
+ [matTooltip]="'Opcional: sobrescreve a propriedade padrão lida apenas para este campo pai'"
12827
14135
  matTooltipPosition="above"
12828
14136
  >
12829
14137
  <mat-icon [praxisIcon]="'help_outline'"></mat-icon>
@@ -12835,29 +14143,32 @@ class CascadeManagerTabComponent {
12835
14143
  </tbody>
12836
14144
  </table>
12837
14145
  } @else {
12838
- <div class="rules-empty">Defina dependentes para sugerir o mapeamento.</div>
14146
+ <div class="rules-empty">Selecione um ou mais campos pais em “Este campo depende de” para configurar o mapeamento de filtros.</div>
12839
14147
  }
12840
14148
  </mat-expansion-panel>
12841
14149
  <!-- Preview -->
12842
14150
  <mat-expansion-panel class="stack-panel">
12843
14151
  <mat-expansion-panel-header>
12844
- <mat-panel-title class="panel-header"><mat-icon [praxisIcon]="'visibility'"></mat-icon> Preview</mat-panel-title>
14152
+ <mat-panel-title class="panel-header"><mat-icon [praxisIcon]="'visibility'"></mat-icon> Exemplo ilustrativo do filtro enviado (Preview)</mat-panel-title>
12845
14153
  </mat-expansion-panel-header>
14154
+ <p class="section-caption" data-testid="cascade-preview-explanation">
14155
+ Simulação ilustrativa (sem consulta ao servidor): mostra a estrutura JSON de filtro que será enviada ao carregar opções de <strong>{{ disambiguatedLabelFor(sel) }}</strong>.
14156
+ </p>
12846
14157
  <pre class="preview-box">{{ buildPreview() }}</pre>
12847
14158
  <div style="display:flex; justify-content:flex-end; padding-top:6px">
12848
- <button mat-stroked-button (click)="copyPreview()">Copiar</button>
14159
+ <button mat-stroked-button (click)="copyPreview()">Copiar exemplo</button>
12849
14160
  </div>
12850
14161
  </mat-expansion-panel>
12851
14162
  </div>
12852
14163
  <div style="display:flex; gap:8px; justify-content:flex-end; padding:8px 12px;">
12853
- <button mat-stroked-button (click)="cancelEdit()">Cancelar</button>
12854
- <button mat-flat-button color="primary" (click)="saveEdit(sel)">Salvar</button>
14164
+ <button mat-stroked-button type="button" (click)="cancelEdit()">Cancelar</button>
14165
+ <button mat-flat-button color="primary" type="button" (click)="saveEdit(sel)">Salvar</button>
12855
14166
  </div>
12856
14167
  </div>
12857
14168
  </div>
12858
14169
  }
12859
14170
  </div>
12860
- `, isInline: true, styles: [":host{display:block}.cm-root{display:grid;grid-template-columns:320px 1fr;gap:16px;min-height:420px}.cm-left{border-right:1px solid var(--md-sys-color-outline-variant);padding-right:8px}.cm-search{width:100%;margin-bottom:8px}.cm-right{padding-left:8px}.field-item{display:flex;align-items:center;justify-content:space-between;padding:8px 6px;cursor:pointer;border-radius:10px;transition:background .12s ease,border .12s ease;border:1px solid transparent}.field-item:hover{background:var(--md-sys-color-surface-container-high)}.field-item.selected{background:var(--md-sys-color-surface-container-highest);border-color:var(--md-sys-color-outline-variant)}.rule-badges{display:inline-flex;gap:6px}.rule-badge{font-size:11px;padding:2px 8px;border-radius:999px;font-weight:600}.b-active{background:var(--md-sys-color-primary-container);color:var(--md-sys-color-on-primary-container)}.b-manual{background:var(--md-sys-color-surface-container-high);color:var(--md-sys-color-on-surface-variant)}.b-immediate{background:var(--md-sys-color-secondary-container);color:var(--md-sys-color-on-secondary-container)}.b-conflict{background:var(--md-sys-color-error-container);color:var(--md-sys-color-on-error-container)}.cm-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px}.rules-empty{opacity:.8;font-style:italic;padding:8px 0;color:var(--md-sys-color-on-surface-variant)}.rule-row{display:grid;grid-template-columns:1fr auto;align-items:center;padding:10px 0;border-bottom:1px dashed var(--md-sys-color-outline-variant)}.rule-actions button{margin-left:4px}.banner{background:var(--md-sys-color-tertiary-container);color:var(--md-sys-color-on-tertiary-container);padding:10px 12px;border:1px solid var(--md-sys-color-outline-variant);border-radius:8px;margin-bottom:10px;display:flex;gap:8px;align-items:center}.import-panel{background:var(--md-sys-color-surface-container-high);border:1px solid var(--md-sys-color-outline-variant);border-radius:8px;padding:10px;margin-bottom:10px}.full{width:100%}.form-row{display:flex;gap:12px;align-items:center;flex-wrap:wrap}.form-grid{display:grid;gap:12px;padding:8px 0}.map-table{width:100%;border-collapse:collapse}.map-table th,.map-table td{padding:8px;border-bottom:1px dashed var(--md-sys-color-outline-variant)}.kbd{font-family:monospace;background:var(--md-sys-color-surface-container-highest);padding:0 6px;border-radius:4px;color:var(--md-sys-color-on-surface-variant)}.panel-surface{background:var(--md-sys-color-surface-container-high);border:1px solid var(--md-sys-color-outline-variant);border-radius:10px;box-shadow:var(--md-sys-elevation-level1, none);margin-bottom:12px}.panel-stack{display:grid;gap:12px;margin:12px 0}.stack-panel{background:var(--md-sys-color-surface-container-high);border:1px solid var(--md-sys-color-outline-variant);border-radius:10px;box-shadow:var(--md-sys-elevation-level1, none)}.stack-panel .mat-expansion-panel-content{padding:0 16px 12px}.panel-header{display:flex;align-items:center;gap:8px}.preview-box{background:var(--md-sys-color-surface-container-low);border:1px solid var(--md-sys-color-outline-variant);border-radius:8px;padding:8px;color:var(--md-sys-color-on-surface)}.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;margin-right:-4px}.help-icon-button mat-icon{font-size:18px;width:18px;height:18px}.mat-mdc-form-field-icon-suffix{align-self:center}\n"], dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.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.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i2$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { 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: "component", type: i1$2.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i3$1.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i5.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: "component", type: i3.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i3.MatLabel, selector: "mat-label" }, { kind: "directive", type: i3.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "ngmodule", type: MatChipsModule }, { kind: "ngmodule", type: MatListModule }, { kind: "ngmodule", type: MatDividerModule }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i6.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.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatCheckboxModule }, { kind: "component", type: i9.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: MatExpansionModule }, { kind: "component", type: i5$1.MatExpansionPanel, selector: "mat-expansion-panel", inputs: ["hideToggle", "togglePosition"], outputs: ["afterExpand", "afterCollapse"], exportAs: ["matExpansionPanel"] }, { kind: "component", type: i5$1.MatExpansionPanelHeader, selector: "mat-expansion-panel-header", inputs: ["expandedHeight", "collapsedHeight", "tabIndex"] }, { kind: "directive", type: i5$1.MatExpansionPanelTitle, selector: "mat-panel-title" }, { kind: "ngmodule", type: MatMenuModule }, { kind: "component", type: i11.MatMenu, selector: "mat-menu", inputs: ["backdropClass", "aria-label", "aria-labelledby", "aria-describedby", "xPosition", "yPosition", "overlapTrigger", "hasBackdrop", "class", "classList"], outputs: ["closed", "close"], exportAs: ["matMenu"] }, { kind: "component", type: i11.MatMenuItem, selector: "[mat-menu-item]", inputs: ["role", "disabled", "disableRipple"], exportAs: ["matMenuItem"] }, { kind: "directive", type: i11.MatMenuTrigger, selector: "[mat-menu-trigger-for], [matMenuTriggerFor]", inputs: ["mat-menu-trigger-for", "matMenuTriggerFor", "matMenuTriggerData", "matMenuTriggerRestoreFocus"], outputs: ["menuOpened", "onMenuOpen", "menuClosed", "onMenuClose"], exportAs: ["matMenuTrigger"] }, { kind: "ngmodule", type: MatAutocompleteModule }, { kind: "component", type: i12.MatAutocomplete, selector: "mat-autocomplete", inputs: ["aria-label", "aria-labelledby", "displayWith", "autoActiveFirstOption", "autoSelectActiveOption", "requireSelection", "panelWidth", "disableRipple", "class", "hideSingleSelectionIndicator"], outputs: ["optionSelected", "opened", "closed", "optionActivated"], exportAs: ["matAutocomplete"] }, { kind: "directive", type: i12.MatAutocompleteTrigger, selector: "input[matAutocomplete], textarea[matAutocomplete]", inputs: ["matAutocomplete", "matAutocompletePosition", "matAutocompleteConnectedTo", "autocomplete", "matAutocompleteDisabled"], exportAs: ["matAutocompleteTrigger"] }, { kind: "ngmodule", type: ScrollingModule }, { kind: "directive", type: i13.CdkFixedSizeVirtualScroll, selector: "cdk-virtual-scroll-viewport[itemSize]", inputs: ["itemSize", "minBufferPx", "maxBufferPx"] }, { kind: "directive", type: i13.CdkVirtualForOf, selector: "[cdkVirtualFor][cdkVirtualForOf]", inputs: ["cdkVirtualForOf", "cdkVirtualForTrackBy", "cdkVirtualForTemplate", "cdkVirtualForTemplateCacheSize"] }, { kind: "component", type: i13.CdkVirtualScrollViewport, selector: "cdk-virtual-scroll-viewport", inputs: ["orientation", "appendOnly"], outputs: ["scrolledIndexChange"] }, { kind: "directive", type: PraxisIconDirective, selector: "mat-icon[praxisIcon]", inputs: ["praxisIcon"] }] });
14171
+ `, isInline: true, styles: [":host{display:block;container-type:inline-size}.cm-root{display:grid;grid-template-columns:minmax(220px,280px) minmax(0,1fr);gap:16px;min-height:420px}.cm-left{border-right:1px solid var(--pfx-settings-panel-border, var(--md-sys-color-outline-variant));padding-right:8px;min-width:0}.cm-search{width:100%;margin-bottom:8px}.cm-right{padding-left:8px;min-width:0}@container (max-width: 720px){.cm-root{grid-template-columns:1fr}.cm-left{border-right:none;border-bottom:1px solid var(--pfx-settings-panel-border, var(--md-sys-color-outline-variant));padding-right:0;padding-bottom:12px}.cm-right{padding-left:0}}.field-item{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:8px;cursor:pointer;border-radius:10px;transition:background .12s ease,border .12s ease;border:1px solid transparent}.field-item:hover{background:var(--md-sys-color-surface-container-high)}.field-item.selected{background:var(--md-sys-color-surface-container-highest);border-color:var(--md-sys-color-outline-variant)}.field-item.has-cycle{border-color:var(--md-sys-color-error)}.field-item-main{min-width:0;flex:1}.field-item-title{font-weight:500;word-break:break-word}.field-item-meta{display:flex;flex-wrap:wrap;align-items:center;gap:6px;margin-top:2px;font-size:.75rem;color:var(--md-sys-color-on-surface-variant)}.field-role-pill{font-size:.7rem;padding:1px 6px;border-radius:999px;background:var(--md-sys-color-surface-container-high);color:var(--md-sys-color-on-surface-variant)}.field-role-pill--remote{background:color-mix(in srgb,var(--md-sys-color-primary-container) 65%,transparent);color:var(--md-sys-color-on-primary-container)}.rule-badges{display:inline-flex;gap:6px;flex-wrap:wrap;justify-content:flex-end}.rule-badge{font-size:11px;padding:2px 8px;border-radius:999px;font-weight:600;white-space:nowrap}.b-active{background:var(--md-sys-color-primary-container);color:var(--md-sys-color-on-primary-container)}.b-manual{background:var(--md-sys-color-surface-container-high);color:var(--md-sys-color-on-surface-variant)}.b-immediate{background:var(--md-sys-color-secondary-container);color:var(--md-sys-color-on-secondary-container)}.b-conflict{background:var(--md-sys-color-error-container);color:var(--md-sys-color-on-error-container)}.cm-header{display:flex;align-items:center;justify-content:space-between;gap:8px;flex-wrap:wrap;margin-bottom:8px}.cm-feedback{padding:8px 12px;border-radius:8px;font-size:.85rem;margin-bottom:10px;border:1px solid var(--md-sys-color-outline-variant);background:var(--md-sys-color-secondary-container);color:var(--md-sys-color-on-secondary-container)}.cm-feedback--error{border-color:var(--md-sys-color-error);background:var(--md-sys-color-error-container);color:var(--md-sys-color-on-error-container)}.cm-cycle-alert{padding:10px 12px;border-radius:8px;font-size:.85rem;margin-bottom:10px;border:1px solid var(--md-sys-color-error);background:var(--md-sys-color-error-container);color:var(--md-sys-color-on-error-container);display:flex;gap:10px;align-items:flex-start}.cm-cycle-alert mat-icon{color:var(--md-sys-color-error);flex-shrink:0;margin-top:2px}.cm-cycle-alert-body{display:grid;gap:4px;flex:1}.cm-source-notice{padding:8px 12px;border-radius:8px;font-size:.82rem;margin-bottom:10px;border:1px solid var(--md-sys-color-outline-variant);background:var(--md-sys-color-surface-container-low);color:var(--md-sys-color-on-surface-variant);display:flex;gap:8px;align-items:center}.rules-empty{opacity:.88;padding:8px 0;font-size:.85rem;color:var(--md-sys-color-on-surface-variant)}.rule-row{display:grid;grid-template-columns:1fr auto;align-items:center;padding:10px 0;border-bottom:1px dashed var(--md-sys-color-outline-variant);gap:8px}.rule-summary-direction{font-size:.85rem;margin-top:2px}.rule-summary-policies{font-size:.8rem;opacity:.78;margin-top:2px}.rule-actions button{margin-left:4px}.banner{background:var(--md-sys-color-tertiary-container);color:var(--md-sys-color-on-tertiary-container);padding:10px 12px;border:1px solid var(--md-sys-color-outline-variant);border-radius:8px;margin-bottom:10px;display:flex;gap:8px;align-items:center}.import-panel{background:var(--pfx-settings-panel-surface, var(--md-sys-color-surface-container-high));border:1px solid var(--pfx-settings-panel-border, var(--md-sys-color-outline-variant));border-radius:8px;padding:10px;margin-bottom:10px}.import-textarea{width:100%;box-sizing:border-box;padding:8px 10px;border-radius:8px;border:1px solid var(--pfx-settings-panel-border, var(--md-sys-color-outline-variant));background:var(--pfx-settings-panel-surface-elevated, var(--md-sys-color-surface-container-low));color:var(--pfx-settings-panel-fg, var(--md-sys-color-on-surface));font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.85rem}.full{width:100%}.form-row{display:flex;gap:12px;align-items:center;flex-wrap:wrap}.form-grid{display:grid;gap:12px;padding:8px 0}.section-caption{margin:0 0 6px;font-size:.82rem;color:var(--md-sys-color-on-surface-variant)}.map-table{width:100%;border-collapse:collapse}.map-table th,.map-table td{padding:8px;border-bottom:1px dashed var(--md-sys-color-outline-variant);text-align:left;vertical-align:middle}.kbd{font-family:monospace;background:var(--md-sys-color-surface-container-highest);padding:0 6px;border-radius:4px;color:var(--md-sys-color-on-surface-variant)}.panel-surface{padding-top:8px;margin-bottom:12px}.panel-stack{display:grid;gap:12px;margin:12px 0}.stack-panel{background:var(--pfx-settings-panel-surface, var(--md-sys-color-surface-container-high));border:1px solid var(--pfx-settings-panel-border, var(--md-sys-color-outline-variant));border-radius:10px;box-shadow:none}.stack-panel .mat-expansion-panel-content{padding:0 16px 12px}.panel-header{display:flex;align-items:center;gap:8px}.preview-box{background:var(--md-sys-color-surface-container-low);border:1px solid var(--md-sys-color-outline-variant);border-radius:8px;padding:8px;color:var(--md-sys-color-on-surface);margin:4px 0 0}.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;margin-right:-4px}.help-icon-button mat-icon{font-size:18px;width:18px;height:18px}.mat-mdc-form-field-icon-suffix{align-self:center}\n"], dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.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.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i2$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { 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: "component", type: i1$2.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i3$1.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i5.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: "component", type: i3.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i3.MatLabel, selector: "mat-label" }, { kind: "directive", type: i3.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "ngmodule", type: MatChipsModule }, { kind: "ngmodule", type: MatListModule }, { kind: "ngmodule", type: MatDividerModule }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i6.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.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatCheckboxModule }, { kind: "component", type: i9.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: MatExpansionModule }, { kind: "component", type: i5$1.MatExpansionPanel, selector: "mat-expansion-panel", inputs: ["hideToggle", "togglePosition"], outputs: ["afterExpand", "afterCollapse"], exportAs: ["matExpansionPanel"] }, { kind: "component", type: i5$1.MatExpansionPanelHeader, selector: "mat-expansion-panel-header", inputs: ["expandedHeight", "collapsedHeight", "tabIndex"] }, { kind: "directive", type: i5$1.MatExpansionPanelTitle, selector: "mat-panel-title" }, { kind: "ngmodule", type: MatMenuModule }, { kind: "component", type: i11.MatMenu, selector: "mat-menu", inputs: ["backdropClass", "aria-label", "aria-labelledby", "aria-describedby", "xPosition", "yPosition", "overlapTrigger", "hasBackdrop", "class", "classList"], outputs: ["closed", "close"], exportAs: ["matMenu"] }, { kind: "component", type: i11.MatMenuItem, selector: "[mat-menu-item]", inputs: ["role", "disabled", "disableRipple"], exportAs: ["matMenuItem"] }, { kind: "directive", type: i11.MatMenuTrigger, selector: "[mat-menu-trigger-for], [matMenuTriggerFor]", inputs: ["mat-menu-trigger-for", "matMenuTriggerFor", "matMenuTriggerData", "matMenuTriggerRestoreFocus"], outputs: ["menuOpened", "onMenuOpen", "menuClosed", "onMenuClose"], exportAs: ["matMenuTrigger"] }, { kind: "ngmodule", type: MatAutocompleteModule }, { kind: "component", type: i12.MatAutocomplete, selector: "mat-autocomplete", inputs: ["aria-label", "aria-labelledby", "displayWith", "autoActiveFirstOption", "autoSelectActiveOption", "requireSelection", "panelWidth", "disableRipple", "class", "hideSingleSelectionIndicator"], outputs: ["optionSelected", "opened", "closed", "optionActivated"], exportAs: ["matAutocomplete"] }, { kind: "directive", type: i12.MatAutocompleteTrigger, selector: "input[matAutocomplete], textarea[matAutocomplete]", inputs: ["matAutocomplete", "matAutocompletePosition", "matAutocompleteConnectedTo", "autocomplete", "matAutocompleteDisabled"], exportAs: ["matAutocompleteTrigger"] }, { kind: "ngmodule", type: ScrollingModule }, { kind: "directive", type: i13.CdkFixedSizeVirtualScroll, selector: "cdk-virtual-scroll-viewport[itemSize]", inputs: ["itemSize", "minBufferPx", "maxBufferPx"] }, { kind: "directive", type: i13.CdkVirtualForOf, selector: "[cdkVirtualFor][cdkVirtualForOf]", inputs: ["cdkVirtualForOf", "cdkVirtualForTrackBy", "cdkVirtualForTemplate", "cdkVirtualForTemplateCacheSize"] }, { kind: "component", type: i13.CdkVirtualScrollViewport, selector: "cdk-virtual-scroll-viewport", inputs: ["orientation", "appendOnly"], outputs: ["scrolledIndexChange"] }, { kind: "directive", type: PraxisIconDirective, selector: "mat-icon[praxisIcon]", inputs: ["praxisIcon"] }] });
12861
14172
  }
12862
14173
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: CascadeManagerTabComponent, decorators: [{
12863
14174
  type: Component,
@@ -12888,14 +14199,29 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
12888
14199
  </mat-form-field>
12889
14200
  @if (fields.length <= 50) {
12890
14201
  <div>
12891
- @for (f of filteredFields(); track f) {
12892
- <div (click)="selectField(f.name)" class="field-item" [class.selected]="f.name===selectedField()" tabindex="0" (keydown)="onFieldKeydown($event, f.name)">
12893
- <div>
12894
- <div>{{ labelFor(f.name) }}</div>
12895
- <small style="opacity:.7">{{ f.name }}</small>
14202
+ @for (f of filteredFields(); track f.name) {
14203
+ <div
14204
+ (click)="selectField(f.name)"
14205
+ class="field-item"
14206
+ [class.selected]="f.name===selectedField()"
14207
+ [class.has-cycle]="isFieldInCycle(f.name)"
14208
+ tabindex="0"
14209
+ (keydown)="onFieldKeydown($event, f.name)"
14210
+ >
14211
+ <div class="field-item-main">
14212
+ <div class="field-item-title">{{ disambiguatedLabelFor(f.name) }}</div>
14213
+ <div class="field-item-meta">
14214
+ <span>{{ f.name }}</span>
14215
+ <span class="field-role-pill" [class.field-role-pill--remote]="hasRemoteOptionSource(f.name)">
14216
+ {{ fieldRoleBadge(f.name) }}
14217
+ </span>
14218
+ </div>
12896
14219
  </div>
12897
- @if (hasRule(f.name)) {
12898
- <div class="rule-badges">
14220
+ <div class="rule-badges">
14221
+ @if (isFieldInCycle(f.name)) {
14222
+ <span class="rule-badge b-conflict">Ciclo</span>
14223
+ }
14224
+ @if (hasRule(f.name)) {
12899
14225
  @if (isCascadeEnabled(f.name)) {
12900
14226
  <span class="rule-badge b-active">Ativo</span>
12901
14227
  }
@@ -12903,22 +14229,38 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
12903
14229
  <span class="rule-badge b-manual">Manual</span>
12904
14230
  }
12905
14231
  @if (loadOnChange(f.name)==='immediate') {
12906
- <span class="rule-badge b-immediate">Immediate</span>
14232
+ <span class="rule-badge b-immediate">Imediato</span>
12907
14233
  }
12908
- </div>
12909
- }
14234
+ }
14235
+ </div>
12910
14236
  </div>
12911
14237
  }
12912
14238
  </div>
12913
14239
  } @else {
12914
- <cdk-virtual-scroll-viewport itemSize="44" style="height: calc(100vh - 240px)">
12915
- <div *cdkVirtualFor="let f of filteredFields()" (click)="selectField(f.name)" class="field-item" [class.selected]="f.name===selectedField()" tabindex="0" (keydown)="onFieldKeydown($event, f.name)">
12916
- <div>
12917
- <div>{{ labelFor(f.name) }}</div>
12918
- <small style="opacity:.7">{{ f.name }}</small>
14240
+ <cdk-virtual-scroll-viewport itemSize="48" style="height: calc(100vh - 240px)">
14241
+ <div
14242
+ *cdkVirtualFor="let f of filteredFields()"
14243
+ (click)="selectField(f.name)"
14244
+ class="field-item"
14245
+ [class.selected]="f.name===selectedField()"
14246
+ [class.has-cycle]="isFieldInCycle(f.name)"
14247
+ tabindex="0"
14248
+ (keydown)="onFieldKeydown($event, f.name)"
14249
+ >
14250
+ <div class="field-item-main">
14251
+ <div class="field-item-title">{{ disambiguatedLabelFor(f.name) }}</div>
14252
+ <div class="field-item-meta">
14253
+ <span>{{ f.name }}</span>
14254
+ <span class="field-role-pill" [class.field-role-pill--remote]="hasRemoteOptionSource(f.name)">
14255
+ {{ fieldRoleBadge(f.name) }}
14256
+ </span>
14257
+ </div>
12919
14258
  </div>
12920
- @if (hasRule(f.name)) {
12921
- <div class="rule-badges">
14259
+ <div class="rule-badges">
14260
+ @if (isFieldInCycle(f.name)) {
14261
+ <span class="rule-badge b-conflict">Ciclo</span>
14262
+ }
14263
+ @if (hasRule(f.name)) {
12922
14264
  @if (isCascadeEnabled(f.name)) {
12923
14265
  <span class="rule-badge b-active">Ativo</span>
12924
14266
  }
@@ -12926,27 +14268,27 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
12926
14268
  <span class="rule-badge b-manual">Manual</span>
12927
14269
  }
12928
14270
  @if (loadOnChange(f.name)==='immediate') {
12929
- <span class="rule-badge b-immediate">Immediate</span>
14271
+ <span class="rule-badge b-immediate">Imediato</span>
12930
14272
  }
12931
- </div>
12932
- }
14273
+ }
14274
+ </div>
12933
14275
  </div>
12934
14276
  </cdk-virtual-scroll-viewport>
12935
14277
  }
12936
14278
  </div>
12937
-
14279
+
12938
14280
  <!-- Right: Rules for selected field -->
12939
14281
  @if (selectedField(); as sel) {
12940
14282
  <div class="cm-right">
12941
14283
  <div class="cm-header">
12942
14284
  <div>
12943
- <h3 style="margin:0">{{ labelFor(sel) }}</h3>
12944
- <div style="opacity:.7">{{ sel }}</div>
14285
+ <h3 style="margin:0">{{ disambiguatedLabelFor(sel) }}</h3>
14286
+ <div style="opacity:.75; font-size:0.85rem">{{ sel }} · {{ fieldRoleSummary(sel) }}</div>
12945
14287
  </div>
12946
14288
  <div>
12947
14289
  <button mat-stroked-button color="primary" (click)="exportRules()" style="margin-right:8px">Exportar</button>
12948
14290
  <button mat-stroked-button (click)="toggleImport()" style="margin-right:8px">Importar</button>
12949
- <button mat-flat-button color="primary" [matMenuTriggerFor]="addMenu" [disabled]="editing()">Adicionar</button>
14291
+ <button mat-flat-button color="primary" [matMenuTriggerFor]="addMenu">Modelos</button>
12950
14292
  <mat-menu #addMenu="matMenu">
12951
14293
  <button mat-menu-item (click)="startEdit(sel)"><mat-icon [praxisIcon]="'add'"></mat-icon><span>Em branco</span></button>
12952
14294
  <button mat-menu-item (click)="startTemplate(sel, 'rota-destino')" [disabled]="!hasField('rotaId') || !hasField('destinoReqId')"><mat-icon [praxisIcon]="'bolt'"></mat-icon><span>Rota → Destino (id)</span></button>
@@ -12954,6 +14296,45 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
12954
14296
  </mat-menu>
12955
14297
  </div>
12956
14298
  </div>
14299
+
14300
+ @if (activeCycleDiagnostics().length > 0) {
14301
+ <div class="cm-cycle-alert" role="alert" data-testid="cascade-cycle-diagnostic">
14302
+ <mat-icon [praxisIcon]="'error'"></mat-icon>
14303
+ <div class="cm-cycle-alert-body">
14304
+ <strong>Dependência circular detectada — não é possível aplicar enquanto houver ciclo</strong>
14305
+ @for (cycle of activeCycleDiagnostics(); track cycle.formattedPath) {
14306
+ <div>{{ cycle.message }}</div>
14307
+ }
14308
+ @if (isFieldInCycle(sel)) {
14309
+ <div style="margin-top: 6px;">
14310
+ <button
14311
+ mat-stroked-button
14312
+ type="button"
14313
+ data-testid="cascade-break-cycle-button"
14314
+ (click)="breakCycleForField(sel)"
14315
+ >
14316
+ Remover dependência circular deste campo
14317
+ </button>
14318
+ </div>
14319
+ }
14320
+ </div>
14321
+ </div>
14322
+ }
14323
+
14324
+ @if (feedbackMessage) {
14325
+ <div class="cm-feedback" [class.cm-feedback--error]="feedbackTone === 'error'" role="status">{{ feedbackMessage }}</div>
14326
+ }
14327
+
14328
+ @if (!hasRemoteOptionSource(sel)) {
14329
+ <div class="cm-source-notice" role="note" data-testid="cascade-no-remote-source-notice">
14330
+ <mat-icon [praxisIcon]="'info'"></mat-icon>
14331
+ <div>
14332
+ Este campo ({{ fieldRoleBadge(sel) }}) não declara fonte de opções remota (<code class="kbd">resourcePath</code> / <code class="kbd">optionSource</code>).
14333
+ Campos de texto ou nomes resolvidos exibem valores derivados; para recarregar opções em cascata, selecione o campo selecionável (ex.: ID) correspondente.
14334
+ </div>
14335
+ </div>
14336
+ }
14337
+
12957
14338
  <!-- Conflict banner (connections) -->
12958
14339
  @if (hasConflict(sel)) {
12959
14340
  <div class="banner">
@@ -12966,43 +14347,56 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
12966
14347
  <button mat-stroked-button color="accent" (click)="quickSet(sel,'manual')">Definir manual</button>
12967
14348
  </div>
12968
14349
  }
14350
+
12969
14351
  @if (importMode) {
12970
14352
  <div class="import-panel">
12971
14353
  <div style="display:flex; gap:8px; align-items:center; margin-bottom:8px"><mat-icon [praxisIcon]="'upload'"></mat-icon><strong>Importar regras (JSON)</strong></div>
12972
- <textarea matInput rows="6" class="full" [(ngModel)]="importJson" placeholder='{ "campo": { "dependencyFields": ["dep"], ... } }'></textarea>
14354
+ <textarea rows="6" class="import-textarea" [(ngModel)]="importJson" placeholder='{ "campo": { "dependencyFields": ["dep"], ... } }'></textarea>
12973
14355
  <div style="display:flex; gap:8px; justify-content:flex-end; padding-top:8px">
12974
14356
  <button mat-stroked-button (click)="toggleImport()">Cancelar</button>
12975
14357
  <button mat-flat-button color="primary" (click)="applyImport()">Aplicar</button>
12976
14358
  </div>
12977
14359
  </div>
12978
14360
  }
12979
- @if (!ruleFor(sel)) {
14361
+
14362
+ @if (!hasRule(sel)) {
12980
14363
  <div>
12981
- <div class="rules-empty">Nenhuma cascata configurada. Clique em “Adicionar” para montar a dependência.</div>
14364
+ <div class="rules-empty" data-testid="cascade-empty-guidance">
14365
+ Nenhuma cascata salva para <strong>{{ disambiguatedLabelFor(sel) }}</strong>.
14366
+ Selecione abaixo em <strong>“Este campo depende de (campos pais)”</strong> quais campos controlam as opções deste campo e clique em <strong>Salvar</strong>.
14367
+ </div>
12982
14368
  </div>
12983
14369
  }
12984
- @if (ruleFor(sel); as rule) {
14370
+ @if (hasRule(sel) && ruleFor(sel); as rule) {
12985
14371
  <div>
12986
14372
  <div class="rule-row">
12987
14373
  <div>
12988
- <div><strong>Dependentes:</strong> {{ rule.dependencyFields.join(', ') || '—' }}</div>
12989
- <div style="opacity:.7">Load on change: {{ rule.dependencyLoadOnChange || 'respectLoadOn' }} | Merge: {{ rule.dependencyMergeStrategy || 'merge' }}</div>
14374
+ <div><strong>Depende de (campos pais):</strong> {{ formatParentList(rule.dependencyFields) }}</div>
14375
+ <div class="rule-summary-direction">
14376
+ <strong>Fluxo:</strong> {{ formatRelationshipSummary(sel, rule.dependencyFields) }}
14377
+ </div>
14378
+ <div class="rule-summary-policies">
14379
+ Quando recarregar: {{ formatLoadOnLabel(rule.dependencyLoadOnChange) }} | Como combinar filtros: {{ formatMergeStrategyLabel(rule.dependencyMergeStrategy) }}
14380
+ </div>
12990
14381
  </div>
12991
14382
  <div class="rule-actions">
12992
- <button mat-icon-button matTooltip="Editar" (click)="startEdit(sel)"><mat-icon [praxisIcon]="'edit'"></mat-icon></button>
12993
- <button mat-icon-button matTooltip="Duplicar" disabled><mat-icon [praxisIcon]="'content_copy'"></mat-icon></button>
12994
- <button mat-icon-button matTooltip="Remover" (click)="remove(sel)"><mat-icon [praxisIcon]="'delete'"></mat-icon></button>
14383
+ <button mat-icon-button type="button" aria-label="Editar regra de cascata" matTooltip="Editar" (click)="startEdit(sel)"><mat-icon [praxisIcon]="'edit'"></mat-icon></button>
14384
+ <button mat-icon-button type="button" aria-label="Duplicar regra de cascata" matTooltip="Duplicar" disabled><mat-icon [praxisIcon]="'content_copy'"></mat-icon></button>
14385
+ <button mat-icon-button type="button" aria-label="Remover regra de cascata" matTooltip="Remover" (click)="remove(sel)"><mat-icon [praxisIcon]="'delete'"></mat-icon></button>
12995
14386
  </div>
12996
14387
  </div>
12997
14388
  </div>
12998
14389
  }
14390
+
12999
14391
  <!-- Editor com seções (sem acordeão duplo) -->
13000
14392
  <div class="panel-surface">
13001
- <div class="panel-header" style="padding: 12px 16px; border-bottom: 1px solid var(--md-sys-color-outline-variant);">
14393
+ <div class="panel-header" style="padding: 8px 4px; border-bottom: 1px solid var(--pfx-settings-panel-border, var(--md-sys-color-outline-variant));">
13002
14394
  <mat-icon [praxisIcon]="'rule'"></mat-icon>
13003
14395
  <div>
13004
14396
  <div>Cascata do campo</div>
13005
- <small style="color: var(--md-sys-color-on-surface-variant);">Defina dependentes, quando recarregar e como mapear filtros</small>
14397
+ <small style="color: var(--md-sys-color-on-surface-variant);">
14398
+ Defina de quais campos pais este campo depende, quando recarregar opções e qual filtro enviar ao serviço
14399
+ </small>
13006
14400
  </div>
13007
14401
  </div>
13008
14402
  <div class="panel-stack">
@@ -13012,13 +14406,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
13012
14406
  <mat-panel-title class="panel-header"><mat-icon [praxisIcon]="'tune'"></mat-icon> Básico</mat-panel-title>
13013
14407
  </mat-expansion-panel-header>
13014
14408
  <div class="form-grid">
14409
+ <p class="section-caption">
14410
+ Quando o usuário alterar qualquer um dos campos pais escolhidos abaixo, as opções disponíveis em <strong>{{ disambiguatedLabelFor(sel) }}</strong> serão filtradas.
14411
+ </p>
13015
14412
  <div class="form-row">
13016
14413
  <mat-form-field appearance="outline" class="full">
13017
- <mat-label>Campos dependentes</mat-label>
14414
+ <mat-label>Este campo depende de (campos pais)</mat-label>
13018
14415
  <mat-select multiple [(ngModel)]="editDepList" (ngModelChange)="onDepListChange()">
13019
- @for (f of availableDependencyFields(); track f) {
14416
+ @for (f of availableDependencyFields(); track f.name) {
13020
14417
  <mat-option [value]="f.name">
13021
- {{ labelFor(f.name) }} ({{ f.name }})
14418
+ {{ disambiguatedLabelFor(f.name) }} ({{ f.name }}) · {{ fieldRoleBadge(f.name) }}
13022
14419
  </mat-option>
13023
14420
  }
13024
14421
  </mat-select>
@@ -13027,42 +14424,54 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
13027
14424
  matSuffix
13028
14425
  class="help-icon-button"
13029
14426
  type="button"
13030
- [matTooltip]="'Selecione os campos que controlam este valor'"
14427
+ [matTooltip]="'Selecione os campos pais (origem) cujo valor filtrará as opções deste campo (destino)'"
13031
14428
  matTooltipPosition="above"
13032
14429
  >
13033
14430
  <mat-icon [praxisIcon]="'help_outline'"></mat-icon>
13034
14431
  </button>
13035
14432
  </mat-form-field>
13036
14433
  </div>
14434
+ @if (editDepList.length > 0) {
14435
+ <div class="section-caption" data-testid="cascade-draft-direction-summary">
14436
+ <strong>Resumo da relação:</strong> {{ formatRelationshipSummary(sel, editDepList) }}
14437
+ </div>
14438
+ }
13037
14439
  <div class="form-row">
13038
- <mat-checkbox [(ngModel)]="editEnable">Ativar cascata nativa</mat-checkbox>
13039
- <mat-checkbox [(ngModel)]="editReset">Resetar ao mudar</mat-checkbox>
14440
+ <mat-checkbox [(ngModel)]="editEnable" (ngModelChange)="onDraftControlChange()">
14441
+ Ativar cascata automática neste campo
14442
+ </mat-checkbox>
14443
+ <mat-checkbox [(ngModel)]="editReset" (ngModelChange)="onDraftControlChange()">
14444
+ Limpar seleção deste campo quando o campo pai mudar
14445
+ </mat-checkbox>
13040
14446
  </div>
13041
14447
  </div>
13042
14448
  </mat-expansion-panel>
13043
14449
  <!-- Execução -->
13044
14450
  <mat-expansion-panel class="stack-panel">
13045
14451
  <mat-expansion-panel-header>
13046
- <mat-panel-title class="panel-header"><mat-icon [praxisIcon]="'schedule'"></mat-icon> Execução</mat-panel-title>
14452
+ <mat-panel-title class="panel-header"><mat-icon [praxisIcon]="'schedule'"></mat-icon> Quando recarregar e combinar filtros</mat-panel-title>
13047
14453
  </mat-expansion-panel-header>
14454
+ <p class="section-caption">
14455
+ Controle o momento em que a consulta de opções é disparada e como o filtro do campo pai convive com outros filtros do campo.
14456
+ </p>
13048
14457
  <div class="form-row">
13049
14458
  <mat-form-field appearance="outline">
13050
- <mat-label>Carregar ao mudar</mat-label>
14459
+ <mat-label>Quando recarregar opções</mat-label>
13051
14460
  <mat-select [(ngModel)]="editLoadOn">
13052
- <mat-option value="respectLoadOn">Respeitar loadOn</mat-option>
13053
- <mat-option value="immediate">Imediato</mat-option>
13054
- <mat-option value="manual">Manual</mat-option>
14461
+ <mat-option value="respectLoadOn">Seguir configuração do campo (respectLoadOn)</mat-option>
14462
+ <mat-option value="immediate">Recarregar imediatamente ao mudar o pai (immediate)</mat-option>
14463
+ <mat-option value="manual">Somente sob comando manual / abertura (manual)</mat-option>
13055
14464
  </mat-select>
13056
14465
  </mat-form-field>
13057
14466
  <mat-form-field appearance="outline">
13058
- <mat-label>Merge de filtros</mat-label>
14467
+ <mat-label>Como combinar filtros</mat-label>
13059
14468
  <mat-select [(ngModel)]="editMerge">
13060
- <mat-option value="merge">merge (padrão)</mat-option>
13061
- <mat-option value="replace">replace</mat-option>
14469
+ <mat-option value="merge">Mesclar com filtros existentes (merge — padrão)</mat-option>
14470
+ <mat-option value="replace">Substituir filtros anteriores (replace)</mat-option>
13062
14471
  </mat-select>
13063
14472
  </mat-form-field>
13064
14473
  <mat-form-field appearance="outline">
13065
- <mat-label>Atraso (ms)</mat-label>
14474
+ <mat-label>Atraso após mudança (ms)</mat-label>
13066
14475
  <input matInput type="number" [(ngModel)]="editDebounce">
13067
14476
  </mat-form-field>
13068
14477
  </div>
@@ -13070,18 +14479,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
13070
14479
  <!-- Extração de valor -->
13071
14480
  <mat-expansion-panel class="stack-panel">
13072
14481
  <mat-expansion-panel-header>
13073
- <mat-panel-title class="panel-header"><mat-icon [praxisIcon]="'call_split'"></mat-icon> Extração de valor</mat-panel-title>
14482
+ <mat-panel-title class="panel-header"><mat-icon [praxisIcon]="'call_split'"></mat-icon> Qual propriedade ler do campo pai</mat-panel-title>
13074
14483
  </mat-expansion-panel-header>
14484
+ <p class="section-caption">
14485
+ Se o campo pai guardar um objeto completo (ex.: <code class="kbd">&#123; id: 10, nome: "TI" &#125;</code>), informe qual propriedade deve ser extraída para envio no filtro (padrão: <code class="kbd">id</code>).
14486
+ </p>
13075
14487
  <div class="form-row">
13076
14488
  <mat-form-field appearance="outline" class="full">
13077
- <mat-label>ValuePath padrão</mat-label>
14489
+ <mat-label>Propriedade padrão lida do campo pai (ValuePath)</mat-label>
13078
14490
  <input matInput [(ngModel)]="editValuePath" placeholder="id">
13079
14491
  <button
13080
14492
  mat-icon-button
13081
14493
  matSuffix
13082
14494
  class="help-icon-button"
13083
14495
  type="button"
13084
- [matTooltip]="'Preenchido automaticamente a partir de optionValueKey() ou \\'id\\''"
14496
+ [matTooltip]="'Preenchido automaticamente a partir de optionValueKey do campo pai ou \\'id\\''"
13085
14497
  matTooltipPosition="above"
13086
14498
  >
13087
14499
  <mat-icon [praxisIcon]="'help_outline'"></mat-icon>
@@ -13092,24 +14504,30 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
13092
14504
  <!-- Mapeamento de filtros (tabela) -->
13093
14505
  <mat-expansion-panel class="stack-panel">
13094
14506
  <mat-expansion-panel-header>
13095
- <mat-panel-title class="panel-header"><mat-icon [praxisIcon]="'filter_alt'"></mat-icon> Mapeamento de filtros</mat-panel-title>
14507
+ <mat-panel-title class="panel-header"><mat-icon [praxisIcon]="'filter_alt'"></mat-icon> Mapeamento de filtros enviados ao serviço</mat-panel-title>
13096
14508
  </mat-expansion-panel-header>
13097
14509
  @if (editMapRows.length) {
14510
+ <p class="section-caption">
14511
+ Para cada campo pai, defina o nome do parâmetro esperado pelo serviço de opções deste campo e, se necessário, a propriedade específica a extrair.
14512
+ </p>
13098
14513
  <table class="map-table">
13099
14514
  <thead>
13100
14515
  <tr>
13101
- <th>Dependente</th>
13102
- <th>Chave (dot‑path)</th>
13103
- <th>ValuePath (opcional)</th>
14516
+ <th>Campo pai (origem)</th>
14517
+ <th>Parâmetro enviado ao filtro (dot‑path)</th>
14518
+ <th>Propriedade lida do pai (opcional)</th>
13104
14519
  </tr>
13105
14520
  </thead>
13106
14521
  <tbody>
13107
- @for (row of editMapRows; track row; let i = $index) {
14522
+ @for (row of editMapRows; track row.dep; let i = $index) {
13108
14523
  <tr>
13109
- <td>{{ row.dep }}</td>
14524
+ <td>
14525
+ <div><strong>{{ disambiguatedLabelFor(row.dep) }}</strong></div>
14526
+ <small style="opacity:.75">{{ row.dep }}</small>
14527
+ </td>
13110
14528
  <td>
13111
14529
  <mat-form-field appearance="outline" class="full">
13112
- <mat-label>Chave</mat-label>
14530
+ <mat-label>Parâmetro no filtro</mat-label>
13113
14531
  <input matInput [(ngModel)]="row.key" [matAutocomplete]="autoKey" [placeholder]="row.dep">
13114
14532
  <mat-autocomplete #autoKey="matAutocomplete">
13115
14533
  @for (opt of getKeySuggestions(row.dep); track opt) {
@@ -13121,7 +14539,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
13121
14539
  matSuffix
13122
14540
  class="help-icon-button"
13123
14541
  type="button"
13124
- [matTooltip]="'Use dot-notation (e.g. ' + row.dep + '.id) for nested objects'"
14542
+ [matTooltip]="'Use notação com ponto (ex.: ' + row.dep + '.id) quando o backend esperar filtro aninhado'"
13125
14543
  matTooltipPosition="above"
13126
14544
  >
13127
14545
  <mat-icon [praxisIcon]="'help_outline'"></mat-icon>
@@ -13130,7 +14548,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
13130
14548
  </td>
13131
14549
  <td>
13132
14550
  <mat-form-field appearance="outline" class="full">
13133
- <mat-label>ValuePath</mat-label>
14551
+ <mat-label>Propriedade do pai (ValuePath)</mat-label>
13134
14552
  <input matInput [(ngModel)]="row.valuePath" [matAutocomplete]="autoValue" [placeholder]="editValuePath||'id'">
13135
14553
  <mat-autocomplete #autoValue="matAutocomplete">
13136
14554
  @for (opt of getValuePathSuggestions(row.dep); track opt) {
@@ -13142,7 +14560,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
13142
14560
  matSuffix
13143
14561
  class="help-icon-button"
13144
14562
  type="button"
13145
- [matTooltip]="'Opcional'"
14563
+ [matTooltip]="'Opcional: sobrescreve a propriedade padrão lida apenas para este campo pai'"
13146
14564
  matTooltipPosition="above"
13147
14565
  >
13148
14566
  <mat-icon [praxisIcon]="'help_outline'"></mat-icon>
@@ -13154,29 +14572,32 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
13154
14572
  </tbody>
13155
14573
  </table>
13156
14574
  } @else {
13157
- <div class="rules-empty">Defina dependentes para sugerir o mapeamento.</div>
14575
+ <div class="rules-empty">Selecione um ou mais campos pais em “Este campo depende de” para configurar o mapeamento de filtros.</div>
13158
14576
  }
13159
14577
  </mat-expansion-panel>
13160
14578
  <!-- Preview -->
13161
14579
  <mat-expansion-panel class="stack-panel">
13162
14580
  <mat-expansion-panel-header>
13163
- <mat-panel-title class="panel-header"><mat-icon [praxisIcon]="'visibility'"></mat-icon> Preview</mat-panel-title>
14581
+ <mat-panel-title class="panel-header"><mat-icon [praxisIcon]="'visibility'"></mat-icon> Exemplo ilustrativo do filtro enviado (Preview)</mat-panel-title>
13164
14582
  </mat-expansion-panel-header>
14583
+ <p class="section-caption" data-testid="cascade-preview-explanation">
14584
+ Simulação ilustrativa (sem consulta ao servidor): mostra a estrutura JSON de filtro que será enviada ao carregar opções de <strong>{{ disambiguatedLabelFor(sel) }}</strong>.
14585
+ </p>
13165
14586
  <pre class="preview-box">{{ buildPreview() }}</pre>
13166
14587
  <div style="display:flex; justify-content:flex-end; padding-top:6px">
13167
- <button mat-stroked-button (click)="copyPreview()">Copiar</button>
14588
+ <button mat-stroked-button (click)="copyPreview()">Copiar exemplo</button>
13168
14589
  </div>
13169
14590
  </mat-expansion-panel>
13170
14591
  </div>
13171
14592
  <div style="display:flex; gap:8px; justify-content:flex-end; padding:8px 12px;">
13172
- <button mat-stroked-button (click)="cancelEdit()">Cancelar</button>
13173
- <button mat-flat-button color="primary" (click)="saveEdit(sel)">Salvar</button>
14593
+ <button mat-stroked-button type="button" (click)="cancelEdit()">Cancelar</button>
14594
+ <button mat-flat-button color="primary" type="button" (click)="saveEdit(sel)">Salvar</button>
13174
14595
  </div>
13175
14596
  </div>
13176
14597
  </div>
13177
14598
  }
13178
14599
  </div>
13179
- `, styles: [":host{display:block}.cm-root{display:grid;grid-template-columns:320px 1fr;gap:16px;min-height:420px}.cm-left{border-right:1px solid var(--md-sys-color-outline-variant);padding-right:8px}.cm-search{width:100%;margin-bottom:8px}.cm-right{padding-left:8px}.field-item{display:flex;align-items:center;justify-content:space-between;padding:8px 6px;cursor:pointer;border-radius:10px;transition:background .12s ease,border .12s ease;border:1px solid transparent}.field-item:hover{background:var(--md-sys-color-surface-container-high)}.field-item.selected{background:var(--md-sys-color-surface-container-highest);border-color:var(--md-sys-color-outline-variant)}.rule-badges{display:inline-flex;gap:6px}.rule-badge{font-size:11px;padding:2px 8px;border-radius:999px;font-weight:600}.b-active{background:var(--md-sys-color-primary-container);color:var(--md-sys-color-on-primary-container)}.b-manual{background:var(--md-sys-color-surface-container-high);color:var(--md-sys-color-on-surface-variant)}.b-immediate{background:var(--md-sys-color-secondary-container);color:var(--md-sys-color-on-secondary-container)}.b-conflict{background:var(--md-sys-color-error-container);color:var(--md-sys-color-on-error-container)}.cm-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px}.rules-empty{opacity:.8;font-style:italic;padding:8px 0;color:var(--md-sys-color-on-surface-variant)}.rule-row{display:grid;grid-template-columns:1fr auto;align-items:center;padding:10px 0;border-bottom:1px dashed var(--md-sys-color-outline-variant)}.rule-actions button{margin-left:4px}.banner{background:var(--md-sys-color-tertiary-container);color:var(--md-sys-color-on-tertiary-container);padding:10px 12px;border:1px solid var(--md-sys-color-outline-variant);border-radius:8px;margin-bottom:10px;display:flex;gap:8px;align-items:center}.import-panel{background:var(--md-sys-color-surface-container-high);border:1px solid var(--md-sys-color-outline-variant);border-radius:8px;padding:10px;margin-bottom:10px}.full{width:100%}.form-row{display:flex;gap:12px;align-items:center;flex-wrap:wrap}.form-grid{display:grid;gap:12px;padding:8px 0}.map-table{width:100%;border-collapse:collapse}.map-table th,.map-table td{padding:8px;border-bottom:1px dashed var(--md-sys-color-outline-variant)}.kbd{font-family:monospace;background:var(--md-sys-color-surface-container-highest);padding:0 6px;border-radius:4px;color:var(--md-sys-color-on-surface-variant)}.panel-surface{background:var(--md-sys-color-surface-container-high);border:1px solid var(--md-sys-color-outline-variant);border-radius:10px;box-shadow:var(--md-sys-elevation-level1, none);margin-bottom:12px}.panel-stack{display:grid;gap:12px;margin:12px 0}.stack-panel{background:var(--md-sys-color-surface-container-high);border:1px solid var(--md-sys-color-outline-variant);border-radius:10px;box-shadow:var(--md-sys-elevation-level1, none)}.stack-panel .mat-expansion-panel-content{padding:0 16px 12px}.panel-header{display:flex;align-items:center;gap:8px}.preview-box{background:var(--md-sys-color-surface-container-low);border:1px solid var(--md-sys-color-outline-variant);border-radius:8px;padding:8px;color:var(--md-sys-color-on-surface)}.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;margin-right:-4px}.help-icon-button mat-icon{font-size:18px;width:18px;height:18px}.mat-mdc-form-field-icon-suffix{align-self:center}\n"] }]
14600
+ `, styles: [":host{display:block;container-type:inline-size}.cm-root{display:grid;grid-template-columns:minmax(220px,280px) minmax(0,1fr);gap:16px;min-height:420px}.cm-left{border-right:1px solid var(--pfx-settings-panel-border, var(--md-sys-color-outline-variant));padding-right:8px;min-width:0}.cm-search{width:100%;margin-bottom:8px}.cm-right{padding-left:8px;min-width:0}@container (max-width: 720px){.cm-root{grid-template-columns:1fr}.cm-left{border-right:none;border-bottom:1px solid var(--pfx-settings-panel-border, var(--md-sys-color-outline-variant));padding-right:0;padding-bottom:12px}.cm-right{padding-left:0}}.field-item{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:8px;cursor:pointer;border-radius:10px;transition:background .12s ease,border .12s ease;border:1px solid transparent}.field-item:hover{background:var(--md-sys-color-surface-container-high)}.field-item.selected{background:var(--md-sys-color-surface-container-highest);border-color:var(--md-sys-color-outline-variant)}.field-item.has-cycle{border-color:var(--md-sys-color-error)}.field-item-main{min-width:0;flex:1}.field-item-title{font-weight:500;word-break:break-word}.field-item-meta{display:flex;flex-wrap:wrap;align-items:center;gap:6px;margin-top:2px;font-size:.75rem;color:var(--md-sys-color-on-surface-variant)}.field-role-pill{font-size:.7rem;padding:1px 6px;border-radius:999px;background:var(--md-sys-color-surface-container-high);color:var(--md-sys-color-on-surface-variant)}.field-role-pill--remote{background:color-mix(in srgb,var(--md-sys-color-primary-container) 65%,transparent);color:var(--md-sys-color-on-primary-container)}.rule-badges{display:inline-flex;gap:6px;flex-wrap:wrap;justify-content:flex-end}.rule-badge{font-size:11px;padding:2px 8px;border-radius:999px;font-weight:600;white-space:nowrap}.b-active{background:var(--md-sys-color-primary-container);color:var(--md-sys-color-on-primary-container)}.b-manual{background:var(--md-sys-color-surface-container-high);color:var(--md-sys-color-on-surface-variant)}.b-immediate{background:var(--md-sys-color-secondary-container);color:var(--md-sys-color-on-secondary-container)}.b-conflict{background:var(--md-sys-color-error-container);color:var(--md-sys-color-on-error-container)}.cm-header{display:flex;align-items:center;justify-content:space-between;gap:8px;flex-wrap:wrap;margin-bottom:8px}.cm-feedback{padding:8px 12px;border-radius:8px;font-size:.85rem;margin-bottom:10px;border:1px solid var(--md-sys-color-outline-variant);background:var(--md-sys-color-secondary-container);color:var(--md-sys-color-on-secondary-container)}.cm-feedback--error{border-color:var(--md-sys-color-error);background:var(--md-sys-color-error-container);color:var(--md-sys-color-on-error-container)}.cm-cycle-alert{padding:10px 12px;border-radius:8px;font-size:.85rem;margin-bottom:10px;border:1px solid var(--md-sys-color-error);background:var(--md-sys-color-error-container);color:var(--md-sys-color-on-error-container);display:flex;gap:10px;align-items:flex-start}.cm-cycle-alert mat-icon{color:var(--md-sys-color-error);flex-shrink:0;margin-top:2px}.cm-cycle-alert-body{display:grid;gap:4px;flex:1}.cm-source-notice{padding:8px 12px;border-radius:8px;font-size:.82rem;margin-bottom:10px;border:1px solid var(--md-sys-color-outline-variant);background:var(--md-sys-color-surface-container-low);color:var(--md-sys-color-on-surface-variant);display:flex;gap:8px;align-items:center}.rules-empty{opacity:.88;padding:8px 0;font-size:.85rem;color:var(--md-sys-color-on-surface-variant)}.rule-row{display:grid;grid-template-columns:1fr auto;align-items:center;padding:10px 0;border-bottom:1px dashed var(--md-sys-color-outline-variant);gap:8px}.rule-summary-direction{font-size:.85rem;margin-top:2px}.rule-summary-policies{font-size:.8rem;opacity:.78;margin-top:2px}.rule-actions button{margin-left:4px}.banner{background:var(--md-sys-color-tertiary-container);color:var(--md-sys-color-on-tertiary-container);padding:10px 12px;border:1px solid var(--md-sys-color-outline-variant);border-radius:8px;margin-bottom:10px;display:flex;gap:8px;align-items:center}.import-panel{background:var(--pfx-settings-panel-surface, var(--md-sys-color-surface-container-high));border:1px solid var(--pfx-settings-panel-border, var(--md-sys-color-outline-variant));border-radius:8px;padding:10px;margin-bottom:10px}.import-textarea{width:100%;box-sizing:border-box;padding:8px 10px;border-radius:8px;border:1px solid var(--pfx-settings-panel-border, var(--md-sys-color-outline-variant));background:var(--pfx-settings-panel-surface-elevated, var(--md-sys-color-surface-container-low));color:var(--pfx-settings-panel-fg, var(--md-sys-color-on-surface));font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.85rem}.full{width:100%}.form-row{display:flex;gap:12px;align-items:center;flex-wrap:wrap}.form-grid{display:grid;gap:12px;padding:8px 0}.section-caption{margin:0 0 6px;font-size:.82rem;color:var(--md-sys-color-on-surface-variant)}.map-table{width:100%;border-collapse:collapse}.map-table th,.map-table td{padding:8px;border-bottom:1px dashed var(--md-sys-color-outline-variant);text-align:left;vertical-align:middle}.kbd{font-family:monospace;background:var(--md-sys-color-surface-container-highest);padding:0 6px;border-radius:4px;color:var(--md-sys-color-on-surface-variant)}.panel-surface{padding-top:8px;margin-bottom:12px}.panel-stack{display:grid;gap:12px;margin:12px 0}.stack-panel{background:var(--pfx-settings-panel-surface, var(--md-sys-color-surface-container-high));border:1px solid var(--pfx-settings-panel-border, var(--md-sys-color-outline-variant));border-radius:10px;box-shadow:none}.stack-panel .mat-expansion-panel-content{padding:0 16px 12px}.panel-header{display:flex;align-items:center;gap:8px}.preview-box{background:var(--md-sys-color-surface-container-low);border:1px solid var(--md-sys-color-outline-variant);border-radius:8px;padding:8px;color:var(--md-sys-color-on-surface);margin:4px 0 0}.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;margin-right:-4px}.help-icon-button mat-icon{font-size:18px;width:18px;height:18px}.mat-mdc-form-field-icon-suffix{align-self:center}\n"] }]
13180
14601
  }], ctorParameters: () => [{ type: CascadeRulesService }], propDecorators: { fields: [{
13181
14602
  type: Input
13182
14603
  }], connections: [{
@@ -13185,6 +14606,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
13185
14606
  type: Output
13186
14607
  }], cancel: [{
13187
14608
  type: Output
14609
+ }], validChange: [{
14610
+ type: Output
13188
14611
  }] } });
13189
14612
 
13190
14613
  /**
@@ -13277,6 +14700,44 @@ const BASE_CAPABILITIES = [
13277
14700
  description: 'Ordered Json Logic presentation rules edited by a guided builder for common conditions and semantic visual effects.',
13278
14701
  safetyNotes: 'Rules may only affect tone, icon, label, badge, appearance and tooltip; they must not mutate submitted values or execute commands.',
13279
14702
  },
14703
+ {
14704
+ path: 'seed.quickViewVisible',
14705
+ category: 'appearance',
14706
+ valueKind: 'boolean',
14707
+ description: 'Whether this field is shown in Resource Quick View summary items when the resource is previewed.',
14708
+ },
14709
+ {
14710
+ path: 'seed.quickViewOrder',
14711
+ category: 'layout',
14712
+ valueKind: 'number',
14713
+ description: 'Numeric ordering priority for this field inside Resource Quick View summary items.',
14714
+ },
14715
+ {
14716
+ path: 'seed.quickView.previewSurfaceId',
14717
+ category: 'behavior',
14718
+ valueKind: 'string',
14719
+ description: 'Optional dedicated read-only surfaceId used to hydrate the Quick View card while preserving the canonical server recordOpen target.',
14720
+ safetyNotes: 'Must only refine the preview surface; never override backend-owned recordOpen.target.resourceKey or surfaceId.',
14721
+ },
14722
+ {
14723
+ path: 'seed.quickView.detailPresentation',
14724
+ category: 'layout',
14725
+ valueKind: 'enum',
14726
+ allowedValues: ['drawer', 'modal'],
14727
+ description: 'Preferred presentation mode (drawer or modal) when handing off from Quick View to the full detail surface.',
14728
+ },
14729
+ {
14730
+ path: 'seed.quickViewActionLabel',
14731
+ category: 'identity',
14732
+ valueKind: 'string',
14733
+ description: 'Accessible custom label for the Quick View trigger action on lookup and select controls.',
14734
+ },
14735
+ {
14736
+ path: 'seed.actions.showQuickView',
14737
+ category: 'behavior',
14738
+ valueKind: 'boolean',
14739
+ description: 'Controls whether the explicit Quick View action button is shown when a governed reference target is present.',
14740
+ },
13280
14741
  {
13281
14742
  path: 'seed.shortcuts',
13282
14743
  category: 'behavior',
@@ -13333,6 +14794,12 @@ const fieldMetadataPropertySchema = {
13333
14794
  'presentation.badge',
13334
14795
  'presentation.tooltip',
13335
14796
  'presentationRules',
14797
+ 'quickViewVisible',
14798
+ 'quickViewOrder',
14799
+ 'quickViewActionLabel',
14800
+ 'actions.showQuickView',
14801
+ 'quickView.previewSurfaceId',
14802
+ 'quickView.detailPresentation',
13336
14803
  'options',
13337
14804
  'optionSource',
13338
14805
  'validators',
@@ -13371,13 +14838,21 @@ const optionSourceSchema = {
13371
14838
  secondaryPropertyPaths: { type: 'array' },
13372
14839
  badgePropertyPaths: { type: 'array' },
13373
14840
  statusToneMap: { type: 'object' },
14841
+ actions: {
14842
+ type: 'object',
14843
+ properties: {
14844
+ showQuickView: { type: 'boolean' },
14845
+ },
14846
+ },
13374
14847
  },
13375
14848
  },
13376
14849
  detail: {
13377
14850
  type: 'object',
13378
14851
  properties: {
13379
14852
  kind: { enum: ['surface', 'route', 'href'] },
14853
+ resourceKey: { type: 'string' },
13380
14854
  surfaceId: { type: 'string' },
14855
+ previewSurfaceId: { type: 'string' },
13381
14856
  presentation: { enum: ['drawer', 'modal'] },
13382
14857
  preferredWidget: { type: 'string' },
13383
14858
  mode: { enum: ['view', 'edit', 'create'] },