@praxisui/metadata-editor 10.0.0-rc.37 → 10.0.0-rc.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -6,7 +6,7 @@ import * as i3$2 from '@angular/common';
|
|
|
6
6
|
import { CommonModule } from '@angular/common';
|
|
7
7
|
import { isValidCssColor, DynamicFieldLoaderDirective, normalizeCssColorToRgb, providePraxisDynamicFieldsI18n } from '@praxisui/dynamic-fields';
|
|
8
8
|
import * as i2$2 from '@praxisui/core';
|
|
9
|
-
import { providePraxisI18n, PraxisI18nService, PraxisIconDirective, FieldControlType, INLINE_FILTER_CONTROL_TYPES, resolveInlineFilterControlType, ResourceDiscoveryService, normalizeControlTypeToken, providePraxisI18nConfig, RESOURCE_DISCOVERY_I18N_CONFIG, 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';
|
|
@@ -653,6 +653,8 @@ const METADATA_EDITOR_EN_US = {
|
|
|
653
653
|
'praxis.metadataEditor.colorPalette.moveDown': 'Move down',
|
|
654
654
|
'praxis.metadataEditor.colorPalette.remove': 'Remove',
|
|
655
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.',
|
|
656
658
|
};
|
|
657
659
|
|
|
658
660
|
const METADATA_EDITOR_PT_BR = {
|
|
@@ -905,6 +907,8 @@ const METADATA_EDITOR_PT_BR = {
|
|
|
905
907
|
'praxis.metadataEditor.colorPalette.moveDown': 'Mover para baixo',
|
|
906
908
|
'praxis.metadataEditor.colorPalette.remove': 'Remover',
|
|
907
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.',
|
|
908
912
|
};
|
|
909
913
|
|
|
910
914
|
function createPraxisMetadataEditorI18nConfig(options = {}) {
|
|
@@ -10925,6 +10929,7 @@ class FieldMetadataEditorComponent {
|
|
|
10925
10929
|
renderedProps = [];
|
|
10926
10930
|
initialSnapshot = null;
|
|
10927
10931
|
initialControlType;
|
|
10932
|
+
initialSeed;
|
|
10928
10933
|
baselineReady = false;
|
|
10929
10934
|
formRebuild$ = new Subject();
|
|
10930
10935
|
hostApplyPatch;
|
|
@@ -11104,17 +11109,86 @@ class FieldMetadataEditorComponent {
|
|
|
11104
11109
|
if (!option?.responseFields?.length) {
|
|
11105
11110
|
continue;
|
|
11106
11111
|
}
|
|
11107
|
-
const
|
|
11108
|
-
|
|
11109
|
-
|
|
11110
|
-
|
|
11111
|
-
|
|
11112
|
-
|
|
11113
|
-
|
|
11114
|
-
|
|
11115
|
-
|
|
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,
|
|
11116
11130
|
description: String(field.description ?? '').trim() || undefined,
|
|
11117
|
-
|
|
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
|
+
}
|
|
11118
11192
|
}
|
|
11119
11193
|
}
|
|
11120
11194
|
}
|
|
@@ -11256,6 +11330,9 @@ class FieldMetadataEditorComponent {
|
|
|
11256
11330
|
if (this.initialControlType === undefined) {
|
|
11257
11331
|
this.initialControlType = ct;
|
|
11258
11332
|
}
|
|
11333
|
+
if (this.initialSeed === undefined) {
|
|
11334
|
+
this.initialSeed = this.cloneSnapshot(this.seed ?? {});
|
|
11335
|
+
}
|
|
11259
11336
|
this.controlType = ct;
|
|
11260
11337
|
let props = this.registry.getProperties(this.controlType);
|
|
11261
11338
|
const controlTypeToken = normalizeControlTypeToken(this.controlType);
|
|
@@ -11999,6 +12076,9 @@ class FieldMetadataEditorComponent {
|
|
|
11999
12076
|
}
|
|
12000
12077
|
}
|
|
12001
12078
|
reset() {
|
|
12079
|
+
if (this.initialSeed !== undefined) {
|
|
12080
|
+
this.seed = this.cloneSnapshot(this.initialSeed);
|
|
12081
|
+
}
|
|
12002
12082
|
if (this.initialControlType !== undefined && this.controlType !== this.initialControlType) {
|
|
12003
12083
|
this.rebuildEditorForType(this.initialControlType);
|
|
12004
12084
|
}
|
|
@@ -12019,6 +12099,79 @@ class FieldMetadataEditorComponent {
|
|
|
12019
12099
|
if (value != null)
|
|
12020
12100
|
this.onControlTypeChange(value);
|
|
12021
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
|
+
}
|
|
12022
12175
|
withSharedFieldMetadataProperties(props) {
|
|
12023
12176
|
const propNames = new Set(props.map((prop) => prop.name));
|
|
12024
12177
|
const sharedProps = [...fieldPresentationProperties, ...fieldSubmitProperties];
|
|
@@ -12323,6 +12476,7 @@ class FieldMetadataEditorComponent {
|
|
|
12323
12476
|
const statusKey = findExact([
|
|
12324
12477
|
'status',
|
|
12325
12478
|
'situacao',
|
|
12479
|
+
'situacaofuncional',
|
|
12326
12480
|
'ativo',
|
|
12327
12481
|
'active',
|
|
12328
12482
|
'estado',
|
|
@@ -12460,6 +12614,20 @@ class FieldMetadataEditorComponent {
|
|
|
12460
12614
|
], ngImport: i0, template: `
|
|
12461
12615
|
@if (form) {
|
|
12462
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
|
+
}
|
|
12463
12631
|
<praxis-dynamic-editor-renderer
|
|
12464
12632
|
[properties]="renderedProps"
|
|
12465
12633
|
[form]="form"
|
|
@@ -12472,7 +12640,7 @@ class FieldMetadataEditorComponent {
|
|
|
12472
12640
|
}
|
|
12473
12641
|
</div>
|
|
12474
12642
|
}
|
|
12475
|
-
`, 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"] }] });
|
|
12476
12644
|
}
|
|
12477
12645
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: FieldMetadataEditorComponent, decorators: [{
|
|
12478
12646
|
type: Component,
|
|
@@ -12481,6 +12649,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
12481
12649
|
standalone: true,
|
|
12482
12650
|
imports: [
|
|
12483
12651
|
ReactiveFormsModule,
|
|
12652
|
+
MatButtonModule,
|
|
12653
|
+
MatIconModule,
|
|
12654
|
+
MatTooltipModule,
|
|
12484
12655
|
DynamicEditorRendererComponent,
|
|
12485
12656
|
ArrayItemSchemaFieldsEditorComponent
|
|
12486
12657
|
],
|
|
@@ -12492,6 +12663,20 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
12492
12663
|
template: `
|
|
12493
12664
|
@if (form) {
|
|
12494
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
|
+
}
|
|
12495
12680
|
<praxis-dynamic-editor-renderer
|
|
12496
12681
|
[properties]="renderedProps"
|
|
12497
12682
|
[form]="form"
|
|
@@ -12677,16 +12862,132 @@ class CascadeRulesService {
|
|
|
12677
12862
|
dependencyLoadOnChange: null,
|
|
12678
12863
|
};
|
|
12679
12864
|
}
|
|
12680
|
-
/**
|
|
12865
|
+
/** Fill missing mappings and value paths without replacing authored values. */
|
|
12681
12866
|
suggest(rule, inferredValueKey) {
|
|
12682
|
-
const map = {};
|
|
12683
|
-
(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
|
+
});
|
|
12684
12872
|
return {
|
|
12685
12873
|
...rule,
|
|
12686
12874
|
dependencyFilterMap: Object.keys(map).length ? map : rule.dependencyFilterMap,
|
|
12687
12875
|
dependencyValuePath: rule.dependencyValuePath || inferredValueKey || 'id',
|
|
12688
12876
|
};
|
|
12689
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
|
+
}
|
|
12690
12991
|
normalizeDependencyFields(value) {
|
|
12691
12992
|
if (Array.isArray(value)) {
|
|
12692
12993
|
const deps = value
|
|
@@ -12715,6 +13016,7 @@ class CascadeManagerTabComponent {
|
|
|
12715
13016
|
connections; // optional signal for conflicts
|
|
12716
13017
|
apply = new EventEmitter();
|
|
12717
13018
|
cancel = new EventEmitter();
|
|
13019
|
+
validChange = new EventEmitter();
|
|
12718
13020
|
searchTerm = '';
|
|
12719
13021
|
searchTimer = null;
|
|
12720
13022
|
searchValue = '';
|
|
@@ -12729,12 +13031,14 @@ class CascadeManagerTabComponent {
|
|
|
12729
13031
|
editLoadOn = 'respectLoadOn';
|
|
12730
13032
|
editDebounce = 150;
|
|
12731
13033
|
editValuePath = '';
|
|
12732
|
-
editFilterMapJson = '';
|
|
12733
13034
|
importMode = false;
|
|
12734
13035
|
importJson = '';
|
|
12735
13036
|
feedbackMessage = '';
|
|
12736
13037
|
feedbackTone = 'info';
|
|
12737
13038
|
editMapRows = [];
|
|
13039
|
+
editInitialFilterMap = {};
|
|
13040
|
+
editInitialValuePath = 'id';
|
|
13041
|
+
lastEmittedValid = null;
|
|
12738
13042
|
setFeedback(message, tone = 'info') {
|
|
12739
13043
|
this.feedbackMessage = message;
|
|
12740
13044
|
this.feedbackTone = tone;
|
|
@@ -12755,20 +13059,17 @@ class CascadeManagerTabComponent {
|
|
|
12755
13059
|
this.selectedFieldSig.set(this.fields[0].name);
|
|
12756
13060
|
this.startEdit(this.fields[0].name);
|
|
12757
13061
|
}
|
|
13062
|
+
this.syncValidityState();
|
|
12758
13063
|
}
|
|
12759
13064
|
ngOnChanges(changes) {
|
|
12760
13065
|
if (changes['fields'] && !changes['fields'].firstChange) {
|
|
12761
|
-
// Re-hydrate rules from incoming fields
|
|
13066
|
+
// Re-hydrate rules from incoming fields; incoming fields own the canonical state
|
|
12762
13067
|
const incoming = this.fields || [];
|
|
12763
13068
|
const next = new Map();
|
|
12764
|
-
// keep existing for fields still present
|
|
12765
13069
|
for (const f of incoming) {
|
|
12766
|
-
const existing = this.rules().get(f.name);
|
|
12767
13070
|
const hydrated = this.svc.hydrateRule(f);
|
|
12768
13071
|
if (hydrated)
|
|
12769
13072
|
next.set(f.name, hydrated);
|
|
12770
|
-
else if (existing)
|
|
12771
|
-
next.set(f.name, existing);
|
|
12772
13073
|
}
|
|
12773
13074
|
this.rules.set(next);
|
|
12774
13075
|
// preserve selection if still valid
|
|
@@ -12776,29 +13077,114 @@ class CascadeManagerTabComponent {
|
|
|
12776
13077
|
if (sel && !incoming.some((f) => f.name === sel)) {
|
|
12777
13078
|
this.selectedFieldSig.set(incoming[0]?.name ?? null);
|
|
12778
13079
|
}
|
|
13080
|
+
const activeSel = this.selectedField();
|
|
13081
|
+
if (activeSel) {
|
|
13082
|
+
this.startEdit(activeSel);
|
|
13083
|
+
}
|
|
13084
|
+
this.syncValidityState();
|
|
12779
13085
|
}
|
|
12780
13086
|
}
|
|
12781
|
-
_logCount = 0;
|
|
12782
13087
|
filteredFields() {
|
|
12783
13088
|
const q = (this.searchValue || '').toLowerCase();
|
|
12784
13089
|
const labelMap = this.svc.getFieldLabelMap(this.fields);
|
|
12785
13090
|
const result = this.fields
|
|
12786
13091
|
.filter((f) => !q || f.name.toLowerCase().includes(q) || (labelMap.get(f.name) || '').toLowerCase().includes(q))
|
|
12787
|
-
.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
|
+
});
|
|
12788
13096
|
return result;
|
|
12789
13097
|
}
|
|
12790
13098
|
labelFor(name) {
|
|
12791
13099
|
return this.svc.getFieldLabelMap(this.fields).get(name) || name;
|
|
12792
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
|
+
}
|
|
12793
13178
|
selectedField() {
|
|
12794
13179
|
return this.selectedFieldSig();
|
|
12795
13180
|
}
|
|
12796
13181
|
selectField(name) {
|
|
12797
13182
|
this.selectedFieldSig.set(name);
|
|
12798
13183
|
this.startEdit(name);
|
|
13184
|
+
this.syncValidityState();
|
|
12799
13185
|
}
|
|
12800
13186
|
hasRule(name) {
|
|
12801
|
-
return this.rules().
|
|
13187
|
+
return (this.rules().get(name)?.dependencyFields?.length ?? 0) > 0;
|
|
12802
13188
|
}
|
|
12803
13189
|
ruleFor(name) {
|
|
12804
13190
|
return this.rules().get(name) || null;
|
|
@@ -12814,6 +13200,52 @@ class CascadeManagerTabComponent {
|
|
|
12814
13200
|
return false;
|
|
12815
13201
|
return this.connections.some((c) => c.toFieldName === name && (c.toInputPath || '').includes('filterCriteria'));
|
|
12816
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
|
+
}
|
|
12817
13249
|
startEdit(name) {
|
|
12818
13250
|
const existing = this.ruleFor(name);
|
|
12819
13251
|
const base = existing || {
|
|
@@ -12836,63 +13268,138 @@ class CascadeManagerTabComponent {
|
|
|
12836
13268
|
this.editLoadOn = suggested.dependencyLoadOnChange || 'respectLoadOn';
|
|
12837
13269
|
this.editDebounce = suggested.dependencyDebounceMs ?? 150;
|
|
12838
13270
|
this.editValuePath = (typeof suggested.dependencyValuePath === 'string' ? suggested.dependencyValuePath : 'id') || '';
|
|
12839
|
-
|
|
12840
|
-
|
|
12841
|
-
|
|
12842
|
-
catch {
|
|
12843
|
-
this.editFilterMapJson = '';
|
|
12844
|
-
}
|
|
13271
|
+
this.editInitialFilterMap = structuredClone(suggested.dependencyFilterMap || {});
|
|
13272
|
+
this.editInitialValuePath = structuredClone(suggested.dependencyValuePath || 'id');
|
|
13273
|
+
this.editMapRows = [];
|
|
12845
13274
|
// Build mapping rows from dependencies
|
|
12846
13275
|
this.buildMapRowsFromDeps();
|
|
12847
13276
|
this.editing.set(true);
|
|
13277
|
+
this.syncValidityState();
|
|
12848
13278
|
}
|
|
12849
13279
|
edit(name) { this.startEdit(name); }
|
|
12850
|
-
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
|
+
}
|
|
12851
13291
|
saveEdit(name) {
|
|
12852
|
-
// Build rule from editor
|
|
13292
|
+
// Build rule from editor (preserving self-reference prevention)
|
|
12853
13293
|
const deps = this.editDepList.filter((s) => !!s && s !== name);
|
|
12854
|
-
|
|
12855
|
-
|
|
12856
|
-
|
|
12857
|
-
|
|
12858
|
-
|
|
12859
|
-
|
|
12860
|
-
if (
|
|
12861
|
-
|
|
12862
|
-
|
|
12863
|
-
|
|
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];
|
|
12864
13304
|
}
|
|
12865
|
-
|
|
12866
|
-
|
|
12867
|
-
|
|
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];
|
|
12868
13334
|
}
|
|
12869
13335
|
}
|
|
12870
|
-
this.setFeedback('');
|
|
12871
13336
|
const rule = {
|
|
12872
13337
|
targetField: name,
|
|
12873
13338
|
dependencyFields: deps,
|
|
12874
13339
|
enableDependencyCascade: this.editEnable,
|
|
12875
13340
|
resetOnDependentChange: this.editReset,
|
|
12876
13341
|
dependencyFilterMap: map,
|
|
12877
|
-
dependencyValuePath:
|
|
13342
|
+
dependencyValuePath: valuePaths,
|
|
12878
13343
|
dependencyMergeStrategy: this.editMerge,
|
|
12879
13344
|
dependencyDebounceMs: this.editDebounce ?? 150,
|
|
12880
13345
|
dependencyLoadOnChange: this.editLoadOn,
|
|
12881
13346
|
};
|
|
12882
13347
|
const next = new Map(this.rules());
|
|
12883
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
|
+
}
|
|
12884
13362
|
this.rules.set(next);
|
|
12885
13363
|
this.emitPatch();
|
|
12886
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
|
+
}
|
|
12887
13388
|
}
|
|
12888
13389
|
remove(name) {
|
|
12889
13390
|
const next = new Map(this.rules());
|
|
12890
13391
|
next.delete(name);
|
|
12891
13392
|
this.rules.set(next);
|
|
13393
|
+
if (this.selectedField() === name) {
|
|
13394
|
+
this.editDepList = [];
|
|
13395
|
+
this.editMapRows = [];
|
|
13396
|
+
}
|
|
12892
13397
|
const patch = {
|
|
12893
13398
|
[name]: this.svc.clearPatch(),
|
|
12894
13399
|
};
|
|
13400
|
+
this.setFeedback('');
|
|
12895
13401
|
this.apply.emit(patch);
|
|
13402
|
+
this.syncValidityState();
|
|
12896
13403
|
}
|
|
12897
13404
|
quickSet(name, action) {
|
|
12898
13405
|
const current = this.ruleFor(name);
|
|
@@ -12907,6 +13414,7 @@ class CascadeManagerTabComponent {
|
|
|
12907
13414
|
map.set(name, next);
|
|
12908
13415
|
this.rules.set(map);
|
|
12909
13416
|
this.emitPatch();
|
|
13417
|
+
this.syncValidityState();
|
|
12910
13418
|
}
|
|
12911
13419
|
emitPatch() {
|
|
12912
13420
|
const out = {};
|
|
@@ -12974,14 +13482,26 @@ class CascadeManagerTabComponent {
|
|
|
12974
13482
|
};
|
|
12975
13483
|
next.set(field, vm);
|
|
12976
13484
|
});
|
|
13485
|
+
const validation = this.svc.validateCascadeGraph(next, this.fields);
|
|
12977
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
|
+
}
|
|
12978
13496
|
this.emitPatch();
|
|
12979
13497
|
this.importMode = false;
|
|
13498
|
+
this.syncValidityState();
|
|
12980
13499
|
}
|
|
12981
13500
|
startTemplate(name, tpl) {
|
|
12982
13501
|
const hasR = this.hasField('rotaId');
|
|
12983
13502
|
const hasD = this.hasField('destinoReqId');
|
|
12984
|
-
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);
|
|
12985
13505
|
this.editDepList = [...deps];
|
|
12986
13506
|
this.editEnable = true;
|
|
12987
13507
|
this.editReset = true;
|
|
@@ -12989,40 +13509,73 @@ class CascadeManagerTabComponent {
|
|
|
12989
13509
|
this.editLoadOn = 'respectLoadOn';
|
|
12990
13510
|
this.editDebounce = 150;
|
|
12991
13511
|
this.editValuePath = 'id';
|
|
13512
|
+
this.editInitialFilterMap = {};
|
|
13513
|
+
this.editInitialValuePath = 'id';
|
|
13514
|
+
this.editMapRows = [];
|
|
12992
13515
|
this.buildMapRowsFromDeps();
|
|
12993
13516
|
this.editing.set(true);
|
|
13517
|
+
this.syncValidityState();
|
|
12994
13518
|
}
|
|
12995
13519
|
buildMapRowsFromDeps() {
|
|
12996
13520
|
const deps = this.editDepList;
|
|
12997
|
-
const
|
|
12998
|
-
return JSON.parse(this.editFilterMapJson || '{}');
|
|
12999
|
-
}
|
|
13000
|
-
catch {
|
|
13001
|
-
return {};
|
|
13002
|
-
} })();
|
|
13521
|
+
const current = new Map(this.editMapRows.map((row) => [row.dep, row]));
|
|
13003
13522
|
this.editMapRows = deps.map((dep) => {
|
|
13004
|
-
const
|
|
13005
|
-
if (
|
|
13006
|
-
return
|
|
13007
|
-
|
|
13008
|
-
|
|
13009
|
-
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) };
|
|
13010
13529
|
});
|
|
13011
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
|
+
}
|
|
13012
13539
|
buildValuePathOverrides(deps) {
|
|
13013
|
-
|
|
13014
|
-
|
|
13015
|
-
|
|
13016
|
-
|
|
13017
|
-
const
|
|
13018
|
-
|
|
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;
|
|
13019
13572
|
}
|
|
13020
13573
|
buildPreview() {
|
|
13021
13574
|
try {
|
|
13022
|
-
const deps = this.editDepList;
|
|
13023
13575
|
const frag = {};
|
|
13024
13576
|
this.editMapRows.forEach((r) => {
|
|
13025
|
-
const
|
|
13577
|
+
const extractedProp = r.valuePath || this.editValuePath || 'id';
|
|
13578
|
+
const example = `<valor de ${r.dep}.${extractedProp}>`;
|
|
13026
13579
|
const parts = (r.key || r.dep).split('.');
|
|
13027
13580
|
let ref = frag;
|
|
13028
13581
|
for (let i = 0; i < parts.length - 1; i++) {
|
|
@@ -13047,10 +13600,14 @@ class CascadeManagerTabComponent {
|
|
|
13047
13600
|
const labelMap = this.svc.getFieldLabelMap(this.fields);
|
|
13048
13601
|
return this.fields
|
|
13049
13602
|
.filter((f) => f.name !== current)
|
|
13050
|
-
.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
|
+
});
|
|
13051
13607
|
}
|
|
13052
13608
|
onDepListChange() {
|
|
13053
13609
|
this.buildMapRowsFromDeps();
|
|
13610
|
+
this.syncValidityState();
|
|
13054
13611
|
}
|
|
13055
13612
|
getKeySuggestions(dep) {
|
|
13056
13613
|
const suggestions = [dep, `${dep}.id`, `${dep}.value`];
|
|
@@ -13070,7 +13627,7 @@ class CascadeManagerTabComponent {
|
|
|
13070
13627
|
return [...new Set(suggestions)];
|
|
13071
13628
|
}
|
|
13072
13629
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: CascadeManagerTabComponent, deps: [{ token: CascadeRulesService }], target: i0.ɵɵFactoryTarget.Component });
|
|
13073
|
-
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: `
|
|
13074
13631
|
<div class="cm-root">
|
|
13075
13632
|
<!-- Left: Fields list -->
|
|
13076
13633
|
<div class="cm-left">
|
|
@@ -13080,14 +13637,29 @@ class CascadeManagerTabComponent {
|
|
|
13080
13637
|
</mat-form-field>
|
|
13081
13638
|
@if (fields.length <= 50) {
|
|
13082
13639
|
<div>
|
|
13083
|
-
@for (f of filteredFields(); track f) {
|
|
13084
|
-
<div
|
|
13085
|
-
|
|
13086
|
-
|
|
13087
|
-
|
|
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>
|
|
13088
13657
|
</div>
|
|
13089
|
-
|
|
13090
|
-
|
|
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)) {
|
|
13091
13663
|
@if (isCascadeEnabled(f.name)) {
|
|
13092
13664
|
<span class="rule-badge b-active">Ativo</span>
|
|
13093
13665
|
}
|
|
@@ -13095,22 +13667,38 @@ class CascadeManagerTabComponent {
|
|
|
13095
13667
|
<span class="rule-badge b-manual">Manual</span>
|
|
13096
13668
|
}
|
|
13097
13669
|
@if (loadOnChange(f.name)==='immediate') {
|
|
13098
|
-
<span class="rule-badge b-immediate">
|
|
13670
|
+
<span class="rule-badge b-immediate">Imediato</span>
|
|
13099
13671
|
}
|
|
13100
|
-
|
|
13101
|
-
|
|
13672
|
+
}
|
|
13673
|
+
</div>
|
|
13102
13674
|
</div>
|
|
13103
13675
|
}
|
|
13104
13676
|
</div>
|
|
13105
13677
|
} @else {
|
|
13106
|
-
<cdk-virtual-scroll-viewport itemSize="
|
|
13107
|
-
<div
|
|
13108
|
-
|
|
13109
|
-
|
|
13110
|
-
|
|
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>
|
|
13111
13696
|
</div>
|
|
13112
|
-
|
|
13113
|
-
|
|
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)) {
|
|
13114
13702
|
@if (isCascadeEnabled(f.name)) {
|
|
13115
13703
|
<span class="rule-badge b-active">Ativo</span>
|
|
13116
13704
|
}
|
|
@@ -13118,27 +13706,27 @@ class CascadeManagerTabComponent {
|
|
|
13118
13706
|
<span class="rule-badge b-manual">Manual</span>
|
|
13119
13707
|
}
|
|
13120
13708
|
@if (loadOnChange(f.name)==='immediate') {
|
|
13121
|
-
<span class="rule-badge b-immediate">
|
|
13709
|
+
<span class="rule-badge b-immediate">Imediato</span>
|
|
13122
13710
|
}
|
|
13123
|
-
|
|
13124
|
-
|
|
13711
|
+
}
|
|
13712
|
+
</div>
|
|
13125
13713
|
</div>
|
|
13126
13714
|
</cdk-virtual-scroll-viewport>
|
|
13127
13715
|
}
|
|
13128
13716
|
</div>
|
|
13129
|
-
|
|
13717
|
+
|
|
13130
13718
|
<!-- Right: Rules for selected field -->
|
|
13131
13719
|
@if (selectedField(); as sel) {
|
|
13132
13720
|
<div class="cm-right">
|
|
13133
13721
|
<div class="cm-header">
|
|
13134
13722
|
<div>
|
|
13135
|
-
<h3 style="margin:0">{{
|
|
13136
|
-
<div style="opacity:.
|
|
13723
|
+
<h3 style="margin:0">{{ disambiguatedLabelFor(sel) }}</h3>
|
|
13724
|
+
<div style="opacity:.75; font-size:0.85rem">{{ sel }} · {{ fieldRoleSummary(sel) }}</div>
|
|
13137
13725
|
</div>
|
|
13138
13726
|
<div>
|
|
13139
13727
|
<button mat-stroked-button color="primary" (click)="exportRules()" style="margin-right:8px">Exportar</button>
|
|
13140
13728
|
<button mat-stroked-button (click)="toggleImport()" style="margin-right:8px">Importar</button>
|
|
13141
|
-
<button mat-flat-button color="primary" [matMenuTriggerFor]="addMenu"
|
|
13729
|
+
<button mat-flat-button color="primary" [matMenuTriggerFor]="addMenu">Modelos</button>
|
|
13142
13730
|
<mat-menu #addMenu="matMenu">
|
|
13143
13731
|
<button mat-menu-item (click)="startEdit(sel)"><mat-icon [praxisIcon]="'add'"></mat-icon><span>Em branco</span></button>
|
|
13144
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>
|
|
@@ -13146,9 +13734,45 @@ class CascadeManagerTabComponent {
|
|
|
13146
13734
|
</mat-menu>
|
|
13147
13735
|
</div>
|
|
13148
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
|
+
|
|
13149
13762
|
@if (feedbackMessage) {
|
|
13150
13763
|
<div class="cm-feedback" [class.cm-feedback--error]="feedbackTone === 'error'" role="status">{{ feedbackMessage }}</div>
|
|
13151
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
|
+
|
|
13152
13776
|
<!-- Conflict banner (connections) -->
|
|
13153
13777
|
@if (hasConflict(sel)) {
|
|
13154
13778
|
<div class="banner">
|
|
@@ -13161,6 +13785,7 @@ class CascadeManagerTabComponent {
|
|
|
13161
13785
|
<button mat-stroked-button color="accent" (click)="quickSet(sel,'manual')">Definir manual</button>
|
|
13162
13786
|
</div>
|
|
13163
13787
|
}
|
|
13788
|
+
|
|
13164
13789
|
@if (importMode) {
|
|
13165
13790
|
<div class="import-panel">
|
|
13166
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>
|
|
@@ -13171,33 +13796,45 @@ class CascadeManagerTabComponent {
|
|
|
13171
13796
|
</div>
|
|
13172
13797
|
</div>
|
|
13173
13798
|
}
|
|
13174
|
-
|
|
13799
|
+
|
|
13800
|
+
@if (!hasRule(sel)) {
|
|
13175
13801
|
<div>
|
|
13176
|
-
<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>
|
|
13177
13806
|
</div>
|
|
13178
13807
|
}
|
|
13179
|
-
@if (ruleFor(sel); as rule) {
|
|
13808
|
+
@if (hasRule(sel) && ruleFor(sel); as rule) {
|
|
13180
13809
|
<div>
|
|
13181
13810
|
<div class="rule-row">
|
|
13182
13811
|
<div>
|
|
13183
|
-
<div><strong>
|
|
13184
|
-
<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>
|
|
13185
13819
|
</div>
|
|
13186
13820
|
<div class="rule-actions">
|
|
13187
|
-
<button mat-icon-button matTooltip="Editar" (click)="startEdit(sel)"><mat-icon [praxisIcon]="'edit'"></mat-icon></button>
|
|
13188
|
-
<button mat-icon-button matTooltip="Duplicar" disabled><mat-icon [praxisIcon]="'content_copy'"></mat-icon></button>
|
|
13189
|
-
<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>
|
|
13190
13824
|
</div>
|
|
13191
13825
|
</div>
|
|
13192
13826
|
</div>
|
|
13193
13827
|
}
|
|
13828
|
+
|
|
13194
13829
|
<!-- Editor com seções (sem acordeão duplo) -->
|
|
13195
13830
|
<div class="panel-surface">
|
|
13196
13831
|
<div class="panel-header" style="padding: 8px 4px; border-bottom: 1px solid var(--pfx-settings-panel-border, var(--md-sys-color-outline-variant));">
|
|
13197
13832
|
<mat-icon [praxisIcon]="'rule'"></mat-icon>
|
|
13198
13833
|
<div>
|
|
13199
13834
|
<div>Cascata do campo</div>
|
|
13200
|
-
<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>
|
|
13201
13838
|
</div>
|
|
13202
13839
|
</div>
|
|
13203
13840
|
<div class="panel-stack">
|
|
@@ -13207,13 +13844,16 @@ class CascadeManagerTabComponent {
|
|
|
13207
13844
|
<mat-panel-title class="panel-header"><mat-icon [praxisIcon]="'tune'"></mat-icon> Básico</mat-panel-title>
|
|
13208
13845
|
</mat-expansion-panel-header>
|
|
13209
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>
|
|
13210
13850
|
<div class="form-row">
|
|
13211
13851
|
<mat-form-field appearance="outline" class="full">
|
|
13212
|
-
<mat-label>
|
|
13852
|
+
<mat-label>Este campo depende de (campos pais)</mat-label>
|
|
13213
13853
|
<mat-select multiple [(ngModel)]="editDepList" (ngModelChange)="onDepListChange()">
|
|
13214
|
-
@for (f of availableDependencyFields(); track f) {
|
|
13854
|
+
@for (f of availableDependencyFields(); track f.name) {
|
|
13215
13855
|
<mat-option [value]="f.name">
|
|
13216
|
-
{{
|
|
13856
|
+
{{ disambiguatedLabelFor(f.name) }} ({{ f.name }}) · {{ fieldRoleBadge(f.name) }}
|
|
13217
13857
|
</mat-option>
|
|
13218
13858
|
}
|
|
13219
13859
|
</mat-select>
|
|
@@ -13222,42 +13862,54 @@ class CascadeManagerTabComponent {
|
|
|
13222
13862
|
matSuffix
|
|
13223
13863
|
class="help-icon-button"
|
|
13224
13864
|
type="button"
|
|
13225
|
-
[matTooltip]="'Selecione os campos
|
|
13865
|
+
[matTooltip]="'Selecione os campos pais (origem) cujo valor filtrará as opções deste campo (destino)'"
|
|
13226
13866
|
matTooltipPosition="above"
|
|
13227
13867
|
>
|
|
13228
13868
|
<mat-icon [praxisIcon]="'help_outline'"></mat-icon>
|
|
13229
13869
|
</button>
|
|
13230
13870
|
</mat-form-field>
|
|
13231
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
|
+
}
|
|
13232
13877
|
<div class="form-row">
|
|
13233
|
-
<mat-checkbox [(ngModel)]="editEnable"
|
|
13234
|
-
|
|
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>
|
|
13235
13884
|
</div>
|
|
13236
13885
|
</div>
|
|
13237
13886
|
</mat-expansion-panel>
|
|
13238
13887
|
<!-- Execução -->
|
|
13239
13888
|
<mat-expansion-panel class="stack-panel">
|
|
13240
13889
|
<mat-expansion-panel-header>
|
|
13241
|
-
<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>
|
|
13242
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>
|
|
13243
13895
|
<div class="form-row">
|
|
13244
13896
|
<mat-form-field appearance="outline">
|
|
13245
|
-
<mat-label>
|
|
13897
|
+
<mat-label>Quando recarregar opções</mat-label>
|
|
13246
13898
|
<mat-select [(ngModel)]="editLoadOn">
|
|
13247
|
-
<mat-option value="respectLoadOn">
|
|
13248
|
-
<mat-option value="immediate">
|
|
13249
|
-
<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>
|
|
13250
13902
|
</mat-select>
|
|
13251
13903
|
</mat-form-field>
|
|
13252
13904
|
<mat-form-field appearance="outline">
|
|
13253
|
-
<mat-label>
|
|
13905
|
+
<mat-label>Como combinar filtros</mat-label>
|
|
13254
13906
|
<mat-select [(ngModel)]="editMerge">
|
|
13255
|
-
<mat-option value="merge">merge
|
|
13256
|
-
<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>
|
|
13257
13909
|
</mat-select>
|
|
13258
13910
|
</mat-form-field>
|
|
13259
13911
|
<mat-form-field appearance="outline">
|
|
13260
|
-
<mat-label>Atraso (ms)</mat-label>
|
|
13912
|
+
<mat-label>Atraso após mudança (ms)</mat-label>
|
|
13261
13913
|
<input matInput type="number" [(ngModel)]="editDebounce">
|
|
13262
13914
|
</mat-form-field>
|
|
13263
13915
|
</div>
|
|
@@ -13265,18 +13917,21 @@ class CascadeManagerTabComponent {
|
|
|
13265
13917
|
<!-- Extração de valor -->
|
|
13266
13918
|
<mat-expansion-panel class="stack-panel">
|
|
13267
13919
|
<mat-expansion-panel-header>
|
|
13268
|
-
<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>
|
|
13269
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>
|
|
13270
13925
|
<div class="form-row">
|
|
13271
13926
|
<mat-form-field appearance="outline" class="full">
|
|
13272
|
-
<mat-label>
|
|
13927
|
+
<mat-label>Propriedade padrão lida do campo pai (ValuePath)</mat-label>
|
|
13273
13928
|
<input matInput [(ngModel)]="editValuePath" placeholder="id">
|
|
13274
13929
|
<button
|
|
13275
13930
|
mat-icon-button
|
|
13276
13931
|
matSuffix
|
|
13277
13932
|
class="help-icon-button"
|
|
13278
13933
|
type="button"
|
|
13279
|
-
[matTooltip]="'Preenchido automaticamente a partir de optionValueKey
|
|
13934
|
+
[matTooltip]="'Preenchido automaticamente a partir de optionValueKey do campo pai ou \\'id\\''"
|
|
13280
13935
|
matTooltipPosition="above"
|
|
13281
13936
|
>
|
|
13282
13937
|
<mat-icon [praxisIcon]="'help_outline'"></mat-icon>
|
|
@@ -13287,24 +13942,30 @@ class CascadeManagerTabComponent {
|
|
|
13287
13942
|
<!-- Mapeamento de filtros (tabela) -->
|
|
13288
13943
|
<mat-expansion-panel class="stack-panel">
|
|
13289
13944
|
<mat-expansion-panel-header>
|
|
13290
|
-
<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>
|
|
13291
13946
|
</mat-expansion-panel-header>
|
|
13292
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>
|
|
13293
13951
|
<table class="map-table">
|
|
13294
13952
|
<thead>
|
|
13295
13953
|
<tr>
|
|
13296
|
-
<th>
|
|
13297
|
-
<th>
|
|
13298
|
-
<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>
|
|
13299
13957
|
</tr>
|
|
13300
13958
|
</thead>
|
|
13301
13959
|
<tbody>
|
|
13302
|
-
@for (row of editMapRows; track row; let i = $index) {
|
|
13960
|
+
@for (row of editMapRows; track row.dep; let i = $index) {
|
|
13303
13961
|
<tr>
|
|
13304
|
-
<td>
|
|
13962
|
+
<td>
|
|
13963
|
+
<div><strong>{{ disambiguatedLabelFor(row.dep) }}</strong></div>
|
|
13964
|
+
<small style="opacity:.75">{{ row.dep }}</small>
|
|
13965
|
+
</td>
|
|
13305
13966
|
<td>
|
|
13306
13967
|
<mat-form-field appearance="outline" class="full">
|
|
13307
|
-
<mat-label>
|
|
13968
|
+
<mat-label>Parâmetro no filtro</mat-label>
|
|
13308
13969
|
<input matInput [(ngModel)]="row.key" [matAutocomplete]="autoKey" [placeholder]="row.dep">
|
|
13309
13970
|
<mat-autocomplete #autoKey="matAutocomplete">
|
|
13310
13971
|
@for (opt of getKeySuggestions(row.dep); track opt) {
|
|
@@ -13316,7 +13977,7 @@ class CascadeManagerTabComponent {
|
|
|
13316
13977
|
matSuffix
|
|
13317
13978
|
class="help-icon-button"
|
|
13318
13979
|
type="button"
|
|
13319
|
-
[matTooltip]="'Use
|
|
13980
|
+
[matTooltip]="'Use notação com ponto (ex.: ' + row.dep + '.id) quando o backend esperar filtro aninhado'"
|
|
13320
13981
|
matTooltipPosition="above"
|
|
13321
13982
|
>
|
|
13322
13983
|
<mat-icon [praxisIcon]="'help_outline'"></mat-icon>
|
|
@@ -13325,7 +13986,7 @@ class CascadeManagerTabComponent {
|
|
|
13325
13986
|
</td>
|
|
13326
13987
|
<td>
|
|
13327
13988
|
<mat-form-field appearance="outline" class="full">
|
|
13328
|
-
<mat-label>ValuePath</mat-label>
|
|
13989
|
+
<mat-label>Propriedade do pai (ValuePath)</mat-label>
|
|
13329
13990
|
<input matInput [(ngModel)]="row.valuePath" [matAutocomplete]="autoValue" [placeholder]="editValuePath||'id'">
|
|
13330
13991
|
<mat-autocomplete #autoValue="matAutocomplete">
|
|
13331
13992
|
@for (opt of getValuePathSuggestions(row.dep); track opt) {
|
|
@@ -13337,7 +13998,7 @@ class CascadeManagerTabComponent {
|
|
|
13337
13998
|
matSuffix
|
|
13338
13999
|
class="help-icon-button"
|
|
13339
14000
|
type="button"
|
|
13340
|
-
[matTooltip]="'Opcional'"
|
|
14001
|
+
[matTooltip]="'Opcional: sobrescreve a propriedade padrão lida apenas para este campo pai'"
|
|
13341
14002
|
matTooltipPosition="above"
|
|
13342
14003
|
>
|
|
13343
14004
|
<mat-icon [praxisIcon]="'help_outline'"></mat-icon>
|
|
@@ -13349,29 +14010,32 @@ class CascadeManagerTabComponent {
|
|
|
13349
14010
|
</tbody>
|
|
13350
14011
|
</table>
|
|
13351
14012
|
} @else {
|
|
13352
|
-
<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>
|
|
13353
14014
|
}
|
|
13354
14015
|
</mat-expansion-panel>
|
|
13355
14016
|
<!-- Preview -->
|
|
13356
14017
|
<mat-expansion-panel class="stack-panel">
|
|
13357
14018
|
<mat-expansion-panel-header>
|
|
13358
|
-
<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>
|
|
13359
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>
|
|
13360
14024
|
<pre class="preview-box">{{ buildPreview() }}</pre>
|
|
13361
14025
|
<div style="display:flex; justify-content:flex-end; padding-top:6px">
|
|
13362
|
-
<button mat-stroked-button (click)="copyPreview()">Copiar</button>
|
|
14026
|
+
<button mat-stroked-button (click)="copyPreview()">Copiar exemplo</button>
|
|
13363
14027
|
</div>
|
|
13364
14028
|
</mat-expansion-panel>
|
|
13365
14029
|
</div>
|
|
13366
14030
|
<div style="display:flex; gap:8px; justify-content:flex-end; padding:8px 12px;">
|
|
13367
|
-
<button mat-stroked-button (click)="cancelEdit()">Cancelar</button>
|
|
13368
|
-
<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>
|
|
13369
14033
|
</div>
|
|
13370
14034
|
</div>
|
|
13371
14035
|
</div>
|
|
13372
14036
|
}
|
|
13373
14037
|
</div>
|
|
13374
|
-
`, 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;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;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)}.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(--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}.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{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)}.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"] }] });
|
|
13375
14039
|
}
|
|
13376
14040
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: CascadeManagerTabComponent, decorators: [{
|
|
13377
14041
|
type: Component,
|
|
@@ -13402,14 +14066,29 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
13402
14066
|
</mat-form-field>
|
|
13403
14067
|
@if (fields.length <= 50) {
|
|
13404
14068
|
<div>
|
|
13405
|
-
@for (f of filteredFields(); track f) {
|
|
13406
|
-
<div
|
|
13407
|
-
|
|
13408
|
-
|
|
13409
|
-
|
|
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>
|
|
13410
14086
|
</div>
|
|
13411
|
-
|
|
13412
|
-
|
|
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)) {
|
|
13413
14092
|
@if (isCascadeEnabled(f.name)) {
|
|
13414
14093
|
<span class="rule-badge b-active">Ativo</span>
|
|
13415
14094
|
}
|
|
@@ -13417,22 +14096,38 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
13417
14096
|
<span class="rule-badge b-manual">Manual</span>
|
|
13418
14097
|
}
|
|
13419
14098
|
@if (loadOnChange(f.name)==='immediate') {
|
|
13420
|
-
<span class="rule-badge b-immediate">
|
|
14099
|
+
<span class="rule-badge b-immediate">Imediato</span>
|
|
13421
14100
|
}
|
|
13422
|
-
|
|
13423
|
-
|
|
14101
|
+
}
|
|
14102
|
+
</div>
|
|
13424
14103
|
</div>
|
|
13425
14104
|
}
|
|
13426
14105
|
</div>
|
|
13427
14106
|
} @else {
|
|
13428
|
-
<cdk-virtual-scroll-viewport itemSize="
|
|
13429
|
-
<div
|
|
13430
|
-
|
|
13431
|
-
|
|
13432
|
-
|
|
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>
|
|
13433
14125
|
</div>
|
|
13434
|
-
|
|
13435
|
-
|
|
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)) {
|
|
13436
14131
|
@if (isCascadeEnabled(f.name)) {
|
|
13437
14132
|
<span class="rule-badge b-active">Ativo</span>
|
|
13438
14133
|
}
|
|
@@ -13440,27 +14135,27 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
13440
14135
|
<span class="rule-badge b-manual">Manual</span>
|
|
13441
14136
|
}
|
|
13442
14137
|
@if (loadOnChange(f.name)==='immediate') {
|
|
13443
|
-
<span class="rule-badge b-immediate">
|
|
14138
|
+
<span class="rule-badge b-immediate">Imediato</span>
|
|
13444
14139
|
}
|
|
13445
|
-
|
|
13446
|
-
|
|
14140
|
+
}
|
|
14141
|
+
</div>
|
|
13447
14142
|
</div>
|
|
13448
14143
|
</cdk-virtual-scroll-viewport>
|
|
13449
14144
|
}
|
|
13450
14145
|
</div>
|
|
13451
|
-
|
|
14146
|
+
|
|
13452
14147
|
<!-- Right: Rules for selected field -->
|
|
13453
14148
|
@if (selectedField(); as sel) {
|
|
13454
14149
|
<div class="cm-right">
|
|
13455
14150
|
<div class="cm-header">
|
|
13456
14151
|
<div>
|
|
13457
|
-
<h3 style="margin:0">{{
|
|
13458
|
-
<div style="opacity:.
|
|
14152
|
+
<h3 style="margin:0">{{ disambiguatedLabelFor(sel) }}</h3>
|
|
14153
|
+
<div style="opacity:.75; font-size:0.85rem">{{ sel }} · {{ fieldRoleSummary(sel) }}</div>
|
|
13459
14154
|
</div>
|
|
13460
14155
|
<div>
|
|
13461
14156
|
<button mat-stroked-button color="primary" (click)="exportRules()" style="margin-right:8px">Exportar</button>
|
|
13462
14157
|
<button mat-stroked-button (click)="toggleImport()" style="margin-right:8px">Importar</button>
|
|
13463
|
-
<button mat-flat-button color="primary" [matMenuTriggerFor]="addMenu"
|
|
14158
|
+
<button mat-flat-button color="primary" [matMenuTriggerFor]="addMenu">Modelos</button>
|
|
13464
14159
|
<mat-menu #addMenu="matMenu">
|
|
13465
14160
|
<button mat-menu-item (click)="startEdit(sel)"><mat-icon [praxisIcon]="'add'"></mat-icon><span>Em branco</span></button>
|
|
13466
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>
|
|
@@ -13468,9 +14163,45 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
13468
14163
|
</mat-menu>
|
|
13469
14164
|
</div>
|
|
13470
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
|
+
|
|
13471
14191
|
@if (feedbackMessage) {
|
|
13472
14192
|
<div class="cm-feedback" [class.cm-feedback--error]="feedbackTone === 'error'" role="status">{{ feedbackMessage }}</div>
|
|
13473
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
|
+
|
|
13474
14205
|
<!-- Conflict banner (connections) -->
|
|
13475
14206
|
@if (hasConflict(sel)) {
|
|
13476
14207
|
<div class="banner">
|
|
@@ -13483,6 +14214,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
13483
14214
|
<button mat-stroked-button color="accent" (click)="quickSet(sel,'manual')">Definir manual</button>
|
|
13484
14215
|
</div>
|
|
13485
14216
|
}
|
|
14217
|
+
|
|
13486
14218
|
@if (importMode) {
|
|
13487
14219
|
<div class="import-panel">
|
|
13488
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>
|
|
@@ -13493,33 +14225,45 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
13493
14225
|
</div>
|
|
13494
14226
|
</div>
|
|
13495
14227
|
}
|
|
13496
|
-
|
|
14228
|
+
|
|
14229
|
+
@if (!hasRule(sel)) {
|
|
13497
14230
|
<div>
|
|
13498
|
-
<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>
|
|
13499
14235
|
</div>
|
|
13500
14236
|
}
|
|
13501
|
-
@if (ruleFor(sel); as rule) {
|
|
14237
|
+
@if (hasRule(sel) && ruleFor(sel); as rule) {
|
|
13502
14238
|
<div>
|
|
13503
14239
|
<div class="rule-row">
|
|
13504
14240
|
<div>
|
|
13505
|
-
<div><strong>
|
|
13506
|
-
<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>
|
|
13507
14248
|
</div>
|
|
13508
14249
|
<div class="rule-actions">
|
|
13509
|
-
<button mat-icon-button matTooltip="Editar" (click)="startEdit(sel)"><mat-icon [praxisIcon]="'edit'"></mat-icon></button>
|
|
13510
|
-
<button mat-icon-button matTooltip="Duplicar" disabled><mat-icon [praxisIcon]="'content_copy'"></mat-icon></button>
|
|
13511
|
-
<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>
|
|
13512
14253
|
</div>
|
|
13513
14254
|
</div>
|
|
13514
14255
|
</div>
|
|
13515
14256
|
}
|
|
14257
|
+
|
|
13516
14258
|
<!-- Editor com seções (sem acordeão duplo) -->
|
|
13517
14259
|
<div class="panel-surface">
|
|
13518
14260
|
<div class="panel-header" style="padding: 8px 4px; border-bottom: 1px solid var(--pfx-settings-panel-border, var(--md-sys-color-outline-variant));">
|
|
13519
14261
|
<mat-icon [praxisIcon]="'rule'"></mat-icon>
|
|
13520
14262
|
<div>
|
|
13521
14263
|
<div>Cascata do campo</div>
|
|
13522
|
-
<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>
|
|
13523
14267
|
</div>
|
|
13524
14268
|
</div>
|
|
13525
14269
|
<div class="panel-stack">
|
|
@@ -13529,13 +14273,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
13529
14273
|
<mat-panel-title class="panel-header"><mat-icon [praxisIcon]="'tune'"></mat-icon> Básico</mat-panel-title>
|
|
13530
14274
|
</mat-expansion-panel-header>
|
|
13531
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>
|
|
13532
14279
|
<div class="form-row">
|
|
13533
14280
|
<mat-form-field appearance="outline" class="full">
|
|
13534
|
-
<mat-label>
|
|
14281
|
+
<mat-label>Este campo depende de (campos pais)</mat-label>
|
|
13535
14282
|
<mat-select multiple [(ngModel)]="editDepList" (ngModelChange)="onDepListChange()">
|
|
13536
|
-
@for (f of availableDependencyFields(); track f) {
|
|
14283
|
+
@for (f of availableDependencyFields(); track f.name) {
|
|
13537
14284
|
<mat-option [value]="f.name">
|
|
13538
|
-
{{
|
|
14285
|
+
{{ disambiguatedLabelFor(f.name) }} ({{ f.name }}) · {{ fieldRoleBadge(f.name) }}
|
|
13539
14286
|
</mat-option>
|
|
13540
14287
|
}
|
|
13541
14288
|
</mat-select>
|
|
@@ -13544,42 +14291,54 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
13544
14291
|
matSuffix
|
|
13545
14292
|
class="help-icon-button"
|
|
13546
14293
|
type="button"
|
|
13547
|
-
[matTooltip]="'Selecione os campos
|
|
14294
|
+
[matTooltip]="'Selecione os campos pais (origem) cujo valor filtrará as opções deste campo (destino)'"
|
|
13548
14295
|
matTooltipPosition="above"
|
|
13549
14296
|
>
|
|
13550
14297
|
<mat-icon [praxisIcon]="'help_outline'"></mat-icon>
|
|
13551
14298
|
</button>
|
|
13552
14299
|
</mat-form-field>
|
|
13553
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
|
+
}
|
|
13554
14306
|
<div class="form-row">
|
|
13555
|
-
<mat-checkbox [(ngModel)]="editEnable"
|
|
13556
|
-
|
|
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>
|
|
13557
14313
|
</div>
|
|
13558
14314
|
</div>
|
|
13559
14315
|
</mat-expansion-panel>
|
|
13560
14316
|
<!-- Execução -->
|
|
13561
14317
|
<mat-expansion-panel class="stack-panel">
|
|
13562
14318
|
<mat-expansion-panel-header>
|
|
13563
|
-
<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>
|
|
13564
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>
|
|
13565
14324
|
<div class="form-row">
|
|
13566
14325
|
<mat-form-field appearance="outline">
|
|
13567
|
-
<mat-label>
|
|
14326
|
+
<mat-label>Quando recarregar opções</mat-label>
|
|
13568
14327
|
<mat-select [(ngModel)]="editLoadOn">
|
|
13569
|
-
<mat-option value="respectLoadOn">
|
|
13570
|
-
<mat-option value="immediate">
|
|
13571
|
-
<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>
|
|
13572
14331
|
</mat-select>
|
|
13573
14332
|
</mat-form-field>
|
|
13574
14333
|
<mat-form-field appearance="outline">
|
|
13575
|
-
<mat-label>
|
|
14334
|
+
<mat-label>Como combinar filtros</mat-label>
|
|
13576
14335
|
<mat-select [(ngModel)]="editMerge">
|
|
13577
|
-
<mat-option value="merge">merge
|
|
13578
|
-
<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>
|
|
13579
14338
|
</mat-select>
|
|
13580
14339
|
</mat-form-field>
|
|
13581
14340
|
<mat-form-field appearance="outline">
|
|
13582
|
-
<mat-label>Atraso (ms)</mat-label>
|
|
14341
|
+
<mat-label>Atraso após mudança (ms)</mat-label>
|
|
13583
14342
|
<input matInput type="number" [(ngModel)]="editDebounce">
|
|
13584
14343
|
</mat-form-field>
|
|
13585
14344
|
</div>
|
|
@@ -13587,18 +14346,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
13587
14346
|
<!-- Extração de valor -->
|
|
13588
14347
|
<mat-expansion-panel class="stack-panel">
|
|
13589
14348
|
<mat-expansion-panel-header>
|
|
13590
|
-
<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>
|
|
13591
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>
|
|
13592
14354
|
<div class="form-row">
|
|
13593
14355
|
<mat-form-field appearance="outline" class="full">
|
|
13594
|
-
<mat-label>
|
|
14356
|
+
<mat-label>Propriedade padrão lida do campo pai (ValuePath)</mat-label>
|
|
13595
14357
|
<input matInput [(ngModel)]="editValuePath" placeholder="id">
|
|
13596
14358
|
<button
|
|
13597
14359
|
mat-icon-button
|
|
13598
14360
|
matSuffix
|
|
13599
14361
|
class="help-icon-button"
|
|
13600
14362
|
type="button"
|
|
13601
|
-
[matTooltip]="'Preenchido automaticamente a partir de optionValueKey
|
|
14363
|
+
[matTooltip]="'Preenchido automaticamente a partir de optionValueKey do campo pai ou \\'id\\''"
|
|
13602
14364
|
matTooltipPosition="above"
|
|
13603
14365
|
>
|
|
13604
14366
|
<mat-icon [praxisIcon]="'help_outline'"></mat-icon>
|
|
@@ -13609,24 +14371,30 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
13609
14371
|
<!-- Mapeamento de filtros (tabela) -->
|
|
13610
14372
|
<mat-expansion-panel class="stack-panel">
|
|
13611
14373
|
<mat-expansion-panel-header>
|
|
13612
|
-
<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>
|
|
13613
14375
|
</mat-expansion-panel-header>
|
|
13614
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>
|
|
13615
14380
|
<table class="map-table">
|
|
13616
14381
|
<thead>
|
|
13617
14382
|
<tr>
|
|
13618
|
-
<th>
|
|
13619
|
-
<th>
|
|
13620
|
-
<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>
|
|
13621
14386
|
</tr>
|
|
13622
14387
|
</thead>
|
|
13623
14388
|
<tbody>
|
|
13624
|
-
@for (row of editMapRows; track row; let i = $index) {
|
|
14389
|
+
@for (row of editMapRows; track row.dep; let i = $index) {
|
|
13625
14390
|
<tr>
|
|
13626
|
-
<td>
|
|
14391
|
+
<td>
|
|
14392
|
+
<div><strong>{{ disambiguatedLabelFor(row.dep) }}</strong></div>
|
|
14393
|
+
<small style="opacity:.75">{{ row.dep }}</small>
|
|
14394
|
+
</td>
|
|
13627
14395
|
<td>
|
|
13628
14396
|
<mat-form-field appearance="outline" class="full">
|
|
13629
|
-
<mat-label>
|
|
14397
|
+
<mat-label>Parâmetro no filtro</mat-label>
|
|
13630
14398
|
<input matInput [(ngModel)]="row.key" [matAutocomplete]="autoKey" [placeholder]="row.dep">
|
|
13631
14399
|
<mat-autocomplete #autoKey="matAutocomplete">
|
|
13632
14400
|
@for (opt of getKeySuggestions(row.dep); track opt) {
|
|
@@ -13638,7 +14406,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
13638
14406
|
matSuffix
|
|
13639
14407
|
class="help-icon-button"
|
|
13640
14408
|
type="button"
|
|
13641
|
-
[matTooltip]="'Use
|
|
14409
|
+
[matTooltip]="'Use notação com ponto (ex.: ' + row.dep + '.id) quando o backend esperar filtro aninhado'"
|
|
13642
14410
|
matTooltipPosition="above"
|
|
13643
14411
|
>
|
|
13644
14412
|
<mat-icon [praxisIcon]="'help_outline'"></mat-icon>
|
|
@@ -13647,7 +14415,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
13647
14415
|
</td>
|
|
13648
14416
|
<td>
|
|
13649
14417
|
<mat-form-field appearance="outline" class="full">
|
|
13650
|
-
<mat-label>ValuePath</mat-label>
|
|
14418
|
+
<mat-label>Propriedade do pai (ValuePath)</mat-label>
|
|
13651
14419
|
<input matInput [(ngModel)]="row.valuePath" [matAutocomplete]="autoValue" [placeholder]="editValuePath||'id'">
|
|
13652
14420
|
<mat-autocomplete #autoValue="matAutocomplete">
|
|
13653
14421
|
@for (opt of getValuePathSuggestions(row.dep); track opt) {
|
|
@@ -13659,7 +14427,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
13659
14427
|
matSuffix
|
|
13660
14428
|
class="help-icon-button"
|
|
13661
14429
|
type="button"
|
|
13662
|
-
[matTooltip]="'Opcional'"
|
|
14430
|
+
[matTooltip]="'Opcional: sobrescreve a propriedade padrão lida apenas para este campo pai'"
|
|
13663
14431
|
matTooltipPosition="above"
|
|
13664
14432
|
>
|
|
13665
14433
|
<mat-icon [praxisIcon]="'help_outline'"></mat-icon>
|
|
@@ -13671,29 +14439,32 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
13671
14439
|
</tbody>
|
|
13672
14440
|
</table>
|
|
13673
14441
|
} @else {
|
|
13674
|
-
<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>
|
|
13675
14443
|
}
|
|
13676
14444
|
</mat-expansion-panel>
|
|
13677
14445
|
<!-- Preview -->
|
|
13678
14446
|
<mat-expansion-panel class="stack-panel">
|
|
13679
14447
|
<mat-expansion-panel-header>
|
|
13680
|
-
<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>
|
|
13681
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>
|
|
13682
14453
|
<pre class="preview-box">{{ buildPreview() }}</pre>
|
|
13683
14454
|
<div style="display:flex; justify-content:flex-end; padding-top:6px">
|
|
13684
|
-
<button mat-stroked-button (click)="copyPreview()">Copiar</button>
|
|
14455
|
+
<button mat-stroked-button (click)="copyPreview()">Copiar exemplo</button>
|
|
13685
14456
|
</div>
|
|
13686
14457
|
</mat-expansion-panel>
|
|
13687
14458
|
</div>
|
|
13688
14459
|
<div style="display:flex; gap:8px; justify-content:flex-end; padding:8px 12px;">
|
|
13689
|
-
<button mat-stroked-button (click)="cancelEdit()">Cancelar</button>
|
|
13690
|
-
<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>
|
|
13691
14462
|
</div>
|
|
13692
14463
|
</div>
|
|
13693
14464
|
</div>
|
|
13694
14465
|
}
|
|
13695
14466
|
</div>
|
|
13696
|
-
`, 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;padding:8px
|
|
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"] }]
|
|
13697
14468
|
}], ctorParameters: () => [{ type: CascadeRulesService }], propDecorators: { fields: [{
|
|
13698
14469
|
type: Input
|
|
13699
14470
|
}], connections: [{
|
|
@@ -13702,6 +14473,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
13702
14473
|
type: Output
|
|
13703
14474
|
}], cancel: [{
|
|
13704
14475
|
type: Output
|
|
14476
|
+
}], validChange: [{
|
|
14477
|
+
type: Output
|
|
13705
14478
|
}] } });
|
|
13706
14479
|
|
|
13707
14480
|
/**
|