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