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