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

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)',
@@ -655,6 +653,8 @@ const METADATA_EDITOR_EN_US = {
655
653
  'praxis.metadataEditor.colorPalette.moveDown': 'Move down',
656
654
  'praxis.metadataEditor.colorPalette.remove': 'Remove',
657
655
  'praxis.metadataEditor.colorPalette.invalid': 'Enter a valid CSS color or var(--token).',
656
+ 'praxis.metadataEditor.smartCalibrate.action': 'Calibrate from schema',
657
+ 'praxis.metadataEditor.smartCalibrate.tooltip': 'Automatically infers control type (Currency, CPF/CNPJ, Date, Email, Phone, Select, Textarea), humanized label, and corporate defaults from field name and metadata.',
658
658
  };
659
659
 
660
660
  const METADATA_EDITOR_PT_BR = {
@@ -694,6 +694,7 @@ const METADATA_EDITOR_PT_BR = {
694
694
  'praxis.metadataEditor.arrayFields.control.date': 'Data',
695
695
  'praxis.metadataEditor.fieldResource.label': 'Recurso governado',
696
696
  'praxis.metadataEditor.fieldResource.searchPlaceholder': 'Buscar por nome, dom\u00ednio ou caminho',
697
+ 'praxis.metadataEditor.fieldResource.fieldSearchPlaceholder': 'Buscar campo por nome ou r\u00f3tulo',
697
698
  'praxis.metadataEditor.fieldResource.hint': 'Selecione no cat\u00e1logo governado do host. O valor gravado continua sendo resourcePath.',
698
699
  'praxis.metadataEditor.common.help': 'Ajuda',
699
700
  'praxis.metadataEditor.common.iconMaterialSymbolsHint': 'Use o nome do \u00edcone (Material Symbols)',
@@ -906,6 +907,8 @@ const METADATA_EDITOR_PT_BR = {
906
907
  'praxis.metadataEditor.colorPalette.moveDown': 'Mover para baixo',
907
908
  'praxis.metadataEditor.colorPalette.remove': 'Remover',
908
909
  'praxis.metadataEditor.colorPalette.invalid': 'Informe uma cor CSS válida ou var(--token).',
910
+ 'praxis.metadataEditor.smartCalibrate.action': 'Calibrar pelo schema',
911
+ 'praxis.metadataEditor.smartCalibrate.tooltip': 'Infere automaticamente o tipo de controle (Moeda, CPF/CNPJ, Data, E-mail, Telefone, Seleção, Texto longo), rótulo humanizado e padrões corporativos a partir do nome e metadados do campo.',
909
912
  };
910
913
 
911
914
  function createPraxisMetadataEditorI18nConfig(options = {}) {
@@ -2721,6 +2724,9 @@ class DynamicEditorRendererComponent {
2721
2724
  return options.map((option) => ({
2722
2725
  ...option,
2723
2726
  text: this.resolveEditorText(option.textKey, option.text) ?? option.text,
2727
+ code: option.code ?? undefined,
2728
+ badge: option.badge ?? undefined,
2729
+ description: option.description ?? undefined,
2724
2730
  }));
2725
2731
  }
2726
2732
  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 +2880,7 @@ class DynamicEditorRendererComponent {
2874
2880
  }
2875
2881
  </mat-accordion>
2876
2882
  </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 });
2883
+ `, 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
2884
  }
2879
2885
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: DynamicEditorRendererComponent, decorators: [{
2880
2886
  type: Component,
@@ -3041,7 +3047,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
3041
3047
  }
3042
3048
  </mat-accordion>
3043
3049
  </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"] }]
3050
+ `, 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
3051
  }], ctorParameters: () => [{ type: i1$1.MatDialog }, { type: i2$2.IconPickerService }, { type: i2$2.PraxisI18nService, decorators: [{
3046
3052
  type: Optional
3047
3053
  }] }], propDecorators: { properties: [{
@@ -4482,7 +4488,7 @@ const entityLookupProperties = [
4482
4488
  { value: 'compact', text: 'compact' },
4483
4489
  { value: 'token', text: 'token' },
4484
4490
  ],
4485
- hint: 'Layout preferencial do valor selecionado publicado pela option-source.',
4491
+ hint: 'Layout preferencial do valor selecionado publicado pela option-source (default compact/inline em formularios; card para revisoes detalhadas).',
4486
4492
  },
4487
4493
  {
4488
4494
  name: 'optionSource.display.resultLayout',
@@ -4606,7 +4612,7 @@ const entityLookupProperties = [
4606
4612
  { value: 'compact', text: 'compact' },
4607
4613
  { value: 'token', text: 'token' },
4608
4614
  ],
4609
- hint: 'Layout do item selecionado no trigger ou card principal.',
4615
+ hint: 'Layout do item selecionado no trigger (default compact/inline; card opt-in para superficie expandida).',
4610
4616
  },
4611
4617
  {
4612
4618
  name: 'resultLayout',
@@ -5101,7 +5107,7 @@ const entityLookupProperties = [
5101
5107
  label: 'Acao: trocar',
5102
5108
  editorType: 'checkbox',
5103
5109
  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.',
5110
+ 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
5111
  },
5106
5112
  {
5107
5113
  name: 'actions.showCopyCode',
@@ -5546,6 +5552,46 @@ const numberProperties = [
5546
5552
  { name: 'step', label: 'Step (incremento)', editorType: 'number', group: 'Formato/Comportamento', hint: 'Incremento em unidades; combine com min/max (ex.: 0.01).' },
5547
5553
  { name: 'numberFormat.decimalPlaces', label: 'Casas decimais', editorType: 'number', group: 'Formato/Comportamento', row: 'formato.decimais', inline: true, hint: 'Precisão exibida/aceita (ex.: 2).' },
5548
5554
  { 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.' },
5555
+ { 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.' },
5556
+ {
5557
+ name: 'numericFormat',
5558
+ label: 'Formato numérico canônico',
5559
+ editorType: 'select',
5560
+ group: 'Formato/Comportamento',
5561
+ options: [
5562
+ { value: 'decimal', text: 'Decimal' },
5563
+ { value: 'integer', text: 'Inteiro' },
5564
+ { value: 'currency', text: 'Moeda' },
5565
+ { value: 'percent', text: 'Percentual (Percent Studio)' },
5566
+ { value: 'scientific', text: 'Científico' },
5567
+ ],
5568
+ hint: 'Formato semântico do campo numérico vindo do contrato x-ui.',
5569
+ },
5570
+ {
5571
+ name: 'showPercentStudio',
5572
+ label: 'Exibir barra Percent Studio no formulário',
5573
+ editorType: 'checkbox',
5574
+ group: 'Formato/Comportamento',
5575
+ hint: 'Mostra o trilho interativo, presets de quartis e steppers rápidos abaixo do pdx-number-input quando em modo percentual.',
5576
+ },
5577
+ {
5578
+ name: 'inlineVisualStyle',
5579
+ label: 'Estilo visual inline (percentual)',
5580
+ editorType: 'select',
5581
+ group: 'Formato/Comportamento',
5582
+ options: [
5583
+ { value: 'compact', text: 'Compacto (Pílula líquida + popover)' },
5584
+ { value: 'graphic', text: 'Gráfico expandido (Card com donut + slider)' },
5585
+ ],
5586
+ hint: 'Define a apresentação de filtros numéricos percentuais em superfícies inline.',
5587
+ },
5588
+ {
5589
+ name: 'percentPresets',
5590
+ label: 'Presets rápidos de percentual',
5591
+ editorType: 'json',
5592
+ group: 'Formato/Comportamento',
5593
+ hint: 'Lista de atalhos rápidos no Percent Studio (ex.: [0, 25, 50, 75, 100] ou [{"label": "Meta", "value": 12.5}]).',
5594
+ },
5549
5595
  {
5550
5596
  name: 'inputMode',
5551
5597
  label: 'Modo de entrada',
@@ -5694,6 +5740,16 @@ const dateProperties = [
5694
5740
  { value: 'year', text: 'Ano' },
5695
5741
  { value: 'multi-year', text: 'Décadas' },
5696
5742
  ] },
5743
+ { name: 'defaultGranularity', label: 'Granularidade de seleção (inline)', editorType: 'select', group: 'Formato/Comportamento', options: [
5744
+ { value: 'day', text: 'Dia' },
5745
+ { value: 'month', text: 'Mês/Ano (1º dia do mês)' },
5746
+ { value: 'year', text: 'Ano (01/01 do ano)' },
5747
+ ], hint: 'No inlineDate, permite selecionar diretamente o ano ou mês/ano sem navegar pelos dias.' },
5748
+ { name: 'inlineChipDisplay', label: 'Exibição no chip (inline)', editorType: 'select', group: 'Formato/Comportamento', options: [
5749
+ { value: 'value', text: 'Apenas valor' },
5750
+ { value: 'label-value', text: 'Rótulo + valor (ex.: Data: Mai/2026)' },
5751
+ ], hint: 'Define como o valor preenchido é exibido na pílula fechada do inlineDate.' },
5752
+ { 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
5753
  { name: 'startAt', label: 'Abrir em', editorType: 'date', group: 'Formato/Comportamento' },
5698
5754
  { name: 'touchUi', label: 'Touch UI', editorType: 'checkbox', group: 'Formato/Comportamento', row: 'fmt.inline1', inline: true },
5699
5755
  { name: 'closeOnSelect', label: 'Fechar ao selecionar', editorType: 'checkbox', group: 'Formato/Comportamento', row: 'fmt.inline1', inline: true },
@@ -5823,6 +5879,19 @@ const dateRangeProperties = [
5823
5879
  { value: 'multi-year', text: 'Décadas' },
5824
5880
  ], hint: 'Visão mostrada ao abrir o calendário (padrão: mês).'
5825
5881
  },
5882
+ {
5883
+ name: 'defaultGranularity', label: 'Granularidade inicial (inline)', editorType: 'select', group: 'Formato/Comportamento', options: [
5884
+ { value: 'day', text: 'Dia (calendário)' },
5885
+ { value: 'month', text: 'Mês/Ano (competência)' },
5886
+ { value: 'year', text: 'Ano (exercício)' },
5887
+ ], hint: 'Define a aba inicial do seletor rápido de período no inlineDateRange (Dia, Mês/Ano ou Ano).'
5888
+ },
5889
+ {
5890
+ name: 'inlineChipDisplay', label: 'Exibição no chip (inline)', editorType: 'select', group: 'Formato/Comportamento', options: [
5891
+ { value: 'value', text: 'Apenas valor (ex.: Q2/2026)' },
5892
+ { value: 'label-value', text: 'Rótulo + valor (ex.: Período: Q2/2026)' },
5893
+ ], hint: 'Define como o período selecionado é exibido na pílula fechada do inlineDateRange.'
5894
+ },
5826
5895
  { 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
5896
  { 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
5897
  // Validação
@@ -7695,6 +7764,7 @@ const rangeSliderProperties = [
7695
7764
  { value: 'compact', text: 'Compacto' },
7696
7765
  { value: 'storage', text: 'Armazenamento' },
7697
7766
  ], hint: 'Formatter declarativo para labels e aria-valuetext.' },
7767
+ { 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
7768
  { name: 'showTicks', label: 'Ticks', editorType: 'select', group: 'Formato/Comportamento', options: [
7699
7769
  { value: false, text: 'Sem ticks' },
7700
7770
  { value: true, text: 'Com ticks' },
@@ -10709,6 +10779,93 @@ function mergeEditorProperties(base, contextual) {
10709
10779
  }
10710
10780
  return merged;
10711
10781
  }
10782
+ const inlinePhoneEditorProperties = mergeEditorProperties(inputProperties, phoneInputProperties);
10783
+ const inlinePeriodRangeEditorProperties = mergeEditorProperties(rangeSliderProperties, [
10784
+ {
10785
+ name: 'granularity',
10786
+ label: 'Granularidade do período',
10787
+ editorType: 'select',
10788
+ group: 'Formato/Comportamento',
10789
+ options: [
10790
+ { value: 'month', text: 'Mês (1-12 ou YYYY-MM)' },
10791
+ { value: 'year', text: 'Ano (YYYY)' },
10792
+ ],
10793
+ hint: 'Define se o seletor opera em competência mensal ou exercício anual.',
10794
+ },
10795
+ {
10796
+ name: 'defaultGranularity',
10797
+ label: 'Granularidade inicial',
10798
+ editorType: 'select',
10799
+ group: 'Formato/Comportamento',
10800
+ options: [
10801
+ { value: 'month', text: 'Mês' },
10802
+ { value: 'year', text: 'Ano' },
10803
+ ],
10804
+ hint: 'Granularidade inicial aberta no painel.',
10805
+ },
10806
+ {
10807
+ name: 'selectionGrid',
10808
+ label: 'Exibir grade de seleção (Jan-Dez / Anos)',
10809
+ editorType: 'checkbox',
10810
+ group: 'Formato/Comportamento',
10811
+ hint: 'Mostra botões clicáveis de meses ou anos para seleção de único item ou faixa.',
10812
+ },
10813
+ {
10814
+ name: 'showSlider',
10815
+ label: 'Exibir slider complementar',
10816
+ editorType: 'checkbox',
10817
+ group: 'Formato/Comportamento',
10818
+ hint: 'Mostra barra deslizante sincronizada com a grade de meses/anos.',
10819
+ },
10820
+ {
10821
+ name: 'inlineChipDisplay',
10822
+ label: 'Exibição no chip (inline)',
10823
+ editorType: 'select',
10824
+ group: 'Formato/Comportamento',
10825
+ options: [
10826
+ { value: 'badge', text: 'Rótulo + badge (ex.: Mês: Jan – Jun)' },
10827
+ { value: 'value', text: 'Apenas valor (ex.: Jan – Jun)' },
10828
+ { value: 'label-value', text: 'Rótulo + valor' },
10829
+ ],
10830
+ hint: 'Define como o intervalo selecionado aparece na pílula fechada.',
10831
+ },
10832
+ {
10833
+ name: 'minYear',
10834
+ label: 'Ano mínimo',
10835
+ editorType: 'number',
10836
+ group: 'Validação',
10837
+ row: 'val.yearminmax',
10838
+ inline: true,
10839
+ hint: 'Ano inicial exibido na grade de anos (ex.: 2020).',
10840
+ },
10841
+ {
10842
+ name: 'maxYear',
10843
+ label: 'Ano máximo',
10844
+ editorType: 'number',
10845
+ group: 'Validação',
10846
+ row: 'val.yearminmax',
10847
+ inline: true,
10848
+ hint: 'Ano final exibido na grade de anos (ex.: 2030).',
10849
+ },
10850
+ {
10851
+ name: 'minPeriod',
10852
+ label: 'Período mínimo',
10853
+ editorType: 'text',
10854
+ group: 'Validação',
10855
+ row: 'val.periodminmax',
10856
+ inline: true,
10857
+ hint: 'Limite inferior de competência (ex.: 2020-01 ou 1).',
10858
+ },
10859
+ {
10860
+ name: 'maxPeriod',
10861
+ label: 'Período máximo',
10862
+ editorType: 'text',
10863
+ group: 'Validação',
10864
+ row: 'val.periodminmax',
10865
+ inline: true,
10866
+ hint: 'Limite superior de competência (ex.: 2027-12 ou 12).',
10867
+ },
10868
+ ]);
10712
10869
  function hasCanonicalDocumentSemantics(metadata) {
10713
10870
  if (!metadata || typeof metadata !== 'object' || Array.isArray(metadata)) {
10714
10871
  return false;
@@ -10724,12 +10881,12 @@ const INLINE_FILTER_EDITOR_PROPERTIES_BY_CONTROL_TYPE = Object.freeze({
10724
10881
  [INLINE_FILTER_CONTROL_TYPES.AUTOCOMPLETE]: selectProperties,
10725
10882
  [INLINE_FILTER_CONTROL_TYPES.MULTI_SELECT]: selectProperties,
10726
10883
  [INLINE_FILTER_CONTROL_TYPES.INPUT]: inputProperties,
10727
- [INLINE_FILTER_CONTROL_TYPES.PHONE]: inputProperties,
10884
+ [INLINE_FILTER_CONTROL_TYPES.PHONE]: inlinePhoneEditorProperties,
10728
10885
  [INLINE_FILTER_CONTROL_TYPES.TOGGLE]: toggleProperties,
10729
10886
  [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,
10887
+ [INLINE_FILTER_CONTROL_TYPES.PERIOD_RANGE]: inlinePeriodRangeEditorProperties,
10888
+ [INLINE_FILTER_CONTROL_TYPES.YEAR_RANGE]: inlinePeriodRangeEditorProperties,
10889
+ [INLINE_FILTER_CONTROL_TYPES.MONTH_RANGE]: inlinePeriodRangeEditorProperties,
10733
10890
  [INLINE_FILTER_CONTROL_TYPES.DATE]: dateProperties,
10734
10891
  [INLINE_FILTER_CONTROL_TYPES.DATE_RANGE]: dateRangeProperties,
10735
10892
  [INLINE_FILTER_CONTROL_TYPES.TIME]: timePickerProperties,
@@ -10771,7 +10928,11 @@ class FieldMetadataEditorComponent {
10771
10928
  normalizedProps = [];
10772
10929
  renderedProps = [];
10773
10930
  initialSnapshot = null;
10931
+ initialControlType;
10932
+ initialSeed;
10774
10933
  baselineReady = false;
10934
+ formRebuild$ = new Subject();
10935
+ hostApplyPatch;
10775
10936
  // Lista de tipos suportados no editor (curada para os tipos com config disponível)
10776
10937
  typeOptions = [
10777
10938
  { value: FieldControlType.INPUT, label: 'Texto (Input)' },
@@ -10824,6 +10985,9 @@ class FieldMetadataEditorComponent {
10824
10985
  isValid$ = new BehaviorSubject(true);
10825
10986
  isBusy$ = new BehaviorSubject(false);
10826
10987
  destroyRef = inject(DestroyRef);
10988
+ resourceDiscovery = inject(ResourceDiscoveryService, { optional: true });
10989
+ i18n = inject(PraxisI18nService, { optional: true });
10990
+ discoveredResourceOptions = [];
10827
10991
  lastCurrencyPreset = null;
10828
10992
  constructor(registry, normalizer, factory, ctxValidators) {
10829
10993
  this.registry = registry;
@@ -10838,10 +11002,7 @@ class FieldMetadataEditorComponent {
10838
11002
  if (data.seed)
10839
11003
  this.seed = data.seed;
10840
11004
  // Bridge: allow host to receive patches even if saved$/applied$ are missed
10841
- try {
10842
- this.__hostApplyPatch = data.hostBridge?.applyPatch;
10843
- }
10844
- catch { }
11005
+ this.hostApplyPatch = data.hostBridge?.applyPatch;
10845
11006
  }
10846
11007
  // Normalize legacy avatar seed (root imageSrc/imageAlt -> extra.* for editor binding)
10847
11008
  try {
@@ -10863,14 +11024,214 @@ class FieldMetadataEditorComponent {
10863
11024
  }
10864
11025
  catch { }
10865
11026
  }
11027
+ catalogLookupRequested = false;
11028
+ resourceSchemaFieldsByPath = new Map();
11029
+ resourceSchemaRequestIdByScope = {
11030
+ root: 0,
11031
+ optionSource: 0,
11032
+ };
10866
11033
  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
11034
  this.rebuildEditorForType(this.controlType);
10873
11035
  }
11036
+ loadCatalogResourceOptionsIfNeeded() {
11037
+ const hasResourcePathProp = this.normalizedProps.some((prop) => prop?.name === 'resourcePath' ||
11038
+ prop?.name === 'optionSource.resourcePath');
11039
+ if (!hasResourcePathProp) {
11040
+ return;
11041
+ }
11042
+ const optionSourceTypeCtrl = this.form?.get('optionSource.type');
11043
+ if (optionSourceTypeCtrl) {
11044
+ optionSourceTypeCtrl.valueChanges
11045
+ .pipe(takeUntil(this.formRebuild$), takeUntilDestroyed(this.destroyRef))
11046
+ .subscribe(() => {
11047
+ if (this.discoveredResourceOptions.length > 0 && this.normalizedProps.length > 0) {
11048
+ this.renderedProps = this.buildRenderedProps(this.normalizedProps);
11049
+ }
11050
+ });
11051
+ }
11052
+ this.bindResourcePathSchemaSync('resourcePath', 'root');
11053
+ this.bindResourcePathSchemaSync('optionSource.resourcePath', 'optionSource');
11054
+ if (this.catalogLookupRequested || !this.resourceDiscovery) {
11055
+ return;
11056
+ }
11057
+ this.catalogLookupRequested = true;
11058
+ const endpointKey = this.resolveDiscoveryEndpointKey();
11059
+ this.resourceDiscovery
11060
+ .getDiscoveredResources({
11061
+ intent: 'any',
11062
+ ...(endpointKey ? { endpointKey } : {}),
11063
+ })
11064
+ .pipe(catchError(() => of([])), takeUntilDestroyed(this.destroyRef))
11065
+ .subscribe((options) => {
11066
+ this.discoveredResourceOptions = options;
11067
+ this.seedCatalogResponseFieldsCache(options);
11068
+ if (options.length > 0 && this.normalizedProps.length > 0) {
11069
+ this.renderedProps = this.buildRenderedProps(this.normalizedProps);
11070
+ }
11071
+ });
11072
+ }
11073
+ resolveDiscoveryEndpointKey() {
11074
+ const raw = this.form?.get('endpointKey')?.value ??
11075
+ this.form?.get('apiEndpointKey')?.value ??
11076
+ this.seed?.endpointKey ??
11077
+ this.seed?.apiEndpointKey;
11078
+ const trimmed = String(raw ?? '').trim();
11079
+ return trimmed || undefined;
11080
+ }
11081
+ bindResourcePathSchemaSync(controlPath, scope) {
11082
+ const ctrl = this.form?.get(controlPath);
11083
+ if (!ctrl) {
11084
+ return;
11085
+ }
11086
+ let lastPath = String(ctrl.value ?? '').trim();
11087
+ if (lastPath) {
11088
+ this.resolveAndApplyResourceSchemaFields(lastPath, scope, false);
11089
+ }
11090
+ ctrl.valueChanges
11091
+ .pipe(takeUntil(this.formRebuild$), takeUntilDestroyed(this.destroyRef))
11092
+ .subscribe((nextValue) => {
11093
+ const nextPath = String(nextValue ?? '').trim();
11094
+ if (nextPath === lastPath) {
11095
+ return;
11096
+ }
11097
+ lastPath = nextPath;
11098
+ if (!nextPath) {
11099
+ if (this.normalizedProps.length > 0) {
11100
+ this.renderedProps = this.buildRenderedProps(this.normalizedProps);
11101
+ }
11102
+ return;
11103
+ }
11104
+ this.resolveAndApplyResourceSchemaFields(nextPath, scope, this.baselineReady);
11105
+ });
11106
+ }
11107
+ seedCatalogResponseFieldsCache(options) {
11108
+ for (const option of options) {
11109
+ if (!option?.responseFields?.length) {
11110
+ continue;
11111
+ }
11112
+ const mappedMetas = mapCatalogFieldsToMetadata(option.responseFields);
11113
+ const metaByName = new Map(mappedMetas.map((meta) => [meta.name, meta]));
11114
+ const definitions = option.responseFields
11115
+ .filter((field) => Boolean(field?.name && String(field.name).trim()))
11116
+ .map((field) => {
11117
+ const name = String(field.name).trim();
11118
+ const rawLabel = String(field.label ?? '').trim();
11119
+ const label = rawLabel && rawLabel !== name
11120
+ ? rawLabel
11121
+ : humanizeCatalogFieldName(name);
11122
+ const mapped = metaByName.get(name);
11123
+ return {
11124
+ name,
11125
+ label,
11126
+ type: String(field.type ?? field.controlType ?? 'string').trim() ||
11127
+ 'string',
11128
+ controlType: String(field.controlType ?? mapped?.controlType ?? '').trim() ||
11129
+ undefined,
11130
+ description: String(field.description ?? '').trim() || undefined,
11131
+ ...(typeof field.format === 'string' && field.format.trim()
11132
+ ? { format: field.format.trim() }
11133
+ : {}),
11134
+ ...(typeof field.required === 'boolean'
11135
+ ? { required: field.required }
11136
+ : {}),
11137
+ ...(typeof field.minimum === 'number'
11138
+ ? { minimum: field.minimum }
11139
+ : {}),
11140
+ ...(typeof field.maximum === 'number'
11141
+ ? { maximum: field.maximum }
11142
+ : {}),
11143
+ ...(typeof field.minLength === 'number'
11144
+ ? { minLength: field.minLength }
11145
+ : {}),
11146
+ ...(typeof field.maxLength === 'number'
11147
+ ? { maxLength: field.maxLength }
11148
+ : {}),
11149
+ ...(typeof field.pattern === 'string' && field.pattern.trim()
11150
+ ? { pattern: field.pattern.trim() }
11151
+ : {}),
11152
+ ...(Array.isArray(field.enumValues)
11153
+ ? { enumValues: field.enumValues }
11154
+ : {}),
11155
+ ...(mapped?.options?.length
11156
+ ? {
11157
+ options: mapped.options.map((opt) => {
11158
+ const optLabel = String(opt.label ?? opt.text ?? opt.value);
11159
+ return {
11160
+ key: String(opt.value),
11161
+ value: String(opt.value),
11162
+ label: optLabel,
11163
+ text: opt.text ?? optLabel,
11164
+ };
11165
+ }),
11166
+ }
11167
+ : {}),
11168
+ };
11169
+ });
11170
+ if (!definitions.length) {
11171
+ continue;
11172
+ }
11173
+ const candidateKeys = new Set();
11174
+ for (const candidate of [option.value, option.path, option.resourceKey]) {
11175
+ const raw = String(candidate ?? '').trim();
11176
+ if (!raw)
11177
+ continue;
11178
+ const normalized = this.normalizeResourceCacheKey(raw);
11179
+ if (normalized)
11180
+ candidateKeys.add(normalized);
11181
+ const stripped = raw.replace(/^\/+|\/+$/g, '');
11182
+ if (stripped)
11183
+ candidateKeys.add(stripped);
11184
+ const withoutApi = stripped.replace(/^api\/+/i, '');
11185
+ if (withoutApi)
11186
+ candidateKeys.add(withoutApi);
11187
+ }
11188
+ for (const cacheKey of candidateKeys) {
11189
+ if (!this.resourceSchemaFieldsByPath.has(cacheKey)) {
11190
+ this.resourceSchemaFieldsByPath.set(cacheKey, definitions);
11191
+ }
11192
+ }
11193
+ }
11194
+ }
11195
+ resolveAndApplyResourceSchemaFields(resourcePath, scope, autoSuggestDefaults) {
11196
+ const trimmed = String(resourcePath ?? '').trim();
11197
+ if (!trimmed) {
11198
+ return;
11199
+ }
11200
+ const cached = this.getCachedFieldsForResource(trimmed);
11201
+ if (cached.length > 0) {
11202
+ if (autoSuggestDefaults) {
11203
+ this.applyAutoSuggestedDependentKeys(scope, cached);
11204
+ }
11205
+ if (this.normalizedProps.length > 0) {
11206
+ this.renderedProps = this.buildRenderedProps(this.normalizedProps);
11207
+ }
11208
+ }
11209
+ if (typeof this.resourceDiscovery?.getResourceSchemaFields !== 'function') {
11210
+ return;
11211
+ }
11212
+ const requestId = ++this.resourceSchemaRequestIdByScope[scope];
11213
+ const endpointKey = this.resolveDiscoveryEndpointKey();
11214
+ const request$ = endpointKey
11215
+ ? this.resourceDiscovery.getResourceSchemaFields(trimmed, { endpointKey })
11216
+ : this.resourceDiscovery.getResourceSchemaFields(trimmed);
11217
+ request$
11218
+ .pipe(catchError(() => of([])), takeUntilDestroyed(this.destroyRef))
11219
+ .subscribe((fields) => {
11220
+ if (requestId !== this.resourceSchemaRequestIdByScope[scope]) {
11221
+ return;
11222
+ }
11223
+ const validFields = (fields || []).filter((field) => Boolean(field?.name && String(field.name).trim()));
11224
+ if (validFields.length > 0) {
11225
+ this.setCachedFieldsForResource(trimmed, validFields);
11226
+ if (autoSuggestDefaults) {
11227
+ this.applyAutoSuggestedDependentKeys(scope, validFields);
11228
+ }
11229
+ }
11230
+ if (this.normalizedProps.length > 0) {
11231
+ this.renderedProps = this.buildRenderedProps(this.normalizedProps);
11232
+ }
11233
+ });
11234
+ }
10874
11235
  resolveCanonicalEditorProperties(controlType, controlTypeToken) {
10875
11236
  const inlineProps = resolveInlineFilterEditorProperties(controlTypeToken, this.seed);
10876
11237
  if (inlineProps?.length)
@@ -10965,6 +11326,13 @@ class FieldMetadataEditorComponent {
10965
11326
  return undefined;
10966
11327
  }
10967
11328
  rebuildEditorForType(ct) {
11329
+ this.formRebuild$.next();
11330
+ if (this.initialControlType === undefined) {
11331
+ this.initialControlType = ct;
11332
+ }
11333
+ if (this.initialSeed === undefined) {
11334
+ this.initialSeed = this.cloneSnapshot(this.seed ?? {});
11335
+ }
10968
11336
  this.controlType = ct;
10969
11337
  let props = this.registry.getProperties(this.controlType);
10970
11338
  const controlTypeToken = normalizeControlTypeToken(this.controlType);
@@ -10985,23 +11353,6 @@ class FieldMetadataEditorComponent {
10985
11353
  }
10986
11354
  }
10987
11355
  }
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
11356
  // Fallback de registro mínimo para facilitar execução inicial
11006
11357
  if (!props.length && canonicalProps?.length) {
11007
11358
  this.registry.register(this.controlType, canonicalProps);
@@ -11174,12 +11525,6 @@ class FieldMetadataEditorComponent {
11174
11525
  props = treeSelectProperties;
11175
11526
  }
11176
11527
  }
11177
- try {
11178
- (console.log || console.debug)('[FieldMetadataEditor] props after registry resolution', {
11179
- resolvedProps: props.map((p) => p.name),
11180
- });
11181
- }
11182
- catch { }
11183
11528
  // Inject controlType selector into 'Geral' group so users can change field type
11184
11529
  try {
11185
11530
  const typeProp = {
@@ -11198,28 +11543,6 @@ class FieldMetadataEditorComponent {
11198
11543
  }
11199
11544
  catch { }
11200
11545
  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
11546
  // Backward-compatible bridge: if a config still uses providerOptions, hydrate from seed.options.
11224
11547
  try {
11225
11548
  const hasProviderOptions = this.normalizedProps.some((p) => p.name === 'providerOptions');
@@ -11232,10 +11555,6 @@ class FieldMetadataEditorComponent {
11232
11555
  return String(seedOptions);
11233
11556
  } })();
11234
11557
  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
11558
  }
11240
11559
  }
11241
11560
  catch { }
@@ -11282,61 +11601,19 @@ class FieldMetadataEditorComponent {
11282
11601
  this.form.patchValue(subset, { emitEvent: false });
11283
11602
  }
11284
11603
  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
11604
  }
11332
11605
  catch { }
11333
11606
  // Capture baseline after seed is applied to avoid missing first user change
11334
11607
  try {
11335
- this.initialSnapshot = this.form.getRawValue();
11608
+ if (this.initialSnapshot == null) {
11609
+ this.initialSnapshot = this.form.getRawValue();
11610
+ }
11336
11611
  this.baselineReady = true;
11337
- if (this.isDirty$.value)
11612
+ const controlTypeChanged = this.initialControlType !== undefined &&
11613
+ this.controlType !== this.initialControlType;
11614
+ if (!controlTypeChanged && this.isDirty$.value) {
11338
11615
  this.isDirty$.next(false);
11339
- (console.log || console.debug)('[FieldMetadataEditor] baseline captured (immediate)');
11616
+ }
11340
11617
  }
11341
11618
  catch {
11342
11619
  this.baselineReady = true;
@@ -11344,12 +11621,12 @@ class FieldMetadataEditorComponent {
11344
11621
  // track validity/dirty with snapshot-based detection (avoid false dirty on init)
11345
11622
  this.isValid$.next(this.form.valid);
11346
11623
  this.form.statusChanges
11347
- .pipe(takeUntilDestroyed(this.destroyRef))
11624
+ .pipe(takeUntil(this.formRebuild$), takeUntilDestroyed(this.destroyRef))
11348
11625
  .subscribe(() => this.isValid$.next(this.form.valid));
11349
11626
  this.setupGradientStopValidationSync();
11350
11627
  this.setupCurrencyPresetSync();
11351
11628
  this.form.valueChanges
11352
- .pipe(takeUntilDestroyed(this.destroyRef))
11629
+ .pipe(takeUntil(this.formRebuild$), takeUntilDestroyed(this.destroyRef))
11353
11630
  .subscribe((val) => {
11354
11631
  if (!this.baselineReady) {
11355
11632
  // Ignore initial churn until baseline is captured
@@ -11359,12 +11636,10 @@ class FieldMetadataEditorComponent {
11359
11636
  this.initialSnapshot = val;
11360
11637
  if (this.isDirty$.value)
11361
11638
  this.isDirty$.next(false);
11362
- try {
11363
- (console.log || console.debug)('[FieldMetadataEditor] baseline set from first stable value');
11364
- }
11365
- catch { }
11366
11639
  return;
11367
11640
  }
11641
+ const controlTypeChanged = this.initialControlType !== undefined &&
11642
+ this.controlType !== this.initialControlType;
11368
11643
  let same = false;
11369
11644
  try {
11370
11645
  same = JSON.stringify(val) === JSON.stringify(this.initialSnapshot);
@@ -11372,15 +11647,15 @@ class FieldMetadataEditorComponent {
11372
11647
  catch {
11373
11648
  same = false;
11374
11649
  }
11375
- const dirty = !same;
11650
+ const dirty = controlTypeChanged || !same;
11376
11651
  if (dirty !== this.isDirty$.value) {
11377
- try {
11378
- (console.log || console.debug)('[FieldMetadataEditor] dirty ->', dirty);
11379
- }
11380
- catch { }
11381
11652
  this.isDirty$.next(dirty);
11382
11653
  }
11383
11654
  });
11655
+ if (this.discoveredResourceOptions.length > 0) {
11656
+ this.renderedProps = this.buildRenderedProps(this.normalizedProps);
11657
+ }
11658
+ this.loadCatalogResourceOptionsIfNeeded();
11384
11659
  }
11385
11660
  setupGradientStopValidationSync() {
11386
11661
  const rows = new Map();
@@ -11396,7 +11671,7 @@ class FieldMetadataEditorComponent {
11396
11671
  if (!source)
11397
11672
  continue;
11398
11673
  source.valueChanges
11399
- .pipe(takeUntilDestroyed(this.destroyRef))
11674
+ .pipe(takeUntil(this.formRebuild$), takeUntilDestroyed(this.destroyRef))
11400
11675
  .subscribe(() => {
11401
11676
  for (const targetField of fieldNames) {
11402
11677
  if (targetField === sourceField)
@@ -11448,7 +11723,7 @@ class FieldMetadataEditorComponent {
11448
11723
  VES: { locale: 'es-VE', decimalPlaces: 2, currencyPosition: 'before' },
11449
11724
  };
11450
11725
  currencyControl.valueChanges
11451
- .pipe(takeUntilDestroyed(this.destroyRef))
11726
+ .pipe(takeUntil(this.formRebuild$), takeUntilDestroyed(this.destroyRef))
11452
11727
  .subscribe((value) => {
11453
11728
  const code = String(value || '').toUpperCase();
11454
11729
  const preset = presets[code];
@@ -11499,13 +11774,7 @@ class FieldMetadataEditorComponent {
11499
11774
  // Sempre incluir controlType no patch quando presente
11500
11775
  patch.controlType = this.controlType;
11501
11776
  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);
11777
+ this.hostApplyPatch?.(patch);
11509
11778
  }
11510
11779
  catch { }
11511
11780
  this.applied.emit(patch);
@@ -11526,10 +11795,6 @@ class FieldMetadataEditorComponent {
11526
11795
  delete patch.providerOptions;
11527
11796
  }
11528
11797
  patch.controlType = this.controlType;
11529
- try {
11530
- console.debug('[FieldMetadataEditor] getSettingsValue() patch', patch);
11531
- }
11532
- catch { }
11533
11798
  return patch;
11534
11799
  }
11535
11800
  /**
@@ -11811,6 +12076,12 @@ class FieldMetadataEditorComponent {
11811
12076
  }
11812
12077
  }
11813
12078
  reset() {
12079
+ if (this.initialSeed !== undefined) {
12080
+ this.seed = this.cloneSnapshot(this.initialSeed);
12081
+ }
12082
+ if (this.initialControlType !== undefined && this.controlType !== this.initialControlType) {
12083
+ this.rebuildEditorForType(this.initialControlType);
12084
+ }
11814
12085
  const snapshot = this.cloneSnapshot(this.initialSnapshot ?? this.form.getRawValue());
11815
12086
  this.form.reset(snapshot);
11816
12087
  this.form.markAsPristine();
@@ -11828,6 +12099,79 @@ class FieldMetadataEditorComponent {
11828
12099
  if (value != null)
11829
12100
  this.onControlTypeChange(value);
11830
12101
  }
12102
+ tx(key, fallback) {
12103
+ const namespacedKey = key.startsWith('praxis.metadataEditor.')
12104
+ ? key
12105
+ : `praxis.metadataEditor.${key}`;
12106
+ return this.i18n?.t(namespacedKey, undefined, fallback) ?? fallback;
12107
+ }
12108
+ buildCurrentSemanticFieldSnapshot() {
12109
+ const currentRaw = this.form?.getRawValue?.() ?? {};
12110
+ const merged = {
12111
+ ...(this.seed || {}),
12112
+ };
12113
+ for (const [k, v] of Object.entries(currentRaw)) {
12114
+ if (v !== undefined && v !== null && v !== '') {
12115
+ merged[k] = v;
12116
+ }
12117
+ }
12118
+ merged['name'] = String(currentRaw?.name ?? this.seed?.name ?? '').trim();
12119
+ merged['controlType'] = this.controlType;
12120
+ return merged;
12121
+ }
12122
+ canCalibrateFromSemantics() {
12123
+ const snapshot = this.buildCurrentSemanticFieldSnapshot();
12124
+ return hasSemanticFieldCalibrationDiff(snapshot);
12125
+ }
12126
+ calibrateFromSemantics() {
12127
+ const mergedField = this.buildCurrentSemanticFieldSnapshot();
12128
+ if (!hasSemanticFieldCalibrationDiff(mergedField)) {
12129
+ return;
12130
+ }
12131
+ const calibrated = calibrateFieldMetadataFromSemantics(mergedField);
12132
+ this.seed = {
12133
+ ...(this.seed || {}),
12134
+ ...calibrated,
12135
+ };
12136
+ const nextControlType = (calibrated.controlType ?? this.controlType);
12137
+ if (nextControlType && nextControlType !== this.controlType) {
12138
+ this.rebuildEditorForType(nextControlType);
12139
+ }
12140
+ const getByPath = (obj, path) => {
12141
+ if (!obj || typeof obj !== 'object' || !path)
12142
+ return undefined;
12143
+ return path.split('.').reduce((acc, part) => (acc == null ? undefined : acc[part]), obj);
12144
+ };
12145
+ const formPatch = {};
12146
+ for (const prop of this.normalizedProps || []) {
12147
+ const propName = String(prop?.name ?? '').trim();
12148
+ if (!propName)
12149
+ continue;
12150
+ const rawVal = getByPath(calibrated, propName);
12151
+ if (rawVal === undefined)
12152
+ continue;
12153
+ const controlVal = this.toEditorControlValue(prop, rawVal);
12154
+ if (controlVal !== undefined && controlVal !== null) {
12155
+ try {
12156
+ this.factory.setValueByPath(this.form, propName, controlVal);
12157
+ }
12158
+ catch { }
12159
+ if (!propName.includes('.') && this.form?.get(propName)) {
12160
+ formPatch[propName] = controlVal;
12161
+ }
12162
+ }
12163
+ }
12164
+ if (calibrated.options !== undefined &&
12165
+ !this.form?.get('options') &&
12166
+ this.form?.get('providerOptions')) {
12167
+ const prop = this.normalizedProps?.find((p) => p?.name === 'providerOptions');
12168
+ formPatch['providerOptions'] = this.toEditorControlValue(prop, calibrated.options);
12169
+ }
12170
+ if (Object.keys(formPatch).length > 0) {
12171
+ this.form.patchValue(formPatch, { emitEvent: true });
12172
+ }
12173
+ this.isDirty$.next(true);
12174
+ }
11831
12175
  withSharedFieldMetadataProperties(props) {
11832
12176
  const propNames = new Set(props.map((prop) => prop.name));
11833
12177
  const sharedProps = [...fieldPresentationProperties, ...fieldSubmitProperties];
@@ -11837,11 +12181,322 @@ class FieldMetadataEditorComponent {
11837
12181
  get isArrayEditor() {
11838
12182
  return this.controlType === FieldControlType.ARRAY_INPUT;
11839
12183
  }
12184
+ static ROOT_DEPENDENT_FIELD_KEY_PROPS = new Set([
12185
+ 'optionValueKey',
12186
+ 'optionLabelKey',
12187
+ 'lookupIdKey',
12188
+ 'lookupLabelKey',
12189
+ 'lookupSubtitleKey',
12190
+ 'parentIdKey',
12191
+ ]);
12192
+ static OPTION_SOURCE_DEPENDENT_FIELD_KEY_PROPS = new Set([
12193
+ 'optionSource.valuePropertyPath',
12194
+ 'optionSource.labelPropertyPath',
12195
+ 'optionSource.codePropertyPath',
12196
+ 'optionSource.statusPropertyPath',
12197
+ 'optionSource.subtitlePropertyPath',
12198
+ 'optionSource.descriptionPropertyPath',
12199
+ ]);
11840
12200
  buildRenderedProps(props) {
11841
- if (!this.isArrayEditor) {
11842
- return props;
12201
+ const base = !this.isArrayEditor
12202
+ ? props || []
12203
+ : (props || []).filter((prop) => prop?.name !== 'array.itemSchema.fields');
12204
+ if (!this.discoveredResourceOptions.length &&
12205
+ this.resourceSchemaFieldsByPath.size === 0) {
12206
+ return base;
12207
+ }
12208
+ return base.map((prop) => {
12209
+ const resourceEnriched = this.enrichResourcePathProperty(prop);
12210
+ return this.enrichDependentFieldKeyProperty(resourceEnriched);
12211
+ });
12212
+ }
12213
+ enrichResourcePathProperty(prop) {
12214
+ if (!prop ||
12215
+ (prop.name !== 'resourcePath' && prop.name !== 'optionSource.resourcePath')) {
12216
+ return prop;
12217
+ }
12218
+ const optionSourceType = String(this.form?.get('optionSource.type')?.value ??
12219
+ this.seed?.optionSource?.type ??
12220
+ '')
12221
+ .trim()
12222
+ .toLowerCase();
12223
+ const useStatsIntent = prop.name === 'optionSource.resourcePath' && optionSourceType === 'stats';
12224
+ const filteredResources = this.discoveredResourceOptions.filter((item) => useStatsIntent
12225
+ ? item.capabilities?.stats !== false
12226
+ : item.capabilities?.options !== false);
12227
+ if (!filteredResources.length) {
12228
+ return prop;
12229
+ }
12230
+ const currentValue = String(this.form?.get(prop.name)?.value ?? prop.defaultValue ?? '').trim();
12231
+ const matchedCatalogOption = currentValue
12232
+ ? this.resourceDiscovery?.findCatalogResourceOption?.(currentValue, filteredResources)
12233
+ : null;
12234
+ const readOnlyLabel = this.i18n?.t('quickConnect.readOnly', undefined, 'Somente leitura', 'resourceDiscovery') ?? 'Somente leitura';
12235
+ const options = filteredResources.map((item) => {
12236
+ const readOnlySuffix = item.readOnly ? readOnlyLabel : null;
12237
+ const hasDistinctPath = Boolean(item.path && item.path !== item.label);
12238
+ const code = hasDistinctPath ? item.path : null;
12239
+ const badge = [item.group, readOnlySuffix]
12240
+ .filter((part) => Boolean(part))
12241
+ .join(' · ') || null;
12242
+ const description = String(item.description ?? '').trim() || null;
12243
+ return {
12244
+ value: item === matchedCatalogOption && currentValue ? currentValue : item.value,
12245
+ text: item.label,
12246
+ icon: item.icon ?? 'api',
12247
+ code,
12248
+ badge,
12249
+ description,
12250
+ searchTerms: [
12251
+ ...item.searchTerms,
12252
+ ...(readOnlySuffix ? [readOnlySuffix] : []),
12253
+ ],
12254
+ };
12255
+ });
12256
+ if (currentValue && !matchedCatalogOption) {
12257
+ options.unshift({
12258
+ value: currentValue,
12259
+ text: currentValue,
12260
+ icon: 'link',
12261
+ code: null,
12262
+ badge: null,
12263
+ description: null,
12264
+ searchTerms: [currentValue],
12265
+ });
11843
12266
  }
11844
- return (props || []).filter((prop) => prop?.name !== 'array.itemSchema.fields');
12267
+ return {
12268
+ ...prop,
12269
+ editorType: 'select',
12270
+ searchable: true,
12271
+ searchPlaceholderKey: 'fieldResource.searchPlaceholder',
12272
+ searchPlaceholder: 'Buscar por nome, domínio ou caminho',
12273
+ options,
12274
+ };
12275
+ }
12276
+ enrichDependentFieldKeyProperty(prop) {
12277
+ const propName = String(prop?.name ?? '').trim();
12278
+ const isRootDependent = FieldMetadataEditorComponent.ROOT_DEPENDENT_FIELD_KEY_PROPS.has(propName);
12279
+ const isOptionSourceDependent = FieldMetadataEditorComponent.OPTION_SOURCE_DEPENDENT_FIELD_KEY_PROPS.has(propName);
12280
+ if (!isRootDependent && !isOptionSourceDependent) {
12281
+ return prop;
12282
+ }
12283
+ const resourceControlPath = isOptionSourceDependent
12284
+ ? 'optionSource.resourcePath'
12285
+ : 'resourcePath';
12286
+ const resourcePath = String(this.form?.get(resourceControlPath)?.value ??
12287
+ (isOptionSourceDependent
12288
+ ? this.seed?.optionSource?.resourcePath
12289
+ : this.seed?.resourcePath) ??
12290
+ '').trim();
12291
+ if (!resourcePath) {
12292
+ return prop;
12293
+ }
12294
+ const fields = this.getCachedFieldsForResource(resourcePath);
12295
+ if (!fields.length) {
12296
+ return prop;
12297
+ }
12298
+ const currentValue = String(this.form?.get(propName)?.value ?? prop.defaultValue ?? '').trim();
12299
+ const options = fields.map((field) => {
12300
+ const fieldName = String(field.name ?? '').trim();
12301
+ const fieldLabel = String(field.label ?? '').trim();
12302
+ const hasDistinctLabel = Boolean(fieldLabel && fieldLabel !== fieldName);
12303
+ const typeHint = String(field.type || field.controlType || '').trim();
12304
+ const description = String(field.description ?? '').trim() || null;
12305
+ return {
12306
+ value: fieldName,
12307
+ text: hasDistinctLabel ? fieldLabel : fieldName,
12308
+ code: hasDistinctLabel ? fieldName : null,
12309
+ badge: typeHint || null,
12310
+ description,
12311
+ searchTerms: [
12312
+ fieldName,
12313
+ fieldLabel,
12314
+ typeHint,
12315
+ description ?? '',
12316
+ ].filter(Boolean),
12317
+ };
12318
+ });
12319
+ if (currentValue &&
12320
+ !options.some((option) => option.value === currentValue)) {
12321
+ options.unshift({
12322
+ value: currentValue,
12323
+ text: currentValue,
12324
+ code: null,
12325
+ badge: null,
12326
+ description: null,
12327
+ searchTerms: [currentValue],
12328
+ });
12329
+ }
12330
+ return {
12331
+ ...prop,
12332
+ editorType: 'select',
12333
+ searchable: true,
12334
+ searchPlaceholderKey: 'fieldResource.fieldSearchPlaceholder',
12335
+ searchPlaceholder: 'Buscar campo por nome ou rótulo',
12336
+ options,
12337
+ };
12338
+ }
12339
+ normalizeResourceCacheKey(resourcePath) {
12340
+ const raw = String(resourcePath ?? '').trim();
12341
+ if (!raw) {
12342
+ return '';
12343
+ }
12344
+ if (this.discoveredResourceOptions.length > 0) {
12345
+ const matched = this.resourceDiscovery?.findCatalogResourceOption?.(raw, this.discoveredResourceOptions);
12346
+ if (matched?.value) {
12347
+ return matched.value.trim().replace(/^\/+|\/+$/g, '');
12348
+ }
12349
+ }
12350
+ return raw.replace(/^\/+|\/+$/g, '').replace(/^api\/+/i, '');
12351
+ }
12352
+ getCachedFieldsForResource(resourcePath) {
12353
+ const key = this.normalizeResourceCacheKey(resourcePath);
12354
+ if (!key) {
12355
+ return [];
12356
+ }
12357
+ const direct = this.resourceSchemaFieldsByPath.get(key);
12358
+ if (direct?.length) {
12359
+ return direct;
12360
+ }
12361
+ const rawKey = String(resourcePath ?? '').trim().replace(/^\/+|\/+$/g, '');
12362
+ return this.resourceSchemaFieldsByPath.get(rawKey) ?? [];
12363
+ }
12364
+ setCachedFieldsForResource(resourcePath, fields) {
12365
+ const key = this.normalizeResourceCacheKey(resourcePath);
12366
+ if (key) {
12367
+ this.resourceSchemaFieldsByPath.set(key, fields);
12368
+ }
12369
+ const rawKey = String(resourcePath ?? '').trim().replace(/^\/+|\/+$/g, '');
12370
+ if (rawKey) {
12371
+ this.resourceSchemaFieldsByPath.set(rawKey, fields);
12372
+ }
12373
+ }
12374
+ applyAutoSuggestedDependentKeys(scope, fields) {
12375
+ if (!this.form || !fields.length) {
12376
+ return;
12377
+ }
12378
+ const fieldNameSet = new Set(fields.map((field) => String(field.name ?? '').trim()).filter(Boolean));
12379
+ const suggestions = this.selectSuggestedFieldKeys(fields);
12380
+ const targets = scope === 'optionSource'
12381
+ ? [
12382
+ {
12383
+ path: 'optionSource.valuePropertyPath',
12384
+ suggested: suggestions.valueKey,
12385
+ },
12386
+ {
12387
+ path: 'optionSource.labelPropertyPath',
12388
+ suggested: suggestions.labelKey,
12389
+ },
12390
+ {
12391
+ path: 'optionSource.codePropertyPath',
12392
+ suggested: suggestions.codeKey,
12393
+ },
12394
+ {
12395
+ path: 'optionSource.statusPropertyPath',
12396
+ suggested: suggestions.statusKey,
12397
+ },
12398
+ ]
12399
+ : [
12400
+ { path: 'optionValueKey', suggested: suggestions.valueKey },
12401
+ { path: 'optionLabelKey', suggested: suggestions.labelKey },
12402
+ { path: 'lookupIdKey', suggested: suggestions.valueKey },
12403
+ { path: 'lookupLabelKey', suggested: suggestions.labelKey },
12404
+ { path: 'lookupSubtitleKey', suggested: suggestions.subtitleKey },
12405
+ ];
12406
+ for (const target of targets) {
12407
+ const ctrl = this.form.get(target.path);
12408
+ if (!ctrl) {
12409
+ continue;
12410
+ }
12411
+ const current = String(ctrl.value ?? '').trim();
12412
+ if (!current || !fieldNameSet.has(current)) {
12413
+ const nextValue = target.suggested ?? '';
12414
+ if (current !== nextValue) {
12415
+ ctrl.setValue(nextValue);
12416
+ }
12417
+ }
12418
+ }
12419
+ }
12420
+ selectSuggestedFieldKeys(fields) {
12421
+ const names = fields
12422
+ .map((field) => String(field.name ?? '').trim())
12423
+ .filter(Boolean);
12424
+ const findExact = (candidates, exclude = new Set()) => {
12425
+ for (const candidate of candidates) {
12426
+ const lower = candidate.toLowerCase();
12427
+ const found = names.find((name) => !exclude.has(name) && name.toLowerCase() === lower);
12428
+ if (found) {
12429
+ return found;
12430
+ }
12431
+ }
12432
+ return null;
12433
+ };
12434
+ const valueKey = findExact([
12435
+ 'id',
12436
+ 'codigo',
12437
+ 'code',
12438
+ 'uuid',
12439
+ 'key',
12440
+ 'valor',
12441
+ 'value',
12442
+ 'matricula',
12443
+ 'sigla',
12444
+ ]) ??
12445
+ names.find((name) => /(?:Id|Codigo|Code)$/.test(name)) ??
12446
+ names[0] ??
12447
+ null;
12448
+ const usedForValue = new Set(valueKey ? [valueKey] : []);
12449
+ const labelKey = findExact([
12450
+ 'nome',
12451
+ 'name',
12452
+ 'label',
12453
+ 'descricao',
12454
+ 'description',
12455
+ 'titulo',
12456
+ 'title',
12457
+ 'razaosocial',
12458
+ 'nomecompleto',
12459
+ 'rotulo',
12460
+ 'text',
12461
+ ], usedForValue) ??
12462
+ fields
12463
+ .find((field) => !usedForValue.has(String(field.name ?? '').trim()) &&
12464
+ String(field.type ?? 'string').toLowerCase() === 'string')
12465
+ ?.name?.trim() ??
12466
+ names.find((name) => !usedForValue.has(name)) ??
12467
+ valueKey;
12468
+ const codeKey = findExact([
12469
+ 'codigo',
12470
+ 'code',
12471
+ 'sigla',
12472
+ 'chave',
12473
+ 'matricula',
12474
+ 'referencia',
12475
+ ]);
12476
+ const statusKey = findExact([
12477
+ 'status',
12478
+ 'situacao',
12479
+ 'situacaofuncional',
12480
+ 'ativo',
12481
+ 'active',
12482
+ 'estado',
12483
+ 'enabled',
12484
+ ]);
12485
+ const usedForPrimary = new Set([valueKey, labelKey].filter((item) => Boolean(item)));
12486
+ const subtitleKey = findExact([
12487
+ 'descricao',
12488
+ 'description',
12489
+ 'subtitulo',
12490
+ 'subtitle',
12491
+ 'resumo',
12492
+ 'summary',
12493
+ 'email',
12494
+ 'cargo',
12495
+ 'departamento',
12496
+ 'sigla',
12497
+ 'codigo',
12498
+ ], usedForPrimary);
12499
+ return { valueKey, labelKey, codeKey, statusKey, subtitleKey };
11845
12500
  }
11846
12501
  cloneSnapshot(value) {
11847
12502
  return this.cloneMetadataValue(value, new WeakMap());
@@ -11954,10 +12609,25 @@ class FieldMetadataEditorComponent {
11954
12609
  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
12610
  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
12611
  providePraxisMetadataEditorI18n(),
12612
+ providePraxisI18nConfig(RESOURCE_DISCOVERY_I18N_CONFIG),
11957
12613
  ...providePraxisDynamicFieldsI18n(),
11958
12614
  ], ngImport: i0, template: `
11959
12615
  @if (form) {
11960
12616
  <div class="p-3">
12617
+ @if (canCalibrateFromSemantics()) {
12618
+ <div class="d-flex justify-content-end mb-2">
12619
+ <button
12620
+ mat-stroked-button
12621
+ type="button"
12622
+ data-testid="field-metadata-smart-calibrate-btn"
12623
+ (click)="calibrateFromSemantics()"
12624
+ [matTooltip]="tx('smartCalibrate.tooltip', 'Infere automaticamente o tipo de controle, rótulo humanizado e padrões corporativos a partir do nome e metadados do campo.')"
12625
+ >
12626
+ <mat-icon aria-hidden="true">auto_fix_high</mat-icon>
12627
+ {{ tx('smartCalibrate.action', 'Calibrar pelo schema') }}
12628
+ </button>
12629
+ </div>
12630
+ }
11961
12631
  <praxis-dynamic-editor-renderer
11962
12632
  [properties]="renderedProps"
11963
12633
  [form]="form"
@@ -11970,7 +12640,7 @@ class FieldMetadataEditorComponent {
11970
12640
  }
11971
12641
  </div>
11972
12642
  }
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"] }] });
12643
+ `, isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i1$2.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i2$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i3$1.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: DynamicEditorRendererComponent, selector: "praxis-dynamic-editor-renderer", inputs: ["properties", "form"] }, { kind: "component", type: ArrayItemSchemaFieldsEditorComponent, selector: "praxis-array-item-schema-fields-editor", inputs: ["control", "arrayControl"] }] });
11974
12644
  }
11975
12645
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: FieldMetadataEditorComponent, decorators: [{
11976
12646
  type: Component,
@@ -11979,16 +12649,34 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
11979
12649
  standalone: true,
11980
12650
  imports: [
11981
12651
  ReactiveFormsModule,
12652
+ MatButtonModule,
12653
+ MatIconModule,
12654
+ MatTooltipModule,
11982
12655
  DynamicEditorRendererComponent,
11983
12656
  ArrayItemSchemaFieldsEditorComponent
11984
12657
  ],
11985
12658
  providers: [
11986
12659
  providePraxisMetadataEditorI18n(),
12660
+ providePraxisI18nConfig(RESOURCE_DISCOVERY_I18N_CONFIG),
11987
12661
  ...providePraxisDynamicFieldsI18n(),
11988
12662
  ],
11989
12663
  template: `
11990
12664
  @if (form) {
11991
12665
  <div class="p-3">
12666
+ @if (canCalibrateFromSemantics()) {
12667
+ <div class="d-flex justify-content-end mb-2">
12668
+ <button
12669
+ mat-stroked-button
12670
+ type="button"
12671
+ data-testid="field-metadata-smart-calibrate-btn"
12672
+ (click)="calibrateFromSemantics()"
12673
+ [matTooltip]="tx('smartCalibrate.tooltip', 'Infere automaticamente o tipo de controle, rótulo humanizado e padrões corporativos a partir do nome e metadados do campo.')"
12674
+ >
12675
+ <mat-icon aria-hidden="true">auto_fix_high</mat-icon>
12676
+ {{ tx('smartCalibrate.action', 'Calibrar pelo schema') }}
12677
+ </button>
12678
+ </div>
12679
+ }
11992
12680
  <praxis-dynamic-editor-renderer
11993
12681
  [properties]="renderedProps"
11994
12682
  [form]="form"
@@ -12174,16 +12862,132 @@ class CascadeRulesService {
12174
12862
  dependencyLoadOnChange: null,
12175
12863
  };
12176
12864
  }
12177
- /** Suggest a basic mapping dep->dep and a default valuePath */
12865
+ /** Fill missing mappings and value paths without replacing authored values. */
12178
12866
  suggest(rule, inferredValueKey) {
12179
- const map = {};
12180
- (rule.dependencyFields || []).forEach((dep) => (map[dep] = dep));
12867
+ const map = { ...rule.dependencyFilterMap };
12868
+ (rule.dependencyFields || []).forEach((dep) => {
12869
+ if (!Object.prototype.hasOwnProperty.call(map, dep))
12870
+ map[dep] = dep;
12871
+ });
12181
12872
  return {
12182
12873
  ...rule,
12183
12874
  dependencyFilterMap: Object.keys(map).length ? map : rule.dependencyFilterMap,
12184
12875
  dependencyValuePath: rule.dependencyValuePath || inferredValueKey || 'id',
12185
12876
  };
12186
12877
  }
12878
+ /**
12879
+ * Validates the cascade dependency graph directly from an array of FieldMetadata/FieldDefinition.
12880
+ */
12881
+ validateFieldsCascadeGraph(fields) {
12882
+ const rules = new Map();
12883
+ for (const field of fields || []) {
12884
+ const vm = this.hydrateRule(field);
12885
+ if (vm) {
12886
+ rules.set(field.name, vm);
12887
+ }
12888
+ }
12889
+ return this.validateCascadeGraph(rules, fields);
12890
+ }
12891
+ /**
12892
+ * Detects self-references, direct cycles (A -> B -> A) and indirect cycles (A -> B -> C -> A),
12893
+ * including graphs where fields have multiple parent dependencies.
12894
+ */
12895
+ validateCascadeGraph(rules, fields = []) {
12896
+ const ruleEntries = rules instanceof Map ? Array.from(rules.entries()) : Object.entries(rules || {});
12897
+ const labelMap = this.getFieldLabelMap([...fields]);
12898
+ const formatNode = (name) => {
12899
+ const label = labelMap.get(name);
12900
+ return label && label !== name ? `${label} (${name})` : name;
12901
+ };
12902
+ const adjacency = new Map();
12903
+ for (const [target, rule] of ruleEntries) {
12904
+ if (!rule || rule.enableDependencyCascade === false)
12905
+ continue;
12906
+ const deps = (rule.dependencyFields || [])
12907
+ .filter((dep) => typeof dep === 'string')
12908
+ .map((dep) => dep.trim())
12909
+ .filter(Boolean);
12910
+ if (deps.length > 0) {
12911
+ adjacency.set(target, Array.from(new Set(deps)));
12912
+ }
12913
+ }
12914
+ const cycles = [];
12915
+ const fieldsInCycles = new Set();
12916
+ const recordedCycleSignatures = new Set();
12917
+ const canonicalizeCycle = (cyclePath) => {
12918
+ const loop = cyclePath.slice(0, -1);
12919
+ if (loop.length <= 1)
12920
+ return loop.join('->');
12921
+ let minIdx = 0;
12922
+ for (let i = 1; i < loop.length; i++) {
12923
+ if (loop[i].localeCompare(loop[minIdx]) < 0) {
12924
+ minIdx = i;
12925
+ }
12926
+ }
12927
+ const rotated = [...loop.slice(minIdx), ...loop.slice(0, minIdx)];
12928
+ return rotated.join('->');
12929
+ };
12930
+ const recordCycle = (rawCyclePath) => {
12931
+ const signature = canonicalizeCycle(rawCyclePath);
12932
+ if (recordedCycleSignatures.has(signature))
12933
+ return;
12934
+ recordedCycleSignatures.add(signature);
12935
+ const loop = rawCyclePath.slice(0, -1);
12936
+ loop.forEach((f) => fieldsInCycles.add(f));
12937
+ const isSelfReference = loop.length === 1;
12938
+ const formattedPath = rawCyclePath.map(formatNode).join(' → ');
12939
+ const lastParent = rawCyclePath[rawCyclePath.length - 2];
12940
+ const closingField = rawCyclePath[rawCyclePath.length - 1];
12941
+ const message = isSelfReference
12942
+ ? `Autorreferência inválida: o campo ${formatNode(closingField)} não pode depender de si mesmo. Remova "${closingField}" da lista de campos pais.`
12943
+ : `Dependência circular detectada: ${formattedPath}. Para corrigir, remova a dependência entre ${formatNode(lastParent)} e ${formatNode(closingField)} em um dos campos do ciclo.`;
12944
+ cycles.push({
12945
+ path: rawCyclePath,
12946
+ formattedPath,
12947
+ isSelfReference,
12948
+ message,
12949
+ });
12950
+ };
12951
+ const sortedNodes = Array.from(adjacency.keys()).sort((a, b) => a.localeCompare(b));
12952
+ const globallyVisited = new Set();
12953
+ for (const startNode of sortedNodes) {
12954
+ const stack = [];
12955
+ const onStack = new Set();
12956
+ const dfs = (current) => {
12957
+ stack.push(current);
12958
+ onStack.add(current);
12959
+ const neighbors = adjacency.get(current) || [];
12960
+ for (const dep of neighbors) {
12961
+ if (dep === current) {
12962
+ recordCycle([current, current]);
12963
+ continue;
12964
+ }
12965
+ if (onStack.has(dep)) {
12966
+ const cycleStartIndex = stack.indexOf(dep);
12967
+ if (cycleStartIndex >= 0) {
12968
+ const cyclePath = [...stack.slice(cycleStartIndex), dep];
12969
+ recordCycle(cyclePath);
12970
+ }
12971
+ continue;
12972
+ }
12973
+ if (!globallyVisited.has(dep) && adjacency.has(dep)) {
12974
+ dfs(dep);
12975
+ }
12976
+ }
12977
+ stack.pop();
12978
+ onStack.delete(current);
12979
+ globallyVisited.add(current);
12980
+ };
12981
+ if (!globallyVisited.has(startNode)) {
12982
+ dfs(startNode);
12983
+ }
12984
+ }
12985
+ return {
12986
+ valid: cycles.length === 0,
12987
+ cycles,
12988
+ fieldsInCycles,
12989
+ };
12990
+ }
12187
12991
  normalizeDependencyFields(value) {
12188
12992
  if (Array.isArray(value)) {
12189
12993
  const deps = value
@@ -12212,6 +13016,7 @@ class CascadeManagerTabComponent {
12212
13016
  connections; // optional signal for conflicts
12213
13017
  apply = new EventEmitter();
12214
13018
  cancel = new EventEmitter();
13019
+ validChange = new EventEmitter();
12215
13020
  searchTerm = '';
12216
13021
  searchTimer = null;
12217
13022
  searchValue = '';
@@ -12226,10 +13031,18 @@ class CascadeManagerTabComponent {
12226
13031
  editLoadOn = 'respectLoadOn';
12227
13032
  editDebounce = 150;
12228
13033
  editValuePath = '';
12229
- editFilterMapJson = '';
12230
13034
  importMode = false;
12231
13035
  importJson = '';
13036
+ feedbackMessage = '';
13037
+ feedbackTone = 'info';
12232
13038
  editMapRows = [];
13039
+ editInitialFilterMap = {};
13040
+ editInitialValuePath = 'id';
13041
+ lastEmittedValid = null;
13042
+ setFeedback(message, tone = 'info') {
13043
+ this.feedbackMessage = message;
13044
+ this.feedbackTone = tone;
13045
+ }
12233
13046
  constructor(svc) {
12234
13047
  this.svc = svc;
12235
13048
  }
@@ -12246,20 +13059,17 @@ class CascadeManagerTabComponent {
12246
13059
  this.selectedFieldSig.set(this.fields[0].name);
12247
13060
  this.startEdit(this.fields[0].name);
12248
13061
  }
13062
+ this.syncValidityState();
12249
13063
  }
12250
13064
  ngOnChanges(changes) {
12251
13065
  if (changes['fields'] && !changes['fields'].firstChange) {
12252
- // Re-hydrate rules from incoming fields while preserving existing ones when possible
13066
+ // Re-hydrate rules from incoming fields; incoming fields own the canonical state
12253
13067
  const incoming = this.fields || [];
12254
13068
  const next = new Map();
12255
- // keep existing for fields still present
12256
13069
  for (const f of incoming) {
12257
- const existing = this.rules().get(f.name);
12258
13070
  const hydrated = this.svc.hydrateRule(f);
12259
13071
  if (hydrated)
12260
13072
  next.set(f.name, hydrated);
12261
- else if (existing)
12262
- next.set(f.name, existing);
12263
13073
  }
12264
13074
  this.rules.set(next);
12265
13075
  // preserve selection if still valid
@@ -12267,29 +13077,114 @@ class CascadeManagerTabComponent {
12267
13077
  if (sel && !incoming.some((f) => f.name === sel)) {
12268
13078
  this.selectedFieldSig.set(incoming[0]?.name ?? null);
12269
13079
  }
13080
+ const activeSel = this.selectedField();
13081
+ if (activeSel) {
13082
+ this.startEdit(activeSel);
13083
+ }
13084
+ this.syncValidityState();
12270
13085
  }
12271
13086
  }
12272
- _logCount = 0;
12273
13087
  filteredFields() {
12274
13088
  const q = (this.searchValue || '').toLowerCase();
12275
13089
  const labelMap = this.svc.getFieldLabelMap(this.fields);
12276
13090
  const result = this.fields
12277
13091
  .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));
13092
+ .sort((a, b) => {
13093
+ const labelCmp = (labelMap.get(a.name) || a.name).localeCompare(labelMap.get(b.name) || b.name);
13094
+ return labelCmp !== 0 ? labelCmp : a.name.localeCompare(b.name);
13095
+ });
12279
13096
  return result;
12280
13097
  }
12281
13098
  labelFor(name) {
12282
13099
  return this.svc.getFieldLabelMap(this.fields).get(name) || name;
12283
13100
  }
13101
+ /**
13102
+ * Disambiguates fields that share the same display label (e.g. "Cargo" for both `cargoId` and `cargoNome`).
13103
+ */
13104
+ disambiguatedLabelFor(name) {
13105
+ const baseLabel = this.labelFor(name);
13106
+ const duplicateCount = this.fields.filter((f) => (f.label || f.name).trim().toLowerCase() === baseLabel.trim().toLowerCase()).length;
13107
+ if (duplicateCount <= 1) {
13108
+ return baseLabel;
13109
+ }
13110
+ const roleHint = this.hasRemoteOptionSource(name) || /id$/i.test(name)
13111
+ ? 'Selecionável · ID'
13112
+ : 'Texto / Nome';
13113
+ return `${baseLabel} — ${roleHint}`;
13114
+ }
13115
+ hasRemoteOptionSource(name) {
13116
+ const f = this.fields.find((item) => item.name === name);
13117
+ if (!f)
13118
+ return false;
13119
+ return Boolean(f.resourcePath ||
13120
+ f.endpoint ||
13121
+ f.optionSource?.resourcePath ||
13122
+ f.optionSource?.key ||
13123
+ f.controlType === 'async-select' ||
13124
+ f.controlType === 'entity-lookup' ||
13125
+ f.controlType === 'select' ||
13126
+ f.controlType === 'multi-select');
13127
+ }
13128
+ fieldRoleBadge(name) {
13129
+ const f = this.fields.find((item) => item.name === name);
13130
+ if (!f)
13131
+ return 'Campo';
13132
+ const hasEndpoint = Boolean(f.resourcePath || f.endpoint || f.optionSource?.resourcePath || f.optionSource?.key);
13133
+ if (hasEndpoint) {
13134
+ return 'Opções remotas';
13135
+ }
13136
+ if (f.controlType === 'select' || f.controlType === 'async-select' || f.controlType === 'multi-select' || /id$/i.test(name)) {
13137
+ return 'Selecionável';
13138
+ }
13139
+ return 'Texto / Exibição';
13140
+ }
13141
+ fieldRoleSummary(name) {
13142
+ const f = this.fields.find((item) => item.name === name);
13143
+ if (!f)
13144
+ return 'Campo do formulário';
13145
+ const resourcePath = f.resourcePath || f.optionSource?.resourcePath || f.endpoint;
13146
+ if (resourcePath) {
13147
+ return `Carrega opções de "${resourcePath}"`;
13148
+ }
13149
+ return `${this.fieldRoleBadge(name)} (${f.controlType || 'input'})`;
13150
+ }
13151
+ formatParentList(deps) {
13152
+ if (!deps || !deps.length)
13153
+ return '—';
13154
+ return deps.map((d) => `${this.disambiguatedLabelFor(d)} (${d})`).join(', ');
13155
+ }
13156
+ formatRelationshipSummary(targetField, deps) {
13157
+ if (!deps || !deps.length) {
13158
+ return `Nenhum campo pai selecionado para ${this.disambiguatedLabelFor(targetField)} (${targetField}).`;
13159
+ }
13160
+ const parents = deps.map((d) => `${this.disambiguatedLabelFor(d)} (${d})`).join(' + ');
13161
+ return `${parents} → filtra opções de ${this.disambiguatedLabelFor(targetField)} (${targetField})`;
13162
+ }
13163
+ formatLoadOnLabel(policy) {
13164
+ switch (policy) {
13165
+ case 'immediate':
13166
+ return 'Imediato ao mudar o pai (immediate)';
13167
+ case 'manual':
13168
+ return 'Manual / sob demanda (manual)';
13169
+ default:
13170
+ return 'Seguir configuração do campo (respectLoadOn)';
13171
+ }
13172
+ }
13173
+ formatMergeStrategyLabel(strategy) {
13174
+ return strategy === 'replace'
13175
+ ? 'Substituir filtros (replace)'
13176
+ : 'Mesclar filtros (merge)';
13177
+ }
12284
13178
  selectedField() {
12285
13179
  return this.selectedFieldSig();
12286
13180
  }
12287
13181
  selectField(name) {
12288
13182
  this.selectedFieldSig.set(name);
12289
13183
  this.startEdit(name);
13184
+ this.syncValidityState();
12290
13185
  }
12291
13186
  hasRule(name) {
12292
- return this.rules().has(name);
13187
+ return (this.rules().get(name)?.dependencyFields?.length ?? 0) > 0;
12293
13188
  }
12294
13189
  ruleFor(name) {
12295
13190
  return this.rules().get(name) || null;
@@ -12305,6 +13200,52 @@ class CascadeManagerTabComponent {
12305
13200
  return false;
12306
13201
  return this.connections.some((c) => c.toFieldName === name && (c.toInputPath || '').includes('filterCriteria'));
12307
13202
  }
13203
+ /**
13204
+ * Computes graph validation combining saved rules with any active draft in the inline editor.
13205
+ */
13206
+ validateCurrentGraph(includeDraft = true) {
13207
+ const effectiveRules = new Map(this.rules());
13208
+ const sel = this.selectedField();
13209
+ if (includeDraft && this.editing() && sel) {
13210
+ const draftDeps = (this.editDepList || []).filter(Boolean);
13211
+ if (draftDeps.length > 0) {
13212
+ effectiveRules.set(sel, {
13213
+ targetField: sel,
13214
+ dependencyFields: draftDeps,
13215
+ enableDependencyCascade: this.editEnable,
13216
+ resetOnDependentChange: this.editReset,
13217
+ dependencyFilterMap: {},
13218
+ dependencyValuePath: this.editValuePath || 'id',
13219
+ dependencyMergeStrategy: this.editMerge,
13220
+ dependencyDebounceMs: this.editDebounce ?? 150,
13221
+ dependencyLoadOnChange: this.editLoadOn,
13222
+ });
13223
+ }
13224
+ else {
13225
+ effectiveRules.delete(sel);
13226
+ }
13227
+ }
13228
+ return this.svc.validateCascadeGraph(effectiveRules, this.fields);
13229
+ }
13230
+ activeCycleDiagnostics() {
13231
+ return this.validateCurrentGraph(true).cycles;
13232
+ }
13233
+ isFieldInCycle(name) {
13234
+ return this.validateCurrentGraph(true).fieldsInCycles.has(name);
13235
+ }
13236
+ hasBlockingErrors() {
13237
+ return !this.validateCurrentGraph(true).valid;
13238
+ }
13239
+ syncValidityState() {
13240
+ const isValid = this.validateCurrentGraph(true).valid;
13241
+ if (this.lastEmittedValid !== isValid) {
13242
+ this.lastEmittedValid = isValid;
13243
+ this.validChange.emit(isValid);
13244
+ }
13245
+ }
13246
+ onDraftControlChange() {
13247
+ this.syncValidityState();
13248
+ }
12308
13249
  startEdit(name) {
12309
13250
  const existing = this.ruleFor(name);
12310
13251
  const base = existing || {
@@ -12327,35 +13268,69 @@ class CascadeManagerTabComponent {
12327
13268
  this.editLoadOn = suggested.dependencyLoadOnChange || 'respectLoadOn';
12328
13269
  this.editDebounce = suggested.dependencyDebounceMs ?? 150;
12329
13270
  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
- }
13271
+ this.editInitialFilterMap = structuredClone(suggested.dependencyFilterMap || {});
13272
+ this.editInitialValuePath = structuredClone(suggested.dependencyValuePath || 'id');
13273
+ this.editMapRows = [];
12336
13274
  // Build mapping rows from dependencies
12337
13275
  this.buildMapRowsFromDeps();
12338
13276
  this.editing.set(true);
13277
+ this.syncValidityState();
12339
13278
  }
12340
13279
  edit(name) { this.startEdit(name); }
12341
- cancelEdit() { this.editing.set(false); }
13280
+ cancelEdit() {
13281
+ const sel = this.selectedField();
13282
+ if (sel) {
13283
+ // Restore draft from committed rule before closing inline edit
13284
+ const existing = this.ruleFor(sel);
13285
+ this.editDepList = existing ? [...existing.dependencyFields] : [];
13286
+ this.editEnable = existing ? existing.enableDependencyCascade : true;
13287
+ }
13288
+ this.editing.set(false);
13289
+ this.syncValidityState();
13290
+ }
12342
13291
  saveEdit(name) {
12343
- // Build rule from editor
13292
+ // Build rule from editor (preserving self-reference prevention)
12344
13293
  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 };
13294
+ this.editDepList = [...deps];
13295
+ // The rows own the draft. The opening snapshot only preserves entry shape;
13296
+ // merging the original JSON after the rows would silently undo visual edits.
13297
+ const map = structuredClone(this.editInitialFilterMap);
13298
+ const dependenciesRemoved = (this.ruleFor(name)?.dependencyFields || [])
13299
+ .some((dep) => !deps.includes(dep));
13300
+ if (dependenciesRemoved) {
13301
+ for (const dep of Object.keys(map)) {
13302
+ if (!deps.includes(dep))
13303
+ delete map[dep];
12355
13304
  }
12356
- catch {
12357
- alert('Mapeamento de filtros inválido (JSON)');
12358
- return;
13305
+ }
13306
+ for (const row of this.editMapRows) {
13307
+ if (!deps.includes(row.dep))
13308
+ continue;
13309
+ const initial = this.editInitialFilterMap[row.dep];
13310
+ const initialPath = this.initialRowValuePath(row.dep);
13311
+ if (typeof initial === 'object' && initial !== null) {
13312
+ const entry = { ...initial, key: row.key || row.dep };
13313
+ if (row.valuePath !== initialPath) {
13314
+ if (row.valuePath)
13315
+ entry.valuePath = row.valuePath;
13316
+ else
13317
+ delete entry.valuePath;
13318
+ }
13319
+ map[row.dep] = entry;
13320
+ }
13321
+ else {
13322
+ map[row.dep] = row.valuePath && row.valuePath !== initialPath
13323
+ ? { key: row.key || row.dep, valuePath: row.valuePath }
13324
+ : row.key || row.dep;
13325
+ }
13326
+ }
13327
+ const valuePaths = this.buildValuePathOverrides(deps);
13328
+ // Removed dependencies must lose their extraction overrides as well as their
13329
+ // filter mappings, otherwise re-adding one resurrects stale extraction state.
13330
+ if (dependenciesRemoved && typeof valuePaths === 'object') {
13331
+ for (const dep of Object.keys(valuePaths)) {
13332
+ if (!deps.includes(dep))
13333
+ delete valuePaths[dep];
12359
13334
  }
12360
13335
  }
12361
13336
  const rule = {
@@ -12364,25 +13339,67 @@ class CascadeManagerTabComponent {
12364
13339
  enableDependencyCascade: this.editEnable,
12365
13340
  resetOnDependentChange: this.editReset,
12366
13341
  dependencyFilterMap: map,
12367
- dependencyValuePath: this.buildValuePathOverrides(deps),
13342
+ dependencyValuePath: valuePaths,
12368
13343
  dependencyMergeStrategy: this.editMerge,
12369
13344
  dependencyDebounceMs: this.editDebounce ?? 150,
12370
13345
  dependencyLoadOnChange: this.editLoadOn,
12371
13346
  };
12372
13347
  const next = new Map(this.rules());
12373
13348
  next.set(name, rule);
13349
+ const validation = this.svc.validateCascadeGraph(next, this.fields);
13350
+ if (validation.fieldsInCycles.has(name)) {
13351
+ const fieldCycle = validation.cycles.find((c) => c.path.includes(name)) || validation.cycles[0];
13352
+ this.setFeedback(fieldCycle.message, 'error');
13353
+ this.syncValidityState();
13354
+ return;
13355
+ }
13356
+ if (!validation.valid) {
13357
+ this.setFeedback(validation.cycles[0].message, 'error');
13358
+ }
13359
+ else {
13360
+ this.setFeedback('');
13361
+ }
12374
13362
  this.rules.set(next);
12375
13363
  this.emitPatch();
12376
13364
  this.editing.set(false);
13365
+ this.syncValidityState();
13366
+ }
13367
+ /**
13368
+ * Removes cyclic dependencies from the given field's draft (and saved rule if present).
13369
+ */
13370
+ breakCycleForField(name) {
13371
+ const currentValidation = this.validateCurrentGraph(true);
13372
+ if (!currentValidation.fieldsInCycles.has(name)) {
13373
+ return;
13374
+ }
13375
+ const cyclicTargets = new Set([name]);
13376
+ for (const cycle of currentValidation.cycles) {
13377
+ for (let i = 0; i < cycle.path.length - 1; i++) {
13378
+ if (cycle.path[i] === name) {
13379
+ cyclicTargets.add(cycle.path[i + 1]);
13380
+ }
13381
+ }
13382
+ }
13383
+ this.editDepList = (this.editDepList || []).filter((dep) => !cyclicTargets.has(dep));
13384
+ this.onDepListChange();
13385
+ if (this.rules().has(name)) {
13386
+ this.saveEdit(name);
13387
+ }
12377
13388
  }
12378
13389
  remove(name) {
12379
13390
  const next = new Map(this.rules());
12380
13391
  next.delete(name);
12381
13392
  this.rules.set(next);
13393
+ if (this.selectedField() === name) {
13394
+ this.editDepList = [];
13395
+ this.editMapRows = [];
13396
+ }
12382
13397
  const patch = {
12383
13398
  [name]: this.svc.clearPatch(),
12384
13399
  };
13400
+ this.setFeedback('');
12385
13401
  this.apply.emit(patch);
13402
+ this.syncValidityState();
12386
13403
  }
12387
13404
  quickSet(name, action) {
12388
13405
  const current = this.ruleFor(name);
@@ -12397,6 +13414,7 @@ class CascadeManagerTabComponent {
12397
13414
  map.set(name, next);
12398
13415
  this.rules.set(map);
12399
13416
  this.emitPatch();
13417
+ this.syncValidityState();
12400
13418
  }
12401
13419
  emitPatch() {
12402
13420
  const out = {};
@@ -12436,7 +13454,7 @@ class CascadeManagerTabComponent {
12436
13454
  navigator.clipboard?.writeText(json);
12437
13455
  }
12438
13456
  catch { }
12439
- alert('Regras exportadas para a área de transferência.');
13457
+ this.setFeedback('Regras exportadas para a área de transferência.', 'info');
12440
13458
  }
12441
13459
  applyImport() {
12442
13460
  let obj;
@@ -12444,9 +13462,10 @@ class CascadeManagerTabComponent {
12444
13462
  obj = JSON.parse(this.importJson || '{}');
12445
13463
  }
12446
13464
  catch {
12447
- alert('JSON inválido');
13465
+ this.setFeedback('JSON inválido na importação de regras.', 'error');
12448
13466
  return;
12449
13467
  }
13468
+ this.setFeedback('');
12450
13469
  const next = new Map(this.rules());
12451
13470
  Object.keys(obj || {}).forEach((field) => {
12452
13471
  const patch = obj[field] || {};
@@ -12463,14 +13482,26 @@ class CascadeManagerTabComponent {
12463
13482
  };
12464
13483
  next.set(field, vm);
12465
13484
  });
13485
+ const validation = this.svc.validateCascadeGraph(next, this.fields);
12466
13486
  this.rules.set(next);
13487
+ const sel = this.selectedField();
13488
+ if (sel && next.has(sel)) {
13489
+ this.startEdit(sel);
13490
+ }
13491
+ if (!validation.valid) {
13492
+ this.setFeedback(validation.cycles[0].message, 'error');
13493
+ this.syncValidityState();
13494
+ return;
13495
+ }
12467
13496
  this.emitPatch();
12468
13497
  this.importMode = false;
13498
+ this.syncValidityState();
12469
13499
  }
12470
13500
  startTemplate(name, tpl) {
12471
13501
  const hasR = this.hasField('rotaId');
12472
13502
  const hasD = this.hasField('destinoReqId');
12473
- const deps = tpl === 'rota-destino' ? (hasR ? ['rotaId'] : []) : (hasR && hasD ? ['rotaId', 'destinoReqId'] : []);
13503
+ const deps = (tpl === 'rota-destino' ? (hasR ? ['rotaId'] : []) : (hasR && hasD ? ['rotaId', 'destinoReqId'] : []))
13504
+ .filter((dep) => dep !== name);
12474
13505
  this.editDepList = [...deps];
12475
13506
  this.editEnable = true;
12476
13507
  this.editReset = true;
@@ -12478,40 +13509,73 @@ class CascadeManagerTabComponent {
12478
13509
  this.editLoadOn = 'respectLoadOn';
12479
13510
  this.editDebounce = 150;
12480
13511
  this.editValuePath = 'id';
13512
+ this.editInitialFilterMap = {};
13513
+ this.editInitialValuePath = 'id';
13514
+ this.editMapRows = [];
12481
13515
  this.buildMapRowsFromDeps();
12482
13516
  this.editing.set(true);
13517
+ this.syncValidityState();
12483
13518
  }
12484
13519
  buildMapRowsFromDeps() {
12485
13520
  const deps = this.editDepList;
12486
- const existing = (() => { try {
12487
- return JSON.parse(this.editFilterMapJson || '{}');
12488
- }
12489
- catch {
12490
- return {};
12491
- } })();
13521
+ const current = new Map(this.editMapRows.map((row) => [row.dep, row]));
12492
13522
  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 };
13523
+ const edited = current.get(dep);
13524
+ if (edited)
13525
+ return edited;
13526
+ const entry = this.editInitialFilterMap[dep];
13527
+ const key = typeof entry === 'string' ? entry : entry?.key || dep;
13528
+ return { dep, key, valuePath: this.initialRowValuePath(dep) };
12499
13529
  });
12500
13530
  }
13531
+ initialRowValuePath(dep) {
13532
+ const entry = this.editInitialFilterMap[dep];
13533
+ if (typeof entry === 'object' && entry?.valuePath)
13534
+ return entry.valuePath;
13535
+ return typeof this.editInitialValuePath === 'object'
13536
+ ? this.editInitialValuePath[dep]
13537
+ : undefined;
13538
+ }
12501
13539
  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');
13540
+ const initialDefault = typeof this.editInitialValuePath === 'string'
13541
+ ? this.editInitialValuePath
13542
+ : 'id';
13543
+ const defaultChanged = this.editValuePath !== initialDefault;
13544
+ const pathsChanged = this.editMapRows.some((row) => row.valuePath !== this.initialRowValuePath(row.dep));
13545
+ if (!defaultChanged && !pathsChanged)
13546
+ return structuredClone(this.editInitialValuePath);
13547
+ const wasRecord = typeof this.editInitialValuePath === 'object';
13548
+ if (!pathsChanged && !wasRecord)
13549
+ return this.editValuePath || 'id';
13550
+ // A specific extraction path must not erase the default/overrides used by
13551
+ // other dependencies. Unchanged documents keep their original shape above.
13552
+ const record = typeof this.editInitialValuePath === 'object'
13553
+ ? { ...this.editInitialValuePath }
13554
+ : {};
13555
+ for (const dep of deps) {
13556
+ const row = this.editMapRows.find((candidate) => candidate.dep === dep);
13557
+ if (row && row.valuePath !== this.initialRowValuePath(dep)) {
13558
+ record[dep] = row.valuePath || this.editValuePath || 'id';
13559
+ }
13560
+ else if (typeof this.editInitialValuePath === 'object') {
13561
+ const existing = this.editInitialValuePath[dep];
13562
+ if (existing !== undefined)
13563
+ record[dep] = existing;
13564
+ else if (defaultChanged)
13565
+ record[dep] = this.editValuePath || 'id';
13566
+ }
13567
+ else {
13568
+ record[dep] = this.editValuePath || 'id';
13569
+ }
13570
+ }
13571
+ return record;
12508
13572
  }
12509
13573
  buildPreview() {
12510
13574
  try {
12511
- const deps = this.editDepList;
12512
13575
  const frag = {};
12513
13576
  this.editMapRows.forEach((r) => {
12514
- const example = r.valuePath ? 123 : 123; // simple placeholder id
13577
+ const extractedProp = r.valuePath || this.editValuePath || 'id';
13578
+ const example = `<valor de ${r.dep}.${extractedProp}>`;
12515
13579
  const parts = (r.key || r.dep).split('.');
12516
13580
  let ref = frag;
12517
13581
  for (let i = 0; i < parts.length - 1; i++) {
@@ -12536,10 +13600,14 @@ class CascadeManagerTabComponent {
12536
13600
  const labelMap = this.svc.getFieldLabelMap(this.fields);
12537
13601
  return this.fields
12538
13602
  .filter((f) => f.name !== current)
12539
- .sort((a, b) => (labelMap.get(a.name) || a.name).localeCompare(labelMap.get(b.name) || b.name));
13603
+ .sort((a, b) => {
13604
+ const labelCmp = (labelMap.get(a.name) || a.name).localeCompare(labelMap.get(b.name) || b.name);
13605
+ return labelCmp !== 0 ? labelCmp : a.name.localeCompare(b.name);
13606
+ });
12540
13607
  }
12541
13608
  onDepListChange() {
12542
13609
  this.buildMapRowsFromDeps();
13610
+ this.syncValidityState();
12543
13611
  }
12544
13612
  getKeySuggestions(dep) {
12545
13613
  const suggestions = [dep, `${dep}.id`, `${dep}.value`];
@@ -12559,7 +13627,7 @@ class CascadeManagerTabComponent {
12559
13627
  return [...new Set(suggestions)];
12560
13628
  }
12561
13629
  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: `
13630
+ 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
13631
  <div class="cm-root">
12564
13632
  <!-- Left: Fields list -->
12565
13633
  <div class="cm-left">
@@ -12569,14 +13637,29 @@ class CascadeManagerTabComponent {
12569
13637
  </mat-form-field>
12570
13638
  @if (fields.length <= 50) {
12571
13639
  <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>
13640
+ @for (f of filteredFields(); track f.name) {
13641
+ <div
13642
+ (click)="selectField(f.name)"
13643
+ class="field-item"
13644
+ [class.selected]="f.name===selectedField()"
13645
+ [class.has-cycle]="isFieldInCycle(f.name)"
13646
+ tabindex="0"
13647
+ (keydown)="onFieldKeydown($event, f.name)"
13648
+ >
13649
+ <div class="field-item-main">
13650
+ <div class="field-item-title">{{ disambiguatedLabelFor(f.name) }}</div>
13651
+ <div class="field-item-meta">
13652
+ <span>{{ f.name }}</span>
13653
+ <span class="field-role-pill" [class.field-role-pill--remote]="hasRemoteOptionSource(f.name)">
13654
+ {{ fieldRoleBadge(f.name) }}
13655
+ </span>
13656
+ </div>
12577
13657
  </div>
12578
- @if (hasRule(f.name)) {
12579
- <div class="rule-badges">
13658
+ <div class="rule-badges">
13659
+ @if (isFieldInCycle(f.name)) {
13660
+ <span class="rule-badge b-conflict">Ciclo</span>
13661
+ }
13662
+ @if (hasRule(f.name)) {
12580
13663
  @if (isCascadeEnabled(f.name)) {
12581
13664
  <span class="rule-badge b-active">Ativo</span>
12582
13665
  }
@@ -12584,22 +13667,38 @@ class CascadeManagerTabComponent {
12584
13667
  <span class="rule-badge b-manual">Manual</span>
12585
13668
  }
12586
13669
  @if (loadOnChange(f.name)==='immediate') {
12587
- <span class="rule-badge b-immediate">Immediate</span>
13670
+ <span class="rule-badge b-immediate">Imediato</span>
12588
13671
  }
12589
- </div>
12590
- }
13672
+ }
13673
+ </div>
12591
13674
  </div>
12592
13675
  }
12593
13676
  </div>
12594
13677
  } @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>
13678
+ <cdk-virtual-scroll-viewport itemSize="48" style="height: calc(100vh - 240px)">
13679
+ <div
13680
+ *cdkVirtualFor="let f of filteredFields()"
13681
+ (click)="selectField(f.name)"
13682
+ class="field-item"
13683
+ [class.selected]="f.name===selectedField()"
13684
+ [class.has-cycle]="isFieldInCycle(f.name)"
13685
+ tabindex="0"
13686
+ (keydown)="onFieldKeydown($event, f.name)"
13687
+ >
13688
+ <div class="field-item-main">
13689
+ <div class="field-item-title">{{ disambiguatedLabelFor(f.name) }}</div>
13690
+ <div class="field-item-meta">
13691
+ <span>{{ f.name }}</span>
13692
+ <span class="field-role-pill" [class.field-role-pill--remote]="hasRemoteOptionSource(f.name)">
13693
+ {{ fieldRoleBadge(f.name) }}
13694
+ </span>
13695
+ </div>
12600
13696
  </div>
12601
- @if (hasRule(f.name)) {
12602
- <div class="rule-badges">
13697
+ <div class="rule-badges">
13698
+ @if (isFieldInCycle(f.name)) {
13699
+ <span class="rule-badge b-conflict">Ciclo</span>
13700
+ }
13701
+ @if (hasRule(f.name)) {
12603
13702
  @if (isCascadeEnabled(f.name)) {
12604
13703
  <span class="rule-badge b-active">Ativo</span>
12605
13704
  }
@@ -12607,27 +13706,27 @@ class CascadeManagerTabComponent {
12607
13706
  <span class="rule-badge b-manual">Manual</span>
12608
13707
  }
12609
13708
  @if (loadOnChange(f.name)==='immediate') {
12610
- <span class="rule-badge b-immediate">Immediate</span>
13709
+ <span class="rule-badge b-immediate">Imediato</span>
12611
13710
  }
12612
- </div>
12613
- }
13711
+ }
13712
+ </div>
12614
13713
  </div>
12615
13714
  </cdk-virtual-scroll-viewport>
12616
13715
  }
12617
13716
  </div>
12618
-
13717
+
12619
13718
  <!-- Right: Rules for selected field -->
12620
13719
  @if (selectedField(); as sel) {
12621
13720
  <div class="cm-right">
12622
13721
  <div class="cm-header">
12623
13722
  <div>
12624
- <h3 style="margin:0">{{ labelFor(sel) }}</h3>
12625
- <div style="opacity:.7">{{ sel }}</div>
13723
+ <h3 style="margin:0">{{ disambiguatedLabelFor(sel) }}</h3>
13724
+ <div style="opacity:.75; font-size:0.85rem">{{ sel }} · {{ fieldRoleSummary(sel) }}</div>
12626
13725
  </div>
12627
13726
  <div>
12628
13727
  <button mat-stroked-button color="primary" (click)="exportRules()" style="margin-right:8px">Exportar</button>
12629
13728
  <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>
13729
+ <button mat-flat-button color="primary" [matMenuTriggerFor]="addMenu">Modelos</button>
12631
13730
  <mat-menu #addMenu="matMenu">
12632
13731
  <button mat-menu-item (click)="startEdit(sel)"><mat-icon [praxisIcon]="'add'"></mat-icon><span>Em branco</span></button>
12633
13732
  <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 +13734,45 @@ class CascadeManagerTabComponent {
12635
13734
  </mat-menu>
12636
13735
  </div>
12637
13736
  </div>
13737
+
13738
+ @if (activeCycleDiagnostics().length > 0) {
13739
+ <div class="cm-cycle-alert" role="alert" data-testid="cascade-cycle-diagnostic">
13740
+ <mat-icon [praxisIcon]="'error'"></mat-icon>
13741
+ <div class="cm-cycle-alert-body">
13742
+ <strong>Dependência circular detectada — não é possível aplicar enquanto houver ciclo</strong>
13743
+ @for (cycle of activeCycleDiagnostics(); track cycle.formattedPath) {
13744
+ <div>{{ cycle.message }}</div>
13745
+ }
13746
+ @if (isFieldInCycle(sel)) {
13747
+ <div style="margin-top: 6px;">
13748
+ <button
13749
+ mat-stroked-button
13750
+ type="button"
13751
+ data-testid="cascade-break-cycle-button"
13752
+ (click)="breakCycleForField(sel)"
13753
+ >
13754
+ Remover dependência circular deste campo
13755
+ </button>
13756
+ </div>
13757
+ }
13758
+ </div>
13759
+ </div>
13760
+ }
13761
+
13762
+ @if (feedbackMessage) {
13763
+ <div class="cm-feedback" [class.cm-feedback--error]="feedbackTone === 'error'" role="status">{{ feedbackMessage }}</div>
13764
+ }
13765
+
13766
+ @if (!hasRemoteOptionSource(sel)) {
13767
+ <div class="cm-source-notice" role="note" data-testid="cascade-no-remote-source-notice">
13768
+ <mat-icon [praxisIcon]="'info'"></mat-icon>
13769
+ <div>
13770
+ Este campo ({{ fieldRoleBadge(sel) }}) não declara fonte de opções remota (<code class="kbd">resourcePath</code> / <code class="kbd">optionSource</code>).
13771
+ Campos de texto ou nomes resolvidos exibem valores derivados; para recarregar opções em cascata, selecione o campo selecionável (ex.: ID) correspondente.
13772
+ </div>
13773
+ </div>
13774
+ }
13775
+
12638
13776
  <!-- Conflict banner (connections) -->
12639
13777
  @if (hasConflict(sel)) {
12640
13778
  <div class="banner">
@@ -12647,43 +13785,56 @@ class CascadeManagerTabComponent {
12647
13785
  <button mat-stroked-button color="accent" (click)="quickSet(sel,'manual')">Definir manual</button>
12648
13786
  </div>
12649
13787
  }
13788
+
12650
13789
  @if (importMode) {
12651
13790
  <div class="import-panel">
12652
13791
  <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>
13792
+ <textarea rows="6" class="import-textarea" [(ngModel)]="importJson" placeholder='{ "campo": { "dependencyFields": ["dep"], ... } }'></textarea>
12654
13793
  <div style="display:flex; gap:8px; justify-content:flex-end; padding-top:8px">
12655
13794
  <button mat-stroked-button (click)="toggleImport()">Cancelar</button>
12656
13795
  <button mat-flat-button color="primary" (click)="applyImport()">Aplicar</button>
12657
13796
  </div>
12658
13797
  </div>
12659
13798
  }
12660
- @if (!ruleFor(sel)) {
13799
+
13800
+ @if (!hasRule(sel)) {
12661
13801
  <div>
12662
- <div class="rules-empty">Nenhuma cascata configurada. Clique em “Adicionar” para montar a dependência.</div>
13802
+ <div class="rules-empty" data-testid="cascade-empty-guidance">
13803
+ Nenhuma cascata salva para <strong>{{ disambiguatedLabelFor(sel) }}</strong>.
13804
+ 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>.
13805
+ </div>
12663
13806
  </div>
12664
13807
  }
12665
- @if (ruleFor(sel); as rule) {
13808
+ @if (hasRule(sel) && ruleFor(sel); as rule) {
12666
13809
  <div>
12667
13810
  <div class="rule-row">
12668
13811
  <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>
13812
+ <div><strong>Depende de (campos pais):</strong> {{ formatParentList(rule.dependencyFields) }}</div>
13813
+ <div class="rule-summary-direction">
13814
+ <strong>Fluxo:</strong> {{ formatRelationshipSummary(sel, rule.dependencyFields) }}
13815
+ </div>
13816
+ <div class="rule-summary-policies">
13817
+ Quando recarregar: {{ formatLoadOnLabel(rule.dependencyLoadOnChange) }} | Como combinar filtros: {{ formatMergeStrategyLabel(rule.dependencyMergeStrategy) }}
13818
+ </div>
12671
13819
  </div>
12672
13820
  <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>
13821
+ <button mat-icon-button type="button" aria-label="Editar regra de cascata" matTooltip="Editar" (click)="startEdit(sel)"><mat-icon [praxisIcon]="'edit'"></mat-icon></button>
13822
+ <button mat-icon-button type="button" aria-label="Duplicar regra de cascata" matTooltip="Duplicar" disabled><mat-icon [praxisIcon]="'content_copy'"></mat-icon></button>
13823
+ <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
13824
  </div>
12677
13825
  </div>
12678
13826
  </div>
12679
13827
  }
13828
+
12680
13829
  <!-- Editor com seções (sem acordeão duplo) -->
12681
13830
  <div class="panel-surface">
12682
- <div class="panel-header" style="padding: 12px 16px; border-bottom: 1px solid var(--md-sys-color-outline-variant);">
13831
+ <div class="panel-header" style="padding: 8px 4px; border-bottom: 1px solid var(--pfx-settings-panel-border, var(--md-sys-color-outline-variant));">
12683
13832
  <mat-icon [praxisIcon]="'rule'"></mat-icon>
12684
13833
  <div>
12685
13834
  <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>
13835
+ <small style="color: var(--md-sys-color-on-surface-variant);">
13836
+ Defina de quais campos pais este campo depende, quando recarregar opções e qual filtro enviar ao serviço
13837
+ </small>
12687
13838
  </div>
12688
13839
  </div>
12689
13840
  <div class="panel-stack">
@@ -12693,13 +13844,16 @@ class CascadeManagerTabComponent {
12693
13844
  <mat-panel-title class="panel-header"><mat-icon [praxisIcon]="'tune'"></mat-icon> Básico</mat-panel-title>
12694
13845
  </mat-expansion-panel-header>
12695
13846
  <div class="form-grid">
13847
+ <p class="section-caption">
13848
+ 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.
13849
+ </p>
12696
13850
  <div class="form-row">
12697
13851
  <mat-form-field appearance="outline" class="full">
12698
- <mat-label>Campos dependentes</mat-label>
13852
+ <mat-label>Este campo depende de (campos pais)</mat-label>
12699
13853
  <mat-select multiple [(ngModel)]="editDepList" (ngModelChange)="onDepListChange()">
12700
- @for (f of availableDependencyFields(); track f) {
13854
+ @for (f of availableDependencyFields(); track f.name) {
12701
13855
  <mat-option [value]="f.name">
12702
- {{ labelFor(f.name) }} ({{ f.name }})
13856
+ {{ disambiguatedLabelFor(f.name) }} ({{ f.name }}) · {{ fieldRoleBadge(f.name) }}
12703
13857
  </mat-option>
12704
13858
  }
12705
13859
  </mat-select>
@@ -12708,42 +13862,54 @@ class CascadeManagerTabComponent {
12708
13862
  matSuffix
12709
13863
  class="help-icon-button"
12710
13864
  type="button"
12711
- [matTooltip]="'Selecione os campos que controlam este valor'"
13865
+ [matTooltip]="'Selecione os campos pais (origem) cujo valor filtrará as opções deste campo (destino)'"
12712
13866
  matTooltipPosition="above"
12713
13867
  >
12714
13868
  <mat-icon [praxisIcon]="'help_outline'"></mat-icon>
12715
13869
  </button>
12716
13870
  </mat-form-field>
12717
13871
  </div>
13872
+ @if (editDepList.length > 0) {
13873
+ <div class="section-caption" data-testid="cascade-draft-direction-summary">
13874
+ <strong>Resumo da relação:</strong> {{ formatRelationshipSummary(sel, editDepList) }}
13875
+ </div>
13876
+ }
12718
13877
  <div class="form-row">
12719
- <mat-checkbox [(ngModel)]="editEnable">Ativar cascata nativa</mat-checkbox>
12720
- <mat-checkbox [(ngModel)]="editReset">Resetar ao mudar</mat-checkbox>
13878
+ <mat-checkbox [(ngModel)]="editEnable" (ngModelChange)="onDraftControlChange()">
13879
+ Ativar cascata automática neste campo
13880
+ </mat-checkbox>
13881
+ <mat-checkbox [(ngModel)]="editReset" (ngModelChange)="onDraftControlChange()">
13882
+ Limpar seleção deste campo quando o campo pai mudar
13883
+ </mat-checkbox>
12721
13884
  </div>
12722
13885
  </div>
12723
13886
  </mat-expansion-panel>
12724
13887
  <!-- Execução -->
12725
13888
  <mat-expansion-panel class="stack-panel">
12726
13889
  <mat-expansion-panel-header>
12727
- <mat-panel-title class="panel-header"><mat-icon [praxisIcon]="'schedule'"></mat-icon> Execução</mat-panel-title>
13890
+ <mat-panel-title class="panel-header"><mat-icon [praxisIcon]="'schedule'"></mat-icon> Quando recarregar e combinar filtros</mat-panel-title>
12728
13891
  </mat-expansion-panel-header>
13892
+ <p class="section-caption">
13893
+ Controle o momento em que a consulta de opções é disparada e como o filtro do campo pai convive com outros filtros do campo.
13894
+ </p>
12729
13895
  <div class="form-row">
12730
13896
  <mat-form-field appearance="outline">
12731
- <mat-label>Carregar ao mudar</mat-label>
13897
+ <mat-label>Quando recarregar opções</mat-label>
12732
13898
  <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>
13899
+ <mat-option value="respectLoadOn">Seguir configuração do campo (respectLoadOn)</mat-option>
13900
+ <mat-option value="immediate">Recarregar imediatamente ao mudar o pai (immediate)</mat-option>
13901
+ <mat-option value="manual">Somente sob comando manual / abertura (manual)</mat-option>
12736
13902
  </mat-select>
12737
13903
  </mat-form-field>
12738
13904
  <mat-form-field appearance="outline">
12739
- <mat-label>Merge de filtros</mat-label>
13905
+ <mat-label>Como combinar filtros</mat-label>
12740
13906
  <mat-select [(ngModel)]="editMerge">
12741
- <mat-option value="merge">merge (padrão)</mat-option>
12742
- <mat-option value="replace">replace</mat-option>
13907
+ <mat-option value="merge">Mesclar com filtros existentes (merge — padrão)</mat-option>
13908
+ <mat-option value="replace">Substituir filtros anteriores (replace)</mat-option>
12743
13909
  </mat-select>
12744
13910
  </mat-form-field>
12745
13911
  <mat-form-field appearance="outline">
12746
- <mat-label>Atraso (ms)</mat-label>
13912
+ <mat-label>Atraso após mudança (ms)</mat-label>
12747
13913
  <input matInput type="number" [(ngModel)]="editDebounce">
12748
13914
  </mat-form-field>
12749
13915
  </div>
@@ -12751,18 +13917,21 @@ class CascadeManagerTabComponent {
12751
13917
  <!-- Extração de valor -->
12752
13918
  <mat-expansion-panel class="stack-panel">
12753
13919
  <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>
13920
+ <mat-panel-title class="panel-header"><mat-icon [praxisIcon]="'call_split'"></mat-icon> Qual propriedade ler do campo pai</mat-panel-title>
12755
13921
  </mat-expansion-panel-header>
13922
+ <p class="section-caption">
13923
+ 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>).
13924
+ </p>
12756
13925
  <div class="form-row">
12757
13926
  <mat-form-field appearance="outline" class="full">
12758
- <mat-label>ValuePath padrão</mat-label>
13927
+ <mat-label>Propriedade padrão lida do campo pai (ValuePath)</mat-label>
12759
13928
  <input matInput [(ngModel)]="editValuePath" placeholder="id">
12760
13929
  <button
12761
13930
  mat-icon-button
12762
13931
  matSuffix
12763
13932
  class="help-icon-button"
12764
13933
  type="button"
12765
- [matTooltip]="'Preenchido automaticamente a partir de optionValueKey() ou \\'id\\''"
13934
+ [matTooltip]="'Preenchido automaticamente a partir de optionValueKey do campo pai ou \\'id\\''"
12766
13935
  matTooltipPosition="above"
12767
13936
  >
12768
13937
  <mat-icon [praxisIcon]="'help_outline'"></mat-icon>
@@ -12773,24 +13942,30 @@ class CascadeManagerTabComponent {
12773
13942
  <!-- Mapeamento de filtros (tabela) -->
12774
13943
  <mat-expansion-panel class="stack-panel">
12775
13944
  <mat-expansion-panel-header>
12776
- <mat-panel-title class="panel-header"><mat-icon [praxisIcon]="'filter_alt'"></mat-icon> Mapeamento de filtros</mat-panel-title>
13945
+ <mat-panel-title class="panel-header"><mat-icon [praxisIcon]="'filter_alt'"></mat-icon> Mapeamento de filtros enviados ao serviço</mat-panel-title>
12777
13946
  </mat-expansion-panel-header>
12778
13947
  @if (editMapRows.length) {
13948
+ <p class="section-caption">
13949
+ 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.
13950
+ </p>
12779
13951
  <table class="map-table">
12780
13952
  <thead>
12781
13953
  <tr>
12782
- <th>Dependente</th>
12783
- <th>Chave (dot‑path)</th>
12784
- <th>ValuePath (opcional)</th>
13954
+ <th>Campo pai (origem)</th>
13955
+ <th>Parâmetro enviado ao filtro (dot‑path)</th>
13956
+ <th>Propriedade lida do pai (opcional)</th>
12785
13957
  </tr>
12786
13958
  </thead>
12787
13959
  <tbody>
12788
- @for (row of editMapRows; track row; let i = $index) {
13960
+ @for (row of editMapRows; track row.dep; let i = $index) {
12789
13961
  <tr>
12790
- <td>{{ row.dep }}</td>
13962
+ <td>
13963
+ <div><strong>{{ disambiguatedLabelFor(row.dep) }}</strong></div>
13964
+ <small style="opacity:.75">{{ row.dep }}</small>
13965
+ </td>
12791
13966
  <td>
12792
13967
  <mat-form-field appearance="outline" class="full">
12793
- <mat-label>Chave</mat-label>
13968
+ <mat-label>Parâmetro no filtro</mat-label>
12794
13969
  <input matInput [(ngModel)]="row.key" [matAutocomplete]="autoKey" [placeholder]="row.dep">
12795
13970
  <mat-autocomplete #autoKey="matAutocomplete">
12796
13971
  @for (opt of getKeySuggestions(row.dep); track opt) {
@@ -12802,7 +13977,7 @@ class CascadeManagerTabComponent {
12802
13977
  matSuffix
12803
13978
  class="help-icon-button"
12804
13979
  type="button"
12805
- [matTooltip]="'Use dot-notation (e.g. ' + row.dep + '.id) for nested objects'"
13980
+ [matTooltip]="'Use notação com ponto (ex.: ' + row.dep + '.id) quando o backend esperar filtro aninhado'"
12806
13981
  matTooltipPosition="above"
12807
13982
  >
12808
13983
  <mat-icon [praxisIcon]="'help_outline'"></mat-icon>
@@ -12811,7 +13986,7 @@ class CascadeManagerTabComponent {
12811
13986
  </td>
12812
13987
  <td>
12813
13988
  <mat-form-field appearance="outline" class="full">
12814
- <mat-label>ValuePath</mat-label>
13989
+ <mat-label>Propriedade do pai (ValuePath)</mat-label>
12815
13990
  <input matInput [(ngModel)]="row.valuePath" [matAutocomplete]="autoValue" [placeholder]="editValuePath||'id'">
12816
13991
  <mat-autocomplete #autoValue="matAutocomplete">
12817
13992
  @for (opt of getValuePathSuggestions(row.dep); track opt) {
@@ -12823,7 +13998,7 @@ class CascadeManagerTabComponent {
12823
13998
  matSuffix
12824
13999
  class="help-icon-button"
12825
14000
  type="button"
12826
- [matTooltip]="'Opcional'"
14001
+ [matTooltip]="'Opcional: sobrescreve a propriedade padrão lida apenas para este campo pai'"
12827
14002
  matTooltipPosition="above"
12828
14003
  >
12829
14004
  <mat-icon [praxisIcon]="'help_outline'"></mat-icon>
@@ -12835,29 +14010,32 @@ class CascadeManagerTabComponent {
12835
14010
  </tbody>
12836
14011
  </table>
12837
14012
  } @else {
12838
- <div class="rules-empty">Defina dependentes para sugerir o mapeamento.</div>
14013
+ <div class="rules-empty">Selecione um ou mais campos pais em “Este campo depende de” para configurar o mapeamento de filtros.</div>
12839
14014
  }
12840
14015
  </mat-expansion-panel>
12841
14016
  <!-- Preview -->
12842
14017
  <mat-expansion-panel class="stack-panel">
12843
14018
  <mat-expansion-panel-header>
12844
- <mat-panel-title class="panel-header"><mat-icon [praxisIcon]="'visibility'"></mat-icon> Preview</mat-panel-title>
14019
+ <mat-panel-title class="panel-header"><mat-icon [praxisIcon]="'visibility'"></mat-icon> Exemplo ilustrativo do filtro enviado (Preview)</mat-panel-title>
12845
14020
  </mat-expansion-panel-header>
14021
+ <p class="section-caption" data-testid="cascade-preview-explanation">
14022
+ 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>.
14023
+ </p>
12846
14024
  <pre class="preview-box">{{ buildPreview() }}</pre>
12847
14025
  <div style="display:flex; justify-content:flex-end; padding-top:6px">
12848
- <button mat-stroked-button (click)="copyPreview()">Copiar</button>
14026
+ <button mat-stroked-button (click)="copyPreview()">Copiar exemplo</button>
12849
14027
  </div>
12850
14028
  </mat-expansion-panel>
12851
14029
  </div>
12852
14030
  <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>
14031
+ <button mat-stroked-button type="button" (click)="cancelEdit()">Cancelar</button>
14032
+ <button mat-flat-button color="primary" type="button" (click)="saveEdit(sel)">Salvar</button>
12855
14033
  </div>
12856
14034
  </div>
12857
14035
  </div>
12858
14036
  }
12859
14037
  </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"] }] });
14038
+ `, 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
14039
  }
12862
14040
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: CascadeManagerTabComponent, decorators: [{
12863
14041
  type: Component,
@@ -12888,14 +14066,29 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
12888
14066
  </mat-form-field>
12889
14067
  @if (fields.length <= 50) {
12890
14068
  <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>
14069
+ @for (f of filteredFields(); track f.name) {
14070
+ <div
14071
+ (click)="selectField(f.name)"
14072
+ class="field-item"
14073
+ [class.selected]="f.name===selectedField()"
14074
+ [class.has-cycle]="isFieldInCycle(f.name)"
14075
+ tabindex="0"
14076
+ (keydown)="onFieldKeydown($event, f.name)"
14077
+ >
14078
+ <div class="field-item-main">
14079
+ <div class="field-item-title">{{ disambiguatedLabelFor(f.name) }}</div>
14080
+ <div class="field-item-meta">
14081
+ <span>{{ f.name }}</span>
14082
+ <span class="field-role-pill" [class.field-role-pill--remote]="hasRemoteOptionSource(f.name)">
14083
+ {{ fieldRoleBadge(f.name) }}
14084
+ </span>
14085
+ </div>
12896
14086
  </div>
12897
- @if (hasRule(f.name)) {
12898
- <div class="rule-badges">
14087
+ <div class="rule-badges">
14088
+ @if (isFieldInCycle(f.name)) {
14089
+ <span class="rule-badge b-conflict">Ciclo</span>
14090
+ }
14091
+ @if (hasRule(f.name)) {
12899
14092
  @if (isCascadeEnabled(f.name)) {
12900
14093
  <span class="rule-badge b-active">Ativo</span>
12901
14094
  }
@@ -12903,22 +14096,38 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
12903
14096
  <span class="rule-badge b-manual">Manual</span>
12904
14097
  }
12905
14098
  @if (loadOnChange(f.name)==='immediate') {
12906
- <span class="rule-badge b-immediate">Immediate</span>
14099
+ <span class="rule-badge b-immediate">Imediato</span>
12907
14100
  }
12908
- </div>
12909
- }
14101
+ }
14102
+ </div>
12910
14103
  </div>
12911
14104
  }
12912
14105
  </div>
12913
14106
  } @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>
14107
+ <cdk-virtual-scroll-viewport itemSize="48" style="height: calc(100vh - 240px)">
14108
+ <div
14109
+ *cdkVirtualFor="let f of filteredFields()"
14110
+ (click)="selectField(f.name)"
14111
+ class="field-item"
14112
+ [class.selected]="f.name===selectedField()"
14113
+ [class.has-cycle]="isFieldInCycle(f.name)"
14114
+ tabindex="0"
14115
+ (keydown)="onFieldKeydown($event, f.name)"
14116
+ >
14117
+ <div class="field-item-main">
14118
+ <div class="field-item-title">{{ disambiguatedLabelFor(f.name) }}</div>
14119
+ <div class="field-item-meta">
14120
+ <span>{{ f.name }}</span>
14121
+ <span class="field-role-pill" [class.field-role-pill--remote]="hasRemoteOptionSource(f.name)">
14122
+ {{ fieldRoleBadge(f.name) }}
14123
+ </span>
14124
+ </div>
12919
14125
  </div>
12920
- @if (hasRule(f.name)) {
12921
- <div class="rule-badges">
14126
+ <div class="rule-badges">
14127
+ @if (isFieldInCycle(f.name)) {
14128
+ <span class="rule-badge b-conflict">Ciclo</span>
14129
+ }
14130
+ @if (hasRule(f.name)) {
12922
14131
  @if (isCascadeEnabled(f.name)) {
12923
14132
  <span class="rule-badge b-active">Ativo</span>
12924
14133
  }
@@ -12926,27 +14135,27 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
12926
14135
  <span class="rule-badge b-manual">Manual</span>
12927
14136
  }
12928
14137
  @if (loadOnChange(f.name)==='immediate') {
12929
- <span class="rule-badge b-immediate">Immediate</span>
14138
+ <span class="rule-badge b-immediate">Imediato</span>
12930
14139
  }
12931
- </div>
12932
- }
14140
+ }
14141
+ </div>
12933
14142
  </div>
12934
14143
  </cdk-virtual-scroll-viewport>
12935
14144
  }
12936
14145
  </div>
12937
-
14146
+
12938
14147
  <!-- Right: Rules for selected field -->
12939
14148
  @if (selectedField(); as sel) {
12940
14149
  <div class="cm-right">
12941
14150
  <div class="cm-header">
12942
14151
  <div>
12943
- <h3 style="margin:0">{{ labelFor(sel) }}</h3>
12944
- <div style="opacity:.7">{{ sel }}</div>
14152
+ <h3 style="margin:0">{{ disambiguatedLabelFor(sel) }}</h3>
14153
+ <div style="opacity:.75; font-size:0.85rem">{{ sel }} · {{ fieldRoleSummary(sel) }}</div>
12945
14154
  </div>
12946
14155
  <div>
12947
14156
  <button mat-stroked-button color="primary" (click)="exportRules()" style="margin-right:8px">Exportar</button>
12948
14157
  <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>
14158
+ <button mat-flat-button color="primary" [matMenuTriggerFor]="addMenu">Modelos</button>
12950
14159
  <mat-menu #addMenu="matMenu">
12951
14160
  <button mat-menu-item (click)="startEdit(sel)"><mat-icon [praxisIcon]="'add'"></mat-icon><span>Em branco</span></button>
12952
14161
  <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 +14163,45 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
12954
14163
  </mat-menu>
12955
14164
  </div>
12956
14165
  </div>
14166
+
14167
+ @if (activeCycleDiagnostics().length > 0) {
14168
+ <div class="cm-cycle-alert" role="alert" data-testid="cascade-cycle-diagnostic">
14169
+ <mat-icon [praxisIcon]="'error'"></mat-icon>
14170
+ <div class="cm-cycle-alert-body">
14171
+ <strong>Dependência circular detectada — não é possível aplicar enquanto houver ciclo</strong>
14172
+ @for (cycle of activeCycleDiagnostics(); track cycle.formattedPath) {
14173
+ <div>{{ cycle.message }}</div>
14174
+ }
14175
+ @if (isFieldInCycle(sel)) {
14176
+ <div style="margin-top: 6px;">
14177
+ <button
14178
+ mat-stroked-button
14179
+ type="button"
14180
+ data-testid="cascade-break-cycle-button"
14181
+ (click)="breakCycleForField(sel)"
14182
+ >
14183
+ Remover dependência circular deste campo
14184
+ </button>
14185
+ </div>
14186
+ }
14187
+ </div>
14188
+ </div>
14189
+ }
14190
+
14191
+ @if (feedbackMessage) {
14192
+ <div class="cm-feedback" [class.cm-feedback--error]="feedbackTone === 'error'" role="status">{{ feedbackMessage }}</div>
14193
+ }
14194
+
14195
+ @if (!hasRemoteOptionSource(sel)) {
14196
+ <div class="cm-source-notice" role="note" data-testid="cascade-no-remote-source-notice">
14197
+ <mat-icon [praxisIcon]="'info'"></mat-icon>
14198
+ <div>
14199
+ Este campo ({{ fieldRoleBadge(sel) }}) não declara fonte de opções remota (<code class="kbd">resourcePath</code> / <code class="kbd">optionSource</code>).
14200
+ Campos de texto ou nomes resolvidos exibem valores derivados; para recarregar opções em cascata, selecione o campo selecionável (ex.: ID) correspondente.
14201
+ </div>
14202
+ </div>
14203
+ }
14204
+
12957
14205
  <!-- Conflict banner (connections) -->
12958
14206
  @if (hasConflict(sel)) {
12959
14207
  <div class="banner">
@@ -12966,43 +14214,56 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
12966
14214
  <button mat-stroked-button color="accent" (click)="quickSet(sel,'manual')">Definir manual</button>
12967
14215
  </div>
12968
14216
  }
14217
+
12969
14218
  @if (importMode) {
12970
14219
  <div class="import-panel">
12971
14220
  <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>
14221
+ <textarea rows="6" class="import-textarea" [(ngModel)]="importJson" placeholder='{ "campo": { "dependencyFields": ["dep"], ... } }'></textarea>
12973
14222
  <div style="display:flex; gap:8px; justify-content:flex-end; padding-top:8px">
12974
14223
  <button mat-stroked-button (click)="toggleImport()">Cancelar</button>
12975
14224
  <button mat-flat-button color="primary" (click)="applyImport()">Aplicar</button>
12976
14225
  </div>
12977
14226
  </div>
12978
14227
  }
12979
- @if (!ruleFor(sel)) {
14228
+
14229
+ @if (!hasRule(sel)) {
12980
14230
  <div>
12981
- <div class="rules-empty">Nenhuma cascata configurada. Clique em “Adicionar” para montar a dependência.</div>
14231
+ <div class="rules-empty" data-testid="cascade-empty-guidance">
14232
+ Nenhuma cascata salva para <strong>{{ disambiguatedLabelFor(sel) }}</strong>.
14233
+ 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>.
14234
+ </div>
12982
14235
  </div>
12983
14236
  }
12984
- @if (ruleFor(sel); as rule) {
14237
+ @if (hasRule(sel) && ruleFor(sel); as rule) {
12985
14238
  <div>
12986
14239
  <div class="rule-row">
12987
14240
  <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>
14241
+ <div><strong>Depende de (campos pais):</strong> {{ formatParentList(rule.dependencyFields) }}</div>
14242
+ <div class="rule-summary-direction">
14243
+ <strong>Fluxo:</strong> {{ formatRelationshipSummary(sel, rule.dependencyFields) }}
14244
+ </div>
14245
+ <div class="rule-summary-policies">
14246
+ Quando recarregar: {{ formatLoadOnLabel(rule.dependencyLoadOnChange) }} | Como combinar filtros: {{ formatMergeStrategyLabel(rule.dependencyMergeStrategy) }}
14247
+ </div>
12990
14248
  </div>
12991
14249
  <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>
14250
+ <button mat-icon-button type="button" aria-label="Editar regra de cascata" matTooltip="Editar" (click)="startEdit(sel)"><mat-icon [praxisIcon]="'edit'"></mat-icon></button>
14251
+ <button mat-icon-button type="button" aria-label="Duplicar regra de cascata" matTooltip="Duplicar" disabled><mat-icon [praxisIcon]="'content_copy'"></mat-icon></button>
14252
+ <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
14253
  </div>
12996
14254
  </div>
12997
14255
  </div>
12998
14256
  }
14257
+
12999
14258
  <!-- Editor com seções (sem acordeão duplo) -->
13000
14259
  <div class="panel-surface">
13001
- <div class="panel-header" style="padding: 12px 16px; border-bottom: 1px solid var(--md-sys-color-outline-variant);">
14260
+ <div class="panel-header" style="padding: 8px 4px; border-bottom: 1px solid var(--pfx-settings-panel-border, var(--md-sys-color-outline-variant));">
13002
14261
  <mat-icon [praxisIcon]="'rule'"></mat-icon>
13003
14262
  <div>
13004
14263
  <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>
14264
+ <small style="color: var(--md-sys-color-on-surface-variant);">
14265
+ Defina de quais campos pais este campo depende, quando recarregar opções e qual filtro enviar ao serviço
14266
+ </small>
13006
14267
  </div>
13007
14268
  </div>
13008
14269
  <div class="panel-stack">
@@ -13012,13 +14273,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
13012
14273
  <mat-panel-title class="panel-header"><mat-icon [praxisIcon]="'tune'"></mat-icon> Básico</mat-panel-title>
13013
14274
  </mat-expansion-panel-header>
13014
14275
  <div class="form-grid">
14276
+ <p class="section-caption">
14277
+ 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.
14278
+ </p>
13015
14279
  <div class="form-row">
13016
14280
  <mat-form-field appearance="outline" class="full">
13017
- <mat-label>Campos dependentes</mat-label>
14281
+ <mat-label>Este campo depende de (campos pais)</mat-label>
13018
14282
  <mat-select multiple [(ngModel)]="editDepList" (ngModelChange)="onDepListChange()">
13019
- @for (f of availableDependencyFields(); track f) {
14283
+ @for (f of availableDependencyFields(); track f.name) {
13020
14284
  <mat-option [value]="f.name">
13021
- {{ labelFor(f.name) }} ({{ f.name }})
14285
+ {{ disambiguatedLabelFor(f.name) }} ({{ f.name }}) · {{ fieldRoleBadge(f.name) }}
13022
14286
  </mat-option>
13023
14287
  }
13024
14288
  </mat-select>
@@ -13027,42 +14291,54 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
13027
14291
  matSuffix
13028
14292
  class="help-icon-button"
13029
14293
  type="button"
13030
- [matTooltip]="'Selecione os campos que controlam este valor'"
14294
+ [matTooltip]="'Selecione os campos pais (origem) cujo valor filtrará as opções deste campo (destino)'"
13031
14295
  matTooltipPosition="above"
13032
14296
  >
13033
14297
  <mat-icon [praxisIcon]="'help_outline'"></mat-icon>
13034
14298
  </button>
13035
14299
  </mat-form-field>
13036
14300
  </div>
14301
+ @if (editDepList.length > 0) {
14302
+ <div class="section-caption" data-testid="cascade-draft-direction-summary">
14303
+ <strong>Resumo da relação:</strong> {{ formatRelationshipSummary(sel, editDepList) }}
14304
+ </div>
14305
+ }
13037
14306
  <div class="form-row">
13038
- <mat-checkbox [(ngModel)]="editEnable">Ativar cascata nativa</mat-checkbox>
13039
- <mat-checkbox [(ngModel)]="editReset">Resetar ao mudar</mat-checkbox>
14307
+ <mat-checkbox [(ngModel)]="editEnable" (ngModelChange)="onDraftControlChange()">
14308
+ Ativar cascata automática neste campo
14309
+ </mat-checkbox>
14310
+ <mat-checkbox [(ngModel)]="editReset" (ngModelChange)="onDraftControlChange()">
14311
+ Limpar seleção deste campo quando o campo pai mudar
14312
+ </mat-checkbox>
13040
14313
  </div>
13041
14314
  </div>
13042
14315
  </mat-expansion-panel>
13043
14316
  <!-- Execução -->
13044
14317
  <mat-expansion-panel class="stack-panel">
13045
14318
  <mat-expansion-panel-header>
13046
- <mat-panel-title class="panel-header"><mat-icon [praxisIcon]="'schedule'"></mat-icon> Execução</mat-panel-title>
14319
+ <mat-panel-title class="panel-header"><mat-icon [praxisIcon]="'schedule'"></mat-icon> Quando recarregar e combinar filtros</mat-panel-title>
13047
14320
  </mat-expansion-panel-header>
14321
+ <p class="section-caption">
14322
+ Controle o momento em que a consulta de opções é disparada e como o filtro do campo pai convive com outros filtros do campo.
14323
+ </p>
13048
14324
  <div class="form-row">
13049
14325
  <mat-form-field appearance="outline">
13050
- <mat-label>Carregar ao mudar</mat-label>
14326
+ <mat-label>Quando recarregar opções</mat-label>
13051
14327
  <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>
14328
+ <mat-option value="respectLoadOn">Seguir configuração do campo (respectLoadOn)</mat-option>
14329
+ <mat-option value="immediate">Recarregar imediatamente ao mudar o pai (immediate)</mat-option>
14330
+ <mat-option value="manual">Somente sob comando manual / abertura (manual)</mat-option>
13055
14331
  </mat-select>
13056
14332
  </mat-form-field>
13057
14333
  <mat-form-field appearance="outline">
13058
- <mat-label>Merge de filtros</mat-label>
14334
+ <mat-label>Como combinar filtros</mat-label>
13059
14335
  <mat-select [(ngModel)]="editMerge">
13060
- <mat-option value="merge">merge (padrão)</mat-option>
13061
- <mat-option value="replace">replace</mat-option>
14336
+ <mat-option value="merge">Mesclar com filtros existentes (merge — padrão)</mat-option>
14337
+ <mat-option value="replace">Substituir filtros anteriores (replace)</mat-option>
13062
14338
  </mat-select>
13063
14339
  </mat-form-field>
13064
14340
  <mat-form-field appearance="outline">
13065
- <mat-label>Atraso (ms)</mat-label>
14341
+ <mat-label>Atraso após mudança (ms)</mat-label>
13066
14342
  <input matInput type="number" [(ngModel)]="editDebounce">
13067
14343
  </mat-form-field>
13068
14344
  </div>
@@ -13070,18 +14346,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
13070
14346
  <!-- Extração de valor -->
13071
14347
  <mat-expansion-panel class="stack-panel">
13072
14348
  <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>
14349
+ <mat-panel-title class="panel-header"><mat-icon [praxisIcon]="'call_split'"></mat-icon> Qual propriedade ler do campo pai</mat-panel-title>
13074
14350
  </mat-expansion-panel-header>
14351
+ <p class="section-caption">
14352
+ 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>).
14353
+ </p>
13075
14354
  <div class="form-row">
13076
14355
  <mat-form-field appearance="outline" class="full">
13077
- <mat-label>ValuePath padrão</mat-label>
14356
+ <mat-label>Propriedade padrão lida do campo pai (ValuePath)</mat-label>
13078
14357
  <input matInput [(ngModel)]="editValuePath" placeholder="id">
13079
14358
  <button
13080
14359
  mat-icon-button
13081
14360
  matSuffix
13082
14361
  class="help-icon-button"
13083
14362
  type="button"
13084
- [matTooltip]="'Preenchido automaticamente a partir de optionValueKey() ou \\'id\\''"
14363
+ [matTooltip]="'Preenchido automaticamente a partir de optionValueKey do campo pai ou \\'id\\''"
13085
14364
  matTooltipPosition="above"
13086
14365
  >
13087
14366
  <mat-icon [praxisIcon]="'help_outline'"></mat-icon>
@@ -13092,24 +14371,30 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
13092
14371
  <!-- Mapeamento de filtros (tabela) -->
13093
14372
  <mat-expansion-panel class="stack-panel">
13094
14373
  <mat-expansion-panel-header>
13095
- <mat-panel-title class="panel-header"><mat-icon [praxisIcon]="'filter_alt'"></mat-icon> Mapeamento de filtros</mat-panel-title>
14374
+ <mat-panel-title class="panel-header"><mat-icon [praxisIcon]="'filter_alt'"></mat-icon> Mapeamento de filtros enviados ao serviço</mat-panel-title>
13096
14375
  </mat-expansion-panel-header>
13097
14376
  @if (editMapRows.length) {
14377
+ <p class="section-caption">
14378
+ 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.
14379
+ </p>
13098
14380
  <table class="map-table">
13099
14381
  <thead>
13100
14382
  <tr>
13101
- <th>Dependente</th>
13102
- <th>Chave (dot‑path)</th>
13103
- <th>ValuePath (opcional)</th>
14383
+ <th>Campo pai (origem)</th>
14384
+ <th>Parâmetro enviado ao filtro (dot‑path)</th>
14385
+ <th>Propriedade lida do pai (opcional)</th>
13104
14386
  </tr>
13105
14387
  </thead>
13106
14388
  <tbody>
13107
- @for (row of editMapRows; track row; let i = $index) {
14389
+ @for (row of editMapRows; track row.dep; let i = $index) {
13108
14390
  <tr>
13109
- <td>{{ row.dep }}</td>
14391
+ <td>
14392
+ <div><strong>{{ disambiguatedLabelFor(row.dep) }}</strong></div>
14393
+ <small style="opacity:.75">{{ row.dep }}</small>
14394
+ </td>
13110
14395
  <td>
13111
14396
  <mat-form-field appearance="outline" class="full">
13112
- <mat-label>Chave</mat-label>
14397
+ <mat-label>Parâmetro no filtro</mat-label>
13113
14398
  <input matInput [(ngModel)]="row.key" [matAutocomplete]="autoKey" [placeholder]="row.dep">
13114
14399
  <mat-autocomplete #autoKey="matAutocomplete">
13115
14400
  @for (opt of getKeySuggestions(row.dep); track opt) {
@@ -13121,7 +14406,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
13121
14406
  matSuffix
13122
14407
  class="help-icon-button"
13123
14408
  type="button"
13124
- [matTooltip]="'Use dot-notation (e.g. ' + row.dep + '.id) for nested objects'"
14409
+ [matTooltip]="'Use notação com ponto (ex.: ' + row.dep + '.id) quando o backend esperar filtro aninhado'"
13125
14410
  matTooltipPosition="above"
13126
14411
  >
13127
14412
  <mat-icon [praxisIcon]="'help_outline'"></mat-icon>
@@ -13130,7 +14415,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
13130
14415
  </td>
13131
14416
  <td>
13132
14417
  <mat-form-field appearance="outline" class="full">
13133
- <mat-label>ValuePath</mat-label>
14418
+ <mat-label>Propriedade do pai (ValuePath)</mat-label>
13134
14419
  <input matInput [(ngModel)]="row.valuePath" [matAutocomplete]="autoValue" [placeholder]="editValuePath||'id'">
13135
14420
  <mat-autocomplete #autoValue="matAutocomplete">
13136
14421
  @for (opt of getValuePathSuggestions(row.dep); track opt) {
@@ -13142,7 +14427,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
13142
14427
  matSuffix
13143
14428
  class="help-icon-button"
13144
14429
  type="button"
13145
- [matTooltip]="'Opcional'"
14430
+ [matTooltip]="'Opcional: sobrescreve a propriedade padrão lida apenas para este campo pai'"
13146
14431
  matTooltipPosition="above"
13147
14432
  >
13148
14433
  <mat-icon [praxisIcon]="'help_outline'"></mat-icon>
@@ -13154,29 +14439,32 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
13154
14439
  </tbody>
13155
14440
  </table>
13156
14441
  } @else {
13157
- <div class="rules-empty">Defina dependentes para sugerir o mapeamento.</div>
14442
+ <div class="rules-empty">Selecione um ou mais campos pais em “Este campo depende de” para configurar o mapeamento de filtros.</div>
13158
14443
  }
13159
14444
  </mat-expansion-panel>
13160
14445
  <!-- Preview -->
13161
14446
  <mat-expansion-panel class="stack-panel">
13162
14447
  <mat-expansion-panel-header>
13163
- <mat-panel-title class="panel-header"><mat-icon [praxisIcon]="'visibility'"></mat-icon> Preview</mat-panel-title>
14448
+ <mat-panel-title class="panel-header"><mat-icon [praxisIcon]="'visibility'"></mat-icon> Exemplo ilustrativo do filtro enviado (Preview)</mat-panel-title>
13164
14449
  </mat-expansion-panel-header>
14450
+ <p class="section-caption" data-testid="cascade-preview-explanation">
14451
+ 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>.
14452
+ </p>
13165
14453
  <pre class="preview-box">{{ buildPreview() }}</pre>
13166
14454
  <div style="display:flex; justify-content:flex-end; padding-top:6px">
13167
- <button mat-stroked-button (click)="copyPreview()">Copiar</button>
14455
+ <button mat-stroked-button (click)="copyPreview()">Copiar exemplo</button>
13168
14456
  </div>
13169
14457
  </mat-expansion-panel>
13170
14458
  </div>
13171
14459
  <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>
14460
+ <button mat-stroked-button type="button" (click)="cancelEdit()">Cancelar</button>
14461
+ <button mat-flat-button color="primary" type="button" (click)="saveEdit(sel)">Salvar</button>
13174
14462
  </div>
13175
14463
  </div>
13176
14464
  </div>
13177
14465
  }
13178
14466
  </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"] }]
14467
+ `, 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
14468
  }], ctorParameters: () => [{ type: CascadeRulesService }], propDecorators: { fields: [{
13181
14469
  type: Input
13182
14470
  }], connections: [{
@@ -13185,6 +14473,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
13185
14473
  type: Output
13186
14474
  }], cancel: [{
13187
14475
  type: Output
14476
+ }], validChange: [{
14477
+ type: Output
13188
14478
  }] } });
13189
14479
 
13190
14480
  /**