@praxisui/metadata-editor 10.0.0-rc.38 → 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.
|
@@ -12875,6 +12875,119 @@ class CascadeRulesService {
|
|
|
12875
12875
|
dependencyValuePath: rule.dependencyValuePath || inferredValueKey || 'id',
|
|
12876
12876
|
};
|
|
12877
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
|
+
}
|
|
12878
12991
|
normalizeDependencyFields(value) {
|
|
12879
12992
|
if (Array.isArray(value)) {
|
|
12880
12993
|
const deps = value
|
|
@@ -12903,6 +13016,7 @@ class CascadeManagerTabComponent {
|
|
|
12903
13016
|
connections; // optional signal for conflicts
|
|
12904
13017
|
apply = new EventEmitter();
|
|
12905
13018
|
cancel = new EventEmitter();
|
|
13019
|
+
validChange = new EventEmitter();
|
|
12906
13020
|
searchTerm = '';
|
|
12907
13021
|
searchTimer = null;
|
|
12908
13022
|
searchValue = '';
|
|
@@ -12924,6 +13038,7 @@ class CascadeManagerTabComponent {
|
|
|
12924
13038
|
editMapRows = [];
|
|
12925
13039
|
editInitialFilterMap = {};
|
|
12926
13040
|
editInitialValuePath = 'id';
|
|
13041
|
+
lastEmittedValid = null;
|
|
12927
13042
|
setFeedback(message, tone = 'info') {
|
|
12928
13043
|
this.feedbackMessage = message;
|
|
12929
13044
|
this.feedbackTone = tone;
|
|
@@ -12944,20 +13059,17 @@ class CascadeManagerTabComponent {
|
|
|
12944
13059
|
this.selectedFieldSig.set(this.fields[0].name);
|
|
12945
13060
|
this.startEdit(this.fields[0].name);
|
|
12946
13061
|
}
|
|
13062
|
+
this.syncValidityState();
|
|
12947
13063
|
}
|
|
12948
13064
|
ngOnChanges(changes) {
|
|
12949
13065
|
if (changes['fields'] && !changes['fields'].firstChange) {
|
|
12950
|
-
// Re-hydrate rules from incoming fields
|
|
13066
|
+
// Re-hydrate rules from incoming fields; incoming fields own the canonical state
|
|
12951
13067
|
const incoming = this.fields || [];
|
|
12952
13068
|
const next = new Map();
|
|
12953
|
-
// keep existing for fields still present
|
|
12954
13069
|
for (const f of incoming) {
|
|
12955
|
-
const existing = this.rules().get(f.name);
|
|
12956
13070
|
const hydrated = this.svc.hydrateRule(f);
|
|
12957
13071
|
if (hydrated)
|
|
12958
13072
|
next.set(f.name, hydrated);
|
|
12959
|
-
else if (existing)
|
|
12960
|
-
next.set(f.name, existing);
|
|
12961
13073
|
}
|
|
12962
13074
|
this.rules.set(next);
|
|
12963
13075
|
// preserve selection if still valid
|
|
@@ -12965,29 +13077,114 @@ class CascadeManagerTabComponent {
|
|
|
12965
13077
|
if (sel && !incoming.some((f) => f.name === sel)) {
|
|
12966
13078
|
this.selectedFieldSig.set(incoming[0]?.name ?? null);
|
|
12967
13079
|
}
|
|
13080
|
+
const activeSel = this.selectedField();
|
|
13081
|
+
if (activeSel) {
|
|
13082
|
+
this.startEdit(activeSel);
|
|
13083
|
+
}
|
|
13084
|
+
this.syncValidityState();
|
|
12968
13085
|
}
|
|
12969
13086
|
}
|
|
12970
|
-
_logCount = 0;
|
|
12971
13087
|
filteredFields() {
|
|
12972
13088
|
const q = (this.searchValue || '').toLowerCase();
|
|
12973
13089
|
const labelMap = this.svc.getFieldLabelMap(this.fields);
|
|
12974
13090
|
const result = this.fields
|
|
12975
13091
|
.filter((f) => !q || f.name.toLowerCase().includes(q) || (labelMap.get(f.name) || '').toLowerCase().includes(q))
|
|
12976
|
-
.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
|
+
});
|
|
12977
13096
|
return result;
|
|
12978
13097
|
}
|
|
12979
13098
|
labelFor(name) {
|
|
12980
13099
|
return this.svc.getFieldLabelMap(this.fields).get(name) || name;
|
|
12981
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
|
+
}
|
|
12982
13178
|
selectedField() {
|
|
12983
13179
|
return this.selectedFieldSig();
|
|
12984
13180
|
}
|
|
12985
13181
|
selectField(name) {
|
|
12986
13182
|
this.selectedFieldSig.set(name);
|
|
12987
13183
|
this.startEdit(name);
|
|
13184
|
+
this.syncValidityState();
|
|
12988
13185
|
}
|
|
12989
13186
|
hasRule(name) {
|
|
12990
|
-
return this.rules().
|
|
13187
|
+
return (this.rules().get(name)?.dependencyFields?.length ?? 0) > 0;
|
|
12991
13188
|
}
|
|
12992
13189
|
ruleFor(name) {
|
|
12993
13190
|
return this.rules().get(name) || null;
|
|
@@ -13003,6 +13200,52 @@ class CascadeManagerTabComponent {
|
|
|
13003
13200
|
return false;
|
|
13004
13201
|
return this.connections.some((c) => c.toFieldName === name && (c.toInputPath || '').includes('filterCriteria'));
|
|
13005
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
|
+
}
|
|
13006
13249
|
startEdit(name) {
|
|
13007
13250
|
const existing = this.ruleFor(name);
|
|
13008
13251
|
const base = existing || {
|
|
@@ -13031,12 +13274,24 @@ class CascadeManagerTabComponent {
|
|
|
13031
13274
|
// Build mapping rows from dependencies
|
|
13032
13275
|
this.buildMapRowsFromDeps();
|
|
13033
13276
|
this.editing.set(true);
|
|
13277
|
+
this.syncValidityState();
|
|
13034
13278
|
}
|
|
13035
13279
|
edit(name) { this.startEdit(name); }
|
|
13036
|
-
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
|
+
}
|
|
13037
13291
|
saveEdit(name) {
|
|
13038
|
-
// Build rule from editor
|
|
13292
|
+
// Build rule from editor (preserving self-reference prevention)
|
|
13039
13293
|
const deps = this.editDepList.filter((s) => !!s && s !== name);
|
|
13294
|
+
this.editDepList = [...deps];
|
|
13040
13295
|
// The rows own the draft. The opening snapshot only preserves entry shape;
|
|
13041
13296
|
// merging the original JSON after the rows would silently undo visual edits.
|
|
13042
13297
|
const map = structuredClone(this.editInitialFilterMap);
|
|
@@ -13078,7 +13333,6 @@ class CascadeManagerTabComponent {
|
|
|
13078
13333
|
delete valuePaths[dep];
|
|
13079
13334
|
}
|
|
13080
13335
|
}
|
|
13081
|
-
this.setFeedback('');
|
|
13082
13336
|
const rule = {
|
|
13083
13337
|
targetField: name,
|
|
13084
13338
|
dependencyFields: deps,
|
|
@@ -13092,18 +13346,60 @@ class CascadeManagerTabComponent {
|
|
|
13092
13346
|
};
|
|
13093
13347
|
const next = new Map(this.rules());
|
|
13094
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
|
+
}
|
|
13095
13362
|
this.rules.set(next);
|
|
13096
13363
|
this.emitPatch();
|
|
13097
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
|
+
}
|
|
13098
13388
|
}
|
|
13099
13389
|
remove(name) {
|
|
13100
13390
|
const next = new Map(this.rules());
|
|
13101
13391
|
next.delete(name);
|
|
13102
13392
|
this.rules.set(next);
|
|
13393
|
+
if (this.selectedField() === name) {
|
|
13394
|
+
this.editDepList = [];
|
|
13395
|
+
this.editMapRows = [];
|
|
13396
|
+
}
|
|
13103
13397
|
const patch = {
|
|
13104
13398
|
[name]: this.svc.clearPatch(),
|
|
13105
13399
|
};
|
|
13400
|
+
this.setFeedback('');
|
|
13106
13401
|
this.apply.emit(patch);
|
|
13402
|
+
this.syncValidityState();
|
|
13107
13403
|
}
|
|
13108
13404
|
quickSet(name, action) {
|
|
13109
13405
|
const current = this.ruleFor(name);
|
|
@@ -13118,6 +13414,7 @@ class CascadeManagerTabComponent {
|
|
|
13118
13414
|
map.set(name, next);
|
|
13119
13415
|
this.rules.set(map);
|
|
13120
13416
|
this.emitPatch();
|
|
13417
|
+
this.syncValidityState();
|
|
13121
13418
|
}
|
|
13122
13419
|
emitPatch() {
|
|
13123
13420
|
const out = {};
|
|
@@ -13185,14 +13482,26 @@ class CascadeManagerTabComponent {
|
|
|
13185
13482
|
};
|
|
13186
13483
|
next.set(field, vm);
|
|
13187
13484
|
});
|
|
13485
|
+
const validation = this.svc.validateCascadeGraph(next, this.fields);
|
|
13188
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
|
+
}
|
|
13189
13496
|
this.emitPatch();
|
|
13190
13497
|
this.importMode = false;
|
|
13498
|
+
this.syncValidityState();
|
|
13191
13499
|
}
|
|
13192
13500
|
startTemplate(name, tpl) {
|
|
13193
13501
|
const hasR = this.hasField('rotaId');
|
|
13194
13502
|
const hasD = this.hasField('destinoReqId');
|
|
13195
|
-
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);
|
|
13196
13505
|
this.editDepList = [...deps];
|
|
13197
13506
|
this.editEnable = true;
|
|
13198
13507
|
this.editReset = true;
|
|
@@ -13205,6 +13514,7 @@ class CascadeManagerTabComponent {
|
|
|
13205
13514
|
this.editMapRows = [];
|
|
13206
13515
|
this.buildMapRowsFromDeps();
|
|
13207
13516
|
this.editing.set(true);
|
|
13517
|
+
this.syncValidityState();
|
|
13208
13518
|
}
|
|
13209
13519
|
buildMapRowsFromDeps() {
|
|
13210
13520
|
const deps = this.editDepList;
|
|
@@ -13262,10 +13572,10 @@ class CascadeManagerTabComponent {
|
|
|
13262
13572
|
}
|
|
13263
13573
|
buildPreview() {
|
|
13264
13574
|
try {
|
|
13265
|
-
const deps = this.editDepList;
|
|
13266
13575
|
const frag = {};
|
|
13267
13576
|
this.editMapRows.forEach((r) => {
|
|
13268
|
-
const
|
|
13577
|
+
const extractedProp = r.valuePath || this.editValuePath || 'id';
|
|
13578
|
+
const example = `<valor de ${r.dep}.${extractedProp}>`;
|
|
13269
13579
|
const parts = (r.key || r.dep).split('.');
|
|
13270
13580
|
let ref = frag;
|
|
13271
13581
|
for (let i = 0; i < parts.length - 1; i++) {
|
|
@@ -13290,10 +13600,14 @@ class CascadeManagerTabComponent {
|
|
|
13290
13600
|
const labelMap = this.svc.getFieldLabelMap(this.fields);
|
|
13291
13601
|
return this.fields
|
|
13292
13602
|
.filter((f) => f.name !== current)
|
|
13293
|
-
.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
|
+
});
|
|
13294
13607
|
}
|
|
13295
13608
|
onDepListChange() {
|
|
13296
13609
|
this.buildMapRowsFromDeps();
|
|
13610
|
+
this.syncValidityState();
|
|
13297
13611
|
}
|
|
13298
13612
|
getKeySuggestions(dep) {
|
|
13299
13613
|
const suggestions = [dep, `${dep}.id`, `${dep}.value`];
|
|
@@ -13313,7 +13627,7 @@ class CascadeManagerTabComponent {
|
|
|
13313
13627
|
return [...new Set(suggestions)];
|
|
13314
13628
|
}
|
|
13315
13629
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: CascadeManagerTabComponent, deps: [{ token: CascadeRulesService }], target: i0.ɵɵFactoryTarget.Component });
|
|
13316
|
-
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: `
|
|
13317
13631
|
<div class="cm-root">
|
|
13318
13632
|
<!-- Left: Fields list -->
|
|
13319
13633
|
<div class="cm-left">
|
|
@@ -13323,14 +13637,29 @@ class CascadeManagerTabComponent {
|
|
|
13323
13637
|
</mat-form-field>
|
|
13324
13638
|
@if (fields.length <= 50) {
|
|
13325
13639
|
<div>
|
|
13326
|
-
@for (f of filteredFields(); track f) {
|
|
13327
|
-
<div
|
|
13328
|
-
|
|
13329
|
-
|
|
13330
|
-
|
|
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>
|
|
13331
13657
|
</div>
|
|
13332
|
-
|
|
13333
|
-
|
|
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)) {
|
|
13334
13663
|
@if (isCascadeEnabled(f.name)) {
|
|
13335
13664
|
<span class="rule-badge b-active">Ativo</span>
|
|
13336
13665
|
}
|
|
@@ -13338,22 +13667,38 @@ class CascadeManagerTabComponent {
|
|
|
13338
13667
|
<span class="rule-badge b-manual">Manual</span>
|
|
13339
13668
|
}
|
|
13340
13669
|
@if (loadOnChange(f.name)==='immediate') {
|
|
13341
|
-
<span class="rule-badge b-immediate">
|
|
13670
|
+
<span class="rule-badge b-immediate">Imediato</span>
|
|
13342
13671
|
}
|
|
13343
|
-
|
|
13344
|
-
|
|
13672
|
+
}
|
|
13673
|
+
</div>
|
|
13345
13674
|
</div>
|
|
13346
13675
|
}
|
|
13347
13676
|
</div>
|
|
13348
13677
|
} @else {
|
|
13349
|
-
<cdk-virtual-scroll-viewport itemSize="
|
|
13350
|
-
<div
|
|
13351
|
-
|
|
13352
|
-
|
|
13353
|
-
|
|
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>
|
|
13354
13696
|
</div>
|
|
13355
|
-
|
|
13356
|
-
|
|
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)) {
|
|
13357
13702
|
@if (isCascadeEnabled(f.name)) {
|
|
13358
13703
|
<span class="rule-badge b-active">Ativo</span>
|
|
13359
13704
|
}
|
|
@@ -13361,27 +13706,27 @@ class CascadeManagerTabComponent {
|
|
|
13361
13706
|
<span class="rule-badge b-manual">Manual</span>
|
|
13362
13707
|
}
|
|
13363
13708
|
@if (loadOnChange(f.name)==='immediate') {
|
|
13364
|
-
<span class="rule-badge b-immediate">
|
|
13709
|
+
<span class="rule-badge b-immediate">Imediato</span>
|
|
13365
13710
|
}
|
|
13366
|
-
|
|
13367
|
-
|
|
13711
|
+
}
|
|
13712
|
+
</div>
|
|
13368
13713
|
</div>
|
|
13369
13714
|
</cdk-virtual-scroll-viewport>
|
|
13370
13715
|
}
|
|
13371
13716
|
</div>
|
|
13372
|
-
|
|
13717
|
+
|
|
13373
13718
|
<!-- Right: Rules for selected field -->
|
|
13374
13719
|
@if (selectedField(); as sel) {
|
|
13375
13720
|
<div class="cm-right">
|
|
13376
13721
|
<div class="cm-header">
|
|
13377
13722
|
<div>
|
|
13378
|
-
<h3 style="margin:0">{{
|
|
13379
|
-
<div style="opacity:.
|
|
13723
|
+
<h3 style="margin:0">{{ disambiguatedLabelFor(sel) }}</h3>
|
|
13724
|
+
<div style="opacity:.75; font-size:0.85rem">{{ sel }} · {{ fieldRoleSummary(sel) }}</div>
|
|
13380
13725
|
</div>
|
|
13381
13726
|
<div>
|
|
13382
13727
|
<button mat-stroked-button color="primary" (click)="exportRules()" style="margin-right:8px">Exportar</button>
|
|
13383
13728
|
<button mat-stroked-button (click)="toggleImport()" style="margin-right:8px">Importar</button>
|
|
13384
|
-
<button mat-flat-button color="primary" [matMenuTriggerFor]="addMenu"
|
|
13729
|
+
<button mat-flat-button color="primary" [matMenuTriggerFor]="addMenu">Modelos</button>
|
|
13385
13730
|
<mat-menu #addMenu="matMenu">
|
|
13386
13731
|
<button mat-menu-item (click)="startEdit(sel)"><mat-icon [praxisIcon]="'add'"></mat-icon><span>Em branco</span></button>
|
|
13387
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>
|
|
@@ -13389,9 +13734,45 @@ class CascadeManagerTabComponent {
|
|
|
13389
13734
|
</mat-menu>
|
|
13390
13735
|
</div>
|
|
13391
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
|
+
|
|
13392
13762
|
@if (feedbackMessage) {
|
|
13393
13763
|
<div class="cm-feedback" [class.cm-feedback--error]="feedbackTone === 'error'" role="status">{{ feedbackMessage }}</div>
|
|
13394
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
|
+
|
|
13395
13776
|
<!-- Conflict banner (connections) -->
|
|
13396
13777
|
@if (hasConflict(sel)) {
|
|
13397
13778
|
<div class="banner">
|
|
@@ -13404,6 +13785,7 @@ class CascadeManagerTabComponent {
|
|
|
13404
13785
|
<button mat-stroked-button color="accent" (click)="quickSet(sel,'manual')">Definir manual</button>
|
|
13405
13786
|
</div>
|
|
13406
13787
|
}
|
|
13788
|
+
|
|
13407
13789
|
@if (importMode) {
|
|
13408
13790
|
<div class="import-panel">
|
|
13409
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>
|
|
@@ -13414,33 +13796,45 @@ class CascadeManagerTabComponent {
|
|
|
13414
13796
|
</div>
|
|
13415
13797
|
</div>
|
|
13416
13798
|
}
|
|
13417
|
-
|
|
13799
|
+
|
|
13800
|
+
@if (!hasRule(sel)) {
|
|
13418
13801
|
<div>
|
|
13419
|
-
<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>
|
|
13420
13806
|
</div>
|
|
13421
13807
|
}
|
|
13422
|
-
@if (ruleFor(sel); as rule) {
|
|
13808
|
+
@if (hasRule(sel) && ruleFor(sel); as rule) {
|
|
13423
13809
|
<div>
|
|
13424
13810
|
<div class="rule-row">
|
|
13425
13811
|
<div>
|
|
13426
|
-
<div><strong>
|
|
13427
|
-
<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>
|
|
13428
13819
|
</div>
|
|
13429
13820
|
<div class="rule-actions">
|
|
13430
|
-
<button mat-icon-button matTooltip="Editar" (click)="startEdit(sel)"><mat-icon [praxisIcon]="'edit'"></mat-icon></button>
|
|
13431
|
-
<button mat-icon-button matTooltip="Duplicar" disabled><mat-icon [praxisIcon]="'content_copy'"></mat-icon></button>
|
|
13432
|
-
<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>
|
|
13433
13824
|
</div>
|
|
13434
13825
|
</div>
|
|
13435
13826
|
</div>
|
|
13436
13827
|
}
|
|
13828
|
+
|
|
13437
13829
|
<!-- Editor com seções (sem acordeão duplo) -->
|
|
13438
13830
|
<div class="panel-surface">
|
|
13439
13831
|
<div class="panel-header" style="padding: 8px 4px; border-bottom: 1px solid var(--pfx-settings-panel-border, var(--md-sys-color-outline-variant));">
|
|
13440
13832
|
<mat-icon [praxisIcon]="'rule'"></mat-icon>
|
|
13441
13833
|
<div>
|
|
13442
13834
|
<div>Cascata do campo</div>
|
|
13443
|
-
<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>
|
|
13444
13838
|
</div>
|
|
13445
13839
|
</div>
|
|
13446
13840
|
<div class="panel-stack">
|
|
@@ -13450,13 +13844,16 @@ class CascadeManagerTabComponent {
|
|
|
13450
13844
|
<mat-panel-title class="panel-header"><mat-icon [praxisIcon]="'tune'"></mat-icon> Básico</mat-panel-title>
|
|
13451
13845
|
</mat-expansion-panel-header>
|
|
13452
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>
|
|
13453
13850
|
<div class="form-row">
|
|
13454
13851
|
<mat-form-field appearance="outline" class="full">
|
|
13455
|
-
<mat-label>
|
|
13852
|
+
<mat-label>Este campo depende de (campos pais)</mat-label>
|
|
13456
13853
|
<mat-select multiple [(ngModel)]="editDepList" (ngModelChange)="onDepListChange()">
|
|
13457
|
-
@for (f of availableDependencyFields(); track f) {
|
|
13854
|
+
@for (f of availableDependencyFields(); track f.name) {
|
|
13458
13855
|
<mat-option [value]="f.name">
|
|
13459
|
-
{{
|
|
13856
|
+
{{ disambiguatedLabelFor(f.name) }} ({{ f.name }}) · {{ fieldRoleBadge(f.name) }}
|
|
13460
13857
|
</mat-option>
|
|
13461
13858
|
}
|
|
13462
13859
|
</mat-select>
|
|
@@ -13465,42 +13862,54 @@ class CascadeManagerTabComponent {
|
|
|
13465
13862
|
matSuffix
|
|
13466
13863
|
class="help-icon-button"
|
|
13467
13864
|
type="button"
|
|
13468
|
-
[matTooltip]="'Selecione os campos
|
|
13865
|
+
[matTooltip]="'Selecione os campos pais (origem) cujo valor filtrará as opções deste campo (destino)'"
|
|
13469
13866
|
matTooltipPosition="above"
|
|
13470
13867
|
>
|
|
13471
13868
|
<mat-icon [praxisIcon]="'help_outline'"></mat-icon>
|
|
13472
13869
|
</button>
|
|
13473
13870
|
</mat-form-field>
|
|
13474
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
|
+
}
|
|
13475
13877
|
<div class="form-row">
|
|
13476
|
-
<mat-checkbox [(ngModel)]="editEnable"
|
|
13477
|
-
|
|
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>
|
|
13478
13884
|
</div>
|
|
13479
13885
|
</div>
|
|
13480
13886
|
</mat-expansion-panel>
|
|
13481
13887
|
<!-- Execução -->
|
|
13482
13888
|
<mat-expansion-panel class="stack-panel">
|
|
13483
13889
|
<mat-expansion-panel-header>
|
|
13484
|
-
<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>
|
|
13485
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>
|
|
13486
13895
|
<div class="form-row">
|
|
13487
13896
|
<mat-form-field appearance="outline">
|
|
13488
|
-
<mat-label>
|
|
13897
|
+
<mat-label>Quando recarregar opções</mat-label>
|
|
13489
13898
|
<mat-select [(ngModel)]="editLoadOn">
|
|
13490
|
-
<mat-option value="respectLoadOn">
|
|
13491
|
-
<mat-option value="immediate">
|
|
13492
|
-
<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>
|
|
13493
13902
|
</mat-select>
|
|
13494
13903
|
</mat-form-field>
|
|
13495
13904
|
<mat-form-field appearance="outline">
|
|
13496
|
-
<mat-label>
|
|
13905
|
+
<mat-label>Como combinar filtros</mat-label>
|
|
13497
13906
|
<mat-select [(ngModel)]="editMerge">
|
|
13498
|
-
<mat-option value="merge">merge
|
|
13499
|
-
<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>
|
|
13500
13909
|
</mat-select>
|
|
13501
13910
|
</mat-form-field>
|
|
13502
13911
|
<mat-form-field appearance="outline">
|
|
13503
|
-
<mat-label>Atraso (ms)</mat-label>
|
|
13912
|
+
<mat-label>Atraso após mudança (ms)</mat-label>
|
|
13504
13913
|
<input matInput type="number" [(ngModel)]="editDebounce">
|
|
13505
13914
|
</mat-form-field>
|
|
13506
13915
|
</div>
|
|
@@ -13508,18 +13917,21 @@ class CascadeManagerTabComponent {
|
|
|
13508
13917
|
<!-- Extração de valor -->
|
|
13509
13918
|
<mat-expansion-panel class="stack-panel">
|
|
13510
13919
|
<mat-expansion-panel-header>
|
|
13511
|
-
<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>
|
|
13512
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>
|
|
13513
13925
|
<div class="form-row">
|
|
13514
13926
|
<mat-form-field appearance="outline" class="full">
|
|
13515
|
-
<mat-label>
|
|
13927
|
+
<mat-label>Propriedade padrão lida do campo pai (ValuePath)</mat-label>
|
|
13516
13928
|
<input matInput [(ngModel)]="editValuePath" placeholder="id">
|
|
13517
13929
|
<button
|
|
13518
13930
|
mat-icon-button
|
|
13519
13931
|
matSuffix
|
|
13520
13932
|
class="help-icon-button"
|
|
13521
13933
|
type="button"
|
|
13522
|
-
[matTooltip]="'Preenchido automaticamente a partir de optionValueKey
|
|
13934
|
+
[matTooltip]="'Preenchido automaticamente a partir de optionValueKey do campo pai ou \\'id\\''"
|
|
13523
13935
|
matTooltipPosition="above"
|
|
13524
13936
|
>
|
|
13525
13937
|
<mat-icon [praxisIcon]="'help_outline'"></mat-icon>
|
|
@@ -13530,24 +13942,30 @@ class CascadeManagerTabComponent {
|
|
|
13530
13942
|
<!-- Mapeamento de filtros (tabela) -->
|
|
13531
13943
|
<mat-expansion-panel class="stack-panel">
|
|
13532
13944
|
<mat-expansion-panel-header>
|
|
13533
|
-
<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>
|
|
13534
13946
|
</mat-expansion-panel-header>
|
|
13535
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>
|
|
13536
13951
|
<table class="map-table">
|
|
13537
13952
|
<thead>
|
|
13538
13953
|
<tr>
|
|
13539
|
-
<th>
|
|
13540
|
-
<th>
|
|
13541
|
-
<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>
|
|
13542
13957
|
</tr>
|
|
13543
13958
|
</thead>
|
|
13544
13959
|
<tbody>
|
|
13545
|
-
@for (row of editMapRows; track row; let i = $index) {
|
|
13960
|
+
@for (row of editMapRows; track row.dep; let i = $index) {
|
|
13546
13961
|
<tr>
|
|
13547
|
-
<td>
|
|
13962
|
+
<td>
|
|
13963
|
+
<div><strong>{{ disambiguatedLabelFor(row.dep) }}</strong></div>
|
|
13964
|
+
<small style="opacity:.75">{{ row.dep }}</small>
|
|
13965
|
+
</td>
|
|
13548
13966
|
<td>
|
|
13549
13967
|
<mat-form-field appearance="outline" class="full">
|
|
13550
|
-
<mat-label>
|
|
13968
|
+
<mat-label>Parâmetro no filtro</mat-label>
|
|
13551
13969
|
<input matInput [(ngModel)]="row.key" [matAutocomplete]="autoKey" [placeholder]="row.dep">
|
|
13552
13970
|
<mat-autocomplete #autoKey="matAutocomplete">
|
|
13553
13971
|
@for (opt of getKeySuggestions(row.dep); track opt) {
|
|
@@ -13559,7 +13977,7 @@ class CascadeManagerTabComponent {
|
|
|
13559
13977
|
matSuffix
|
|
13560
13978
|
class="help-icon-button"
|
|
13561
13979
|
type="button"
|
|
13562
|
-
[matTooltip]="'Use
|
|
13980
|
+
[matTooltip]="'Use notação com ponto (ex.: ' + row.dep + '.id) quando o backend esperar filtro aninhado'"
|
|
13563
13981
|
matTooltipPosition="above"
|
|
13564
13982
|
>
|
|
13565
13983
|
<mat-icon [praxisIcon]="'help_outline'"></mat-icon>
|
|
@@ -13568,7 +13986,7 @@ class CascadeManagerTabComponent {
|
|
|
13568
13986
|
</td>
|
|
13569
13987
|
<td>
|
|
13570
13988
|
<mat-form-field appearance="outline" class="full">
|
|
13571
|
-
<mat-label>ValuePath</mat-label>
|
|
13989
|
+
<mat-label>Propriedade do pai (ValuePath)</mat-label>
|
|
13572
13990
|
<input matInput [(ngModel)]="row.valuePath" [matAutocomplete]="autoValue" [placeholder]="editValuePath||'id'">
|
|
13573
13991
|
<mat-autocomplete #autoValue="matAutocomplete">
|
|
13574
13992
|
@for (opt of getValuePathSuggestions(row.dep); track opt) {
|
|
@@ -13580,7 +13998,7 @@ class CascadeManagerTabComponent {
|
|
|
13580
13998
|
matSuffix
|
|
13581
13999
|
class="help-icon-button"
|
|
13582
14000
|
type="button"
|
|
13583
|
-
[matTooltip]="'Opcional'"
|
|
14001
|
+
[matTooltip]="'Opcional: sobrescreve a propriedade padrão lida apenas para este campo pai'"
|
|
13584
14002
|
matTooltipPosition="above"
|
|
13585
14003
|
>
|
|
13586
14004
|
<mat-icon [praxisIcon]="'help_outline'"></mat-icon>
|
|
@@ -13592,29 +14010,32 @@ class CascadeManagerTabComponent {
|
|
|
13592
14010
|
</tbody>
|
|
13593
14011
|
</table>
|
|
13594
14012
|
} @else {
|
|
13595
|
-
<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>
|
|
13596
14014
|
}
|
|
13597
14015
|
</mat-expansion-panel>
|
|
13598
14016
|
<!-- Preview -->
|
|
13599
14017
|
<mat-expansion-panel class="stack-panel">
|
|
13600
14018
|
<mat-expansion-panel-header>
|
|
13601
|
-
<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>
|
|
13602
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>
|
|
13603
14024
|
<pre class="preview-box">{{ buildPreview() }}</pre>
|
|
13604
14025
|
<div style="display:flex; justify-content:flex-end; padding-top:6px">
|
|
13605
|
-
<button mat-stroked-button (click)="copyPreview()">Copiar</button>
|
|
14026
|
+
<button mat-stroked-button (click)="copyPreview()">Copiar exemplo</button>
|
|
13606
14027
|
</div>
|
|
13607
14028
|
</mat-expansion-panel>
|
|
13608
14029
|
</div>
|
|
13609
14030
|
<div style="display:flex; gap:8px; justify-content:flex-end; padding:8px 12px;">
|
|
13610
|
-
<button mat-stroked-button (click)="cancelEdit()">Cancelar</button>
|
|
13611
|
-
<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>
|
|
13612
14033
|
</div>
|
|
13613
14034
|
</div>
|
|
13614
14035
|
</div>
|
|
13615
14036
|
}
|
|
13616
14037
|
</div>
|
|
13617
|
-
`, 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"] }] });
|
|
13618
14039
|
}
|
|
13619
14040
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: CascadeManagerTabComponent, decorators: [{
|
|
13620
14041
|
type: Component,
|
|
@@ -13645,14 +14066,29 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
13645
14066
|
</mat-form-field>
|
|
13646
14067
|
@if (fields.length <= 50) {
|
|
13647
14068
|
<div>
|
|
13648
|
-
@for (f of filteredFields(); track f) {
|
|
13649
|
-
<div
|
|
13650
|
-
|
|
13651
|
-
|
|
13652
|
-
|
|
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>
|
|
13653
14086
|
</div>
|
|
13654
|
-
|
|
13655
|
-
|
|
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)) {
|
|
13656
14092
|
@if (isCascadeEnabled(f.name)) {
|
|
13657
14093
|
<span class="rule-badge b-active">Ativo</span>
|
|
13658
14094
|
}
|
|
@@ -13660,22 +14096,38 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
13660
14096
|
<span class="rule-badge b-manual">Manual</span>
|
|
13661
14097
|
}
|
|
13662
14098
|
@if (loadOnChange(f.name)==='immediate') {
|
|
13663
|
-
<span class="rule-badge b-immediate">
|
|
14099
|
+
<span class="rule-badge b-immediate">Imediato</span>
|
|
13664
14100
|
}
|
|
13665
|
-
|
|
13666
|
-
|
|
14101
|
+
}
|
|
14102
|
+
</div>
|
|
13667
14103
|
</div>
|
|
13668
14104
|
}
|
|
13669
14105
|
</div>
|
|
13670
14106
|
} @else {
|
|
13671
|
-
<cdk-virtual-scroll-viewport itemSize="
|
|
13672
|
-
<div
|
|
13673
|
-
|
|
13674
|
-
|
|
13675
|
-
|
|
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>
|
|
13676
14125
|
</div>
|
|
13677
|
-
|
|
13678
|
-
|
|
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)) {
|
|
13679
14131
|
@if (isCascadeEnabled(f.name)) {
|
|
13680
14132
|
<span class="rule-badge b-active">Ativo</span>
|
|
13681
14133
|
}
|
|
@@ -13683,27 +14135,27 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
13683
14135
|
<span class="rule-badge b-manual">Manual</span>
|
|
13684
14136
|
}
|
|
13685
14137
|
@if (loadOnChange(f.name)==='immediate') {
|
|
13686
|
-
<span class="rule-badge b-immediate">
|
|
14138
|
+
<span class="rule-badge b-immediate">Imediato</span>
|
|
13687
14139
|
}
|
|
13688
|
-
|
|
13689
|
-
|
|
14140
|
+
}
|
|
14141
|
+
</div>
|
|
13690
14142
|
</div>
|
|
13691
14143
|
</cdk-virtual-scroll-viewport>
|
|
13692
14144
|
}
|
|
13693
14145
|
</div>
|
|
13694
|
-
|
|
14146
|
+
|
|
13695
14147
|
<!-- Right: Rules for selected field -->
|
|
13696
14148
|
@if (selectedField(); as sel) {
|
|
13697
14149
|
<div class="cm-right">
|
|
13698
14150
|
<div class="cm-header">
|
|
13699
14151
|
<div>
|
|
13700
|
-
<h3 style="margin:0">{{
|
|
13701
|
-
<div style="opacity:.
|
|
14152
|
+
<h3 style="margin:0">{{ disambiguatedLabelFor(sel) }}</h3>
|
|
14153
|
+
<div style="opacity:.75; font-size:0.85rem">{{ sel }} · {{ fieldRoleSummary(sel) }}</div>
|
|
13702
14154
|
</div>
|
|
13703
14155
|
<div>
|
|
13704
14156
|
<button mat-stroked-button color="primary" (click)="exportRules()" style="margin-right:8px">Exportar</button>
|
|
13705
14157
|
<button mat-stroked-button (click)="toggleImport()" style="margin-right:8px">Importar</button>
|
|
13706
|
-
<button mat-flat-button color="primary" [matMenuTriggerFor]="addMenu"
|
|
14158
|
+
<button mat-flat-button color="primary" [matMenuTriggerFor]="addMenu">Modelos</button>
|
|
13707
14159
|
<mat-menu #addMenu="matMenu">
|
|
13708
14160
|
<button mat-menu-item (click)="startEdit(sel)"><mat-icon [praxisIcon]="'add'"></mat-icon><span>Em branco</span></button>
|
|
13709
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>
|
|
@@ -13711,9 +14163,45 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
13711
14163
|
</mat-menu>
|
|
13712
14164
|
</div>
|
|
13713
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
|
+
|
|
13714
14191
|
@if (feedbackMessage) {
|
|
13715
14192
|
<div class="cm-feedback" [class.cm-feedback--error]="feedbackTone === 'error'" role="status">{{ feedbackMessage }}</div>
|
|
13716
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
|
+
|
|
13717
14205
|
<!-- Conflict banner (connections) -->
|
|
13718
14206
|
@if (hasConflict(sel)) {
|
|
13719
14207
|
<div class="banner">
|
|
@@ -13726,6 +14214,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
13726
14214
|
<button mat-stroked-button color="accent" (click)="quickSet(sel,'manual')">Definir manual</button>
|
|
13727
14215
|
</div>
|
|
13728
14216
|
}
|
|
14217
|
+
|
|
13729
14218
|
@if (importMode) {
|
|
13730
14219
|
<div class="import-panel">
|
|
13731
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>
|
|
@@ -13736,33 +14225,45 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
13736
14225
|
</div>
|
|
13737
14226
|
</div>
|
|
13738
14227
|
}
|
|
13739
|
-
|
|
14228
|
+
|
|
14229
|
+
@if (!hasRule(sel)) {
|
|
13740
14230
|
<div>
|
|
13741
|
-
<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>
|
|
13742
14235
|
</div>
|
|
13743
14236
|
}
|
|
13744
|
-
@if (ruleFor(sel); as rule) {
|
|
14237
|
+
@if (hasRule(sel) && ruleFor(sel); as rule) {
|
|
13745
14238
|
<div>
|
|
13746
14239
|
<div class="rule-row">
|
|
13747
14240
|
<div>
|
|
13748
|
-
<div><strong>
|
|
13749
|
-
<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>
|
|
13750
14248
|
</div>
|
|
13751
14249
|
<div class="rule-actions">
|
|
13752
|
-
<button mat-icon-button matTooltip="Editar" (click)="startEdit(sel)"><mat-icon [praxisIcon]="'edit'"></mat-icon></button>
|
|
13753
|
-
<button mat-icon-button matTooltip="Duplicar" disabled><mat-icon [praxisIcon]="'content_copy'"></mat-icon></button>
|
|
13754
|
-
<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>
|
|
13755
14253
|
</div>
|
|
13756
14254
|
</div>
|
|
13757
14255
|
</div>
|
|
13758
14256
|
}
|
|
14257
|
+
|
|
13759
14258
|
<!-- Editor com seções (sem acordeão duplo) -->
|
|
13760
14259
|
<div class="panel-surface">
|
|
13761
14260
|
<div class="panel-header" style="padding: 8px 4px; border-bottom: 1px solid var(--pfx-settings-panel-border, var(--md-sys-color-outline-variant));">
|
|
13762
14261
|
<mat-icon [praxisIcon]="'rule'"></mat-icon>
|
|
13763
14262
|
<div>
|
|
13764
14263
|
<div>Cascata do campo</div>
|
|
13765
|
-
<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>
|
|
13766
14267
|
</div>
|
|
13767
14268
|
</div>
|
|
13768
14269
|
<div class="panel-stack">
|
|
@@ -13772,13 +14273,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
13772
14273
|
<mat-panel-title class="panel-header"><mat-icon [praxisIcon]="'tune'"></mat-icon> Básico</mat-panel-title>
|
|
13773
14274
|
</mat-expansion-panel-header>
|
|
13774
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>
|
|
13775
14279
|
<div class="form-row">
|
|
13776
14280
|
<mat-form-field appearance="outline" class="full">
|
|
13777
|
-
<mat-label>
|
|
14281
|
+
<mat-label>Este campo depende de (campos pais)</mat-label>
|
|
13778
14282
|
<mat-select multiple [(ngModel)]="editDepList" (ngModelChange)="onDepListChange()">
|
|
13779
|
-
@for (f of availableDependencyFields(); track f) {
|
|
14283
|
+
@for (f of availableDependencyFields(); track f.name) {
|
|
13780
14284
|
<mat-option [value]="f.name">
|
|
13781
|
-
{{
|
|
14285
|
+
{{ disambiguatedLabelFor(f.name) }} ({{ f.name }}) · {{ fieldRoleBadge(f.name) }}
|
|
13782
14286
|
</mat-option>
|
|
13783
14287
|
}
|
|
13784
14288
|
</mat-select>
|
|
@@ -13787,42 +14291,54 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
13787
14291
|
matSuffix
|
|
13788
14292
|
class="help-icon-button"
|
|
13789
14293
|
type="button"
|
|
13790
|
-
[matTooltip]="'Selecione os campos
|
|
14294
|
+
[matTooltip]="'Selecione os campos pais (origem) cujo valor filtrará as opções deste campo (destino)'"
|
|
13791
14295
|
matTooltipPosition="above"
|
|
13792
14296
|
>
|
|
13793
14297
|
<mat-icon [praxisIcon]="'help_outline'"></mat-icon>
|
|
13794
14298
|
</button>
|
|
13795
14299
|
</mat-form-field>
|
|
13796
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
|
+
}
|
|
13797
14306
|
<div class="form-row">
|
|
13798
|
-
<mat-checkbox [(ngModel)]="editEnable"
|
|
13799
|
-
|
|
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>
|
|
13800
14313
|
</div>
|
|
13801
14314
|
</div>
|
|
13802
14315
|
</mat-expansion-panel>
|
|
13803
14316
|
<!-- Execução -->
|
|
13804
14317
|
<mat-expansion-panel class="stack-panel">
|
|
13805
14318
|
<mat-expansion-panel-header>
|
|
13806
|
-
<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>
|
|
13807
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>
|
|
13808
14324
|
<div class="form-row">
|
|
13809
14325
|
<mat-form-field appearance="outline">
|
|
13810
|
-
<mat-label>
|
|
14326
|
+
<mat-label>Quando recarregar opções</mat-label>
|
|
13811
14327
|
<mat-select [(ngModel)]="editLoadOn">
|
|
13812
|
-
<mat-option value="respectLoadOn">
|
|
13813
|
-
<mat-option value="immediate">
|
|
13814
|
-
<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>
|
|
13815
14331
|
</mat-select>
|
|
13816
14332
|
</mat-form-field>
|
|
13817
14333
|
<mat-form-field appearance="outline">
|
|
13818
|
-
<mat-label>
|
|
14334
|
+
<mat-label>Como combinar filtros</mat-label>
|
|
13819
14335
|
<mat-select [(ngModel)]="editMerge">
|
|
13820
|
-
<mat-option value="merge">merge
|
|
13821
|
-
<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>
|
|
13822
14338
|
</mat-select>
|
|
13823
14339
|
</mat-form-field>
|
|
13824
14340
|
<mat-form-field appearance="outline">
|
|
13825
|
-
<mat-label>Atraso (ms)</mat-label>
|
|
14341
|
+
<mat-label>Atraso após mudança (ms)</mat-label>
|
|
13826
14342
|
<input matInput type="number" [(ngModel)]="editDebounce">
|
|
13827
14343
|
</mat-form-field>
|
|
13828
14344
|
</div>
|
|
@@ -13830,18 +14346,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
13830
14346
|
<!-- Extração de valor -->
|
|
13831
14347
|
<mat-expansion-panel class="stack-panel">
|
|
13832
14348
|
<mat-expansion-panel-header>
|
|
13833
|
-
<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>
|
|
13834
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>
|
|
13835
14354
|
<div class="form-row">
|
|
13836
14355
|
<mat-form-field appearance="outline" class="full">
|
|
13837
|
-
<mat-label>
|
|
14356
|
+
<mat-label>Propriedade padrão lida do campo pai (ValuePath)</mat-label>
|
|
13838
14357
|
<input matInput [(ngModel)]="editValuePath" placeholder="id">
|
|
13839
14358
|
<button
|
|
13840
14359
|
mat-icon-button
|
|
13841
14360
|
matSuffix
|
|
13842
14361
|
class="help-icon-button"
|
|
13843
14362
|
type="button"
|
|
13844
|
-
[matTooltip]="'Preenchido automaticamente a partir de optionValueKey
|
|
14363
|
+
[matTooltip]="'Preenchido automaticamente a partir de optionValueKey do campo pai ou \\'id\\''"
|
|
13845
14364
|
matTooltipPosition="above"
|
|
13846
14365
|
>
|
|
13847
14366
|
<mat-icon [praxisIcon]="'help_outline'"></mat-icon>
|
|
@@ -13852,24 +14371,30 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
13852
14371
|
<!-- Mapeamento de filtros (tabela) -->
|
|
13853
14372
|
<mat-expansion-panel class="stack-panel">
|
|
13854
14373
|
<mat-expansion-panel-header>
|
|
13855
|
-
<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>
|
|
13856
14375
|
</mat-expansion-panel-header>
|
|
13857
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>
|
|
13858
14380
|
<table class="map-table">
|
|
13859
14381
|
<thead>
|
|
13860
14382
|
<tr>
|
|
13861
|
-
<th>
|
|
13862
|
-
<th>
|
|
13863
|
-
<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>
|
|
13864
14386
|
</tr>
|
|
13865
14387
|
</thead>
|
|
13866
14388
|
<tbody>
|
|
13867
|
-
@for (row of editMapRows; track row; let i = $index) {
|
|
14389
|
+
@for (row of editMapRows; track row.dep; let i = $index) {
|
|
13868
14390
|
<tr>
|
|
13869
|
-
<td>
|
|
14391
|
+
<td>
|
|
14392
|
+
<div><strong>{{ disambiguatedLabelFor(row.dep) }}</strong></div>
|
|
14393
|
+
<small style="opacity:.75">{{ row.dep }}</small>
|
|
14394
|
+
</td>
|
|
13870
14395
|
<td>
|
|
13871
14396
|
<mat-form-field appearance="outline" class="full">
|
|
13872
|
-
<mat-label>
|
|
14397
|
+
<mat-label>Parâmetro no filtro</mat-label>
|
|
13873
14398
|
<input matInput [(ngModel)]="row.key" [matAutocomplete]="autoKey" [placeholder]="row.dep">
|
|
13874
14399
|
<mat-autocomplete #autoKey="matAutocomplete">
|
|
13875
14400
|
@for (opt of getKeySuggestions(row.dep); track opt) {
|
|
@@ -13881,7 +14406,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
13881
14406
|
matSuffix
|
|
13882
14407
|
class="help-icon-button"
|
|
13883
14408
|
type="button"
|
|
13884
|
-
[matTooltip]="'Use
|
|
14409
|
+
[matTooltip]="'Use notação com ponto (ex.: ' + row.dep + '.id) quando o backend esperar filtro aninhado'"
|
|
13885
14410
|
matTooltipPosition="above"
|
|
13886
14411
|
>
|
|
13887
14412
|
<mat-icon [praxisIcon]="'help_outline'"></mat-icon>
|
|
@@ -13890,7 +14415,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
13890
14415
|
</td>
|
|
13891
14416
|
<td>
|
|
13892
14417
|
<mat-form-field appearance="outline" class="full">
|
|
13893
|
-
<mat-label>ValuePath</mat-label>
|
|
14418
|
+
<mat-label>Propriedade do pai (ValuePath)</mat-label>
|
|
13894
14419
|
<input matInput [(ngModel)]="row.valuePath" [matAutocomplete]="autoValue" [placeholder]="editValuePath||'id'">
|
|
13895
14420
|
<mat-autocomplete #autoValue="matAutocomplete">
|
|
13896
14421
|
@for (opt of getValuePathSuggestions(row.dep); track opt) {
|
|
@@ -13902,7 +14427,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
13902
14427
|
matSuffix
|
|
13903
14428
|
class="help-icon-button"
|
|
13904
14429
|
type="button"
|
|
13905
|
-
[matTooltip]="'Opcional'"
|
|
14430
|
+
[matTooltip]="'Opcional: sobrescreve a propriedade padrão lida apenas para este campo pai'"
|
|
13906
14431
|
matTooltipPosition="above"
|
|
13907
14432
|
>
|
|
13908
14433
|
<mat-icon [praxisIcon]="'help_outline'"></mat-icon>
|
|
@@ -13914,29 +14439,32 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
13914
14439
|
</tbody>
|
|
13915
14440
|
</table>
|
|
13916
14441
|
} @else {
|
|
13917
|
-
<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>
|
|
13918
14443
|
}
|
|
13919
14444
|
</mat-expansion-panel>
|
|
13920
14445
|
<!-- Preview -->
|
|
13921
14446
|
<mat-expansion-panel class="stack-panel">
|
|
13922
14447
|
<mat-expansion-panel-header>
|
|
13923
|
-
<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>
|
|
13924
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>
|
|
13925
14453
|
<pre class="preview-box">{{ buildPreview() }}</pre>
|
|
13926
14454
|
<div style="display:flex; justify-content:flex-end; padding-top:6px">
|
|
13927
|
-
<button mat-stroked-button (click)="copyPreview()">Copiar</button>
|
|
14455
|
+
<button mat-stroked-button (click)="copyPreview()">Copiar exemplo</button>
|
|
13928
14456
|
</div>
|
|
13929
14457
|
</mat-expansion-panel>
|
|
13930
14458
|
</div>
|
|
13931
14459
|
<div style="display:flex; gap:8px; justify-content:flex-end; padding:8px 12px;">
|
|
13932
|
-
<button mat-stroked-button (click)="cancelEdit()">Cancelar</button>
|
|
13933
|
-
<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>
|
|
13934
14462
|
</div>
|
|
13935
14463
|
</div>
|
|
13936
14464
|
</div>
|
|
13937
14465
|
}
|
|
13938
14466
|
</div>
|
|
13939
|
-
`, 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"] }]
|
|
13940
14468
|
}], ctorParameters: () => [{ type: CascadeRulesService }], propDecorators: { fields: [{
|
|
13941
14469
|
type: Input
|
|
13942
14470
|
}], connections: [{
|
|
@@ -13945,6 +14473,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
13945
14473
|
type: Output
|
|
13946
14474
|
}], cancel: [{
|
|
13947
14475
|
type: Output
|
|
14476
|
+
}], validChange: [{
|
|
14477
|
+
type: Output
|
|
13948
14478
|
}] } });
|
|
13949
14479
|
|
|
13950
14480
|
/**
|