jupiter-dynamic-forms 1.20.3 → 1.20.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/concept-tree.d.ts.map +1 -1
- package/dist/core/dynamic-form.d.ts +11 -1
- package/dist/core/dynamic-form.d.ts.map +1 -1
- package/dist/core/formula-validation-dialog.d.ts +4 -2
- package/dist/core/formula-validation-dialog.d.ts.map +1 -1
- package/dist/index.js +178 -133
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +533 -80
- package/dist/index.mjs.map +1 -1
- package/dist/schema/types.d.ts +7 -0
- package/dist/schema/types.d.ts.map +1 -1
- package/dist/schema/xbrl-types.d.ts +1 -1
- package/dist/schema/xbrl-types.d.ts.map +1 -1
- package/dist/utils/formula-expression-evaluator.d.ts +8 -8
- package/dist/utils/formula-expression-evaluator.d.ts.map +1 -1
- package/dist/utils/formula-precondition-evaluator.d.ts +13 -0
- package/dist/utils/formula-precondition-evaluator.d.ts.map +1 -1
- package/dist/utils/formula-resolution-context.d.ts +50 -4
- package/dist/utils/formula-resolution-context.d.ts.map +1 -1
- package/dist/utils/formula-variable-resolver.d.ts +26 -0
- package/dist/utils/formula-variable-resolver.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -1668,7 +1668,8 @@ const form$1 = {
|
|
|
1668
1668
|
download: "Download",
|
|
1669
1669
|
cancelValidation: "Cancel Validation",
|
|
1670
1670
|
lastValidationResults: "Last Validation Results",
|
|
1671
|
-
validationInProgress: "Validation In Progress"
|
|
1671
|
+
validationInProgress: "Validation In Progress",
|
|
1672
|
+
quickValidate: "Quick Validate"
|
|
1672
1673
|
};
|
|
1673
1674
|
const filter$1 = {
|
|
1674
1675
|
selectRoles: "Select Roles",
|
|
@@ -1873,7 +1874,8 @@ const form = {
|
|
|
1873
1874
|
download: "Downloaden",
|
|
1874
1875
|
cancelValidation: "Validatie annuleren",
|
|
1875
1876
|
lastValidationResults: "Laatste validatieresultaten",
|
|
1876
|
-
validationInProgress: "Validatie bezig"
|
|
1877
|
+
validationInProgress: "Validatie bezig",
|
|
1878
|
+
quickValidate: "Snel valideren"
|
|
1877
1879
|
};
|
|
1878
1880
|
const filter = {
|
|
1879
1881
|
selectRoles: "Rollen selecteren",
|
|
@@ -2491,6 +2493,10 @@ class Parser {
|
|
|
2491
2493
|
this.next();
|
|
2492
2494
|
return { kind: "number", value: Number(token.value) };
|
|
2493
2495
|
}
|
|
2496
|
+
if (token.type === "STRING") {
|
|
2497
|
+
this.next();
|
|
2498
|
+
return { kind: "string", value: token.value };
|
|
2499
|
+
}
|
|
2494
2500
|
if (token.type === "VARIABLE") {
|
|
2495
2501
|
this.next();
|
|
2496
2502
|
return { kind: "variable", name: token.value };
|
|
@@ -2518,20 +2524,30 @@ class Parser {
|
|
|
2518
2524
|
function lookup(bindings, name) {
|
|
2519
2525
|
return bindings.get(name);
|
|
2520
2526
|
}
|
|
2521
|
-
function evalSum(variable, bindings) {
|
|
2527
|
+
function evalSum(variable, bindings, test) {
|
|
2522
2528
|
const binding = lookup(bindings, variable);
|
|
2523
2529
|
if (binding === void 0)
|
|
2524
2530
|
return 0;
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2531
|
+
const values = Array.isArray(binding) ? binding : [binding];
|
|
2532
|
+
return values.reduce((total, value) => {
|
|
2533
|
+
if (typeof value === "string") {
|
|
2534
|
+
throw new FormulaExpressionError(
|
|
2535
|
+
`Variable "$${variable}" resolved to a non-numeric value ("${value}") and cannot be used in sum(...)`,
|
|
2536
|
+
test,
|
|
2537
|
+
"unsupported-syntax"
|
|
2538
|
+
);
|
|
2539
|
+
}
|
|
2540
|
+
return total + value;
|
|
2541
|
+
}, 0);
|
|
2528
2542
|
}
|
|
2529
2543
|
function evalValue(node, bindings, test) {
|
|
2530
2544
|
switch (node.kind) {
|
|
2531
2545
|
case "number":
|
|
2532
2546
|
return node.value;
|
|
2547
|
+
case "string":
|
|
2548
|
+
return node.value;
|
|
2533
2549
|
case "sum":
|
|
2534
|
-
return evalSum(node.variable, bindings);
|
|
2550
|
+
return evalSum(node.variable, bindings, test);
|
|
2535
2551
|
case "variable": {
|
|
2536
2552
|
const binding = lookup(bindings, node.name);
|
|
2537
2553
|
if (binding === void 0) {
|
|
@@ -2551,7 +2567,17 @@ function evalValue(node, bindings, test) {
|
|
|
2551
2567
|
return binding;
|
|
2552
2568
|
}
|
|
2553
2569
|
case "additive":
|
|
2554
|
-
return node.terms.reduce((total, { sign, term }) =>
|
|
2570
|
+
return node.terms.reduce((total, { sign, term }) => {
|
|
2571
|
+
const value = evalValue(term, bindings, test);
|
|
2572
|
+
if (typeof value === "string") {
|
|
2573
|
+
throw new FormulaExpressionError(
|
|
2574
|
+
`Non-numeric value ("${value}") cannot be used in arithmetic (+ / -)`,
|
|
2575
|
+
test,
|
|
2576
|
+
"unsupported-syntax"
|
|
2577
|
+
);
|
|
2578
|
+
}
|
|
2579
|
+
return total + sign * value;
|
|
2580
|
+
}, 0);
|
|
2555
2581
|
}
|
|
2556
2582
|
}
|
|
2557
2583
|
function evalBool(node, bindings, test, fallbackUsage) {
|
|
@@ -2580,6 +2606,9 @@ function evalBool(node, bindings, test, fallbackUsage) {
|
|
|
2580
2606
|
case "comparison": {
|
|
2581
2607
|
const left = evalValue(node.left, bindings, test);
|
|
2582
2608
|
const right = evalValue(node.right, bindings, test);
|
|
2609
|
+
if (typeof left === "string" || typeof right === "string") {
|
|
2610
|
+
return String(left) === String(right);
|
|
2611
|
+
}
|
|
2583
2612
|
return Math.abs(left - right) < CALCULATION_TOLERANCE;
|
|
2584
2613
|
}
|
|
2585
2614
|
}
|
|
@@ -2715,56 +2744,74 @@ function collectAllCandidates(formData) {
|
|
|
2715
2744
|
}
|
|
2716
2745
|
return candidates;
|
|
2717
2746
|
}
|
|
2718
|
-
function indexConceptTree(concept, conceptByQName, conceptById, childrenIndex) {
|
|
2747
|
+
function indexConceptTree(concept, conceptByQName, conceptById, conceptIdByQName, conceptIdByQNameHasChildren, childrenIndex) {
|
|
2719
2748
|
const info = {
|
|
2720
2749
|
conceptId: concept.id,
|
|
2721
|
-
qname: concept.
|
|
2722
|
-
balance: concept.balance
|
|
2750
|
+
qname: concept.name,
|
|
2751
|
+
balance: concept.balance,
|
|
2752
|
+
effectivePeriodRole: concept.effectivePeriodRole
|
|
2723
2753
|
};
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
if (!conceptByQName.has(concept.conceptName))
|
|
2727
|
-
conceptByQName.set(concept.conceptName, info);
|
|
2754
|
+
conceptById.set(concept.id, info);
|
|
2755
|
+
(conceptByQName.get(concept.name) ?? conceptByQName.set(concept.name, []).get(concept.name)).push(info);
|
|
2728
2756
|
const children = concept.children || [];
|
|
2757
|
+
const hasOwnChildren = children.length > 0;
|
|
2758
|
+
const alreadyHasChildren = conceptIdByQNameHasChildren.get(concept.name) === true;
|
|
2759
|
+
if (!conceptIdByQName.has(concept.name) || hasOwnChildren && !alreadyHasChildren) {
|
|
2760
|
+
conceptIdByQName.set(concept.name, concept.id);
|
|
2761
|
+
conceptIdByQNameHasChildren.set(concept.name, hasOwnChildren);
|
|
2762
|
+
}
|
|
2729
2763
|
childrenIndex.set(
|
|
2730
2764
|
concept.id,
|
|
2731
2765
|
children.map((child) => child.id)
|
|
2732
2766
|
);
|
|
2733
|
-
children.forEach((child) => indexConceptTree(child, conceptByQName, conceptById, childrenIndex));
|
|
2767
|
+
children.forEach((child) => indexConceptTree(child, conceptByQName, conceptById, conceptIdByQName, conceptIdByQNameHasChildren, childrenIndex));
|
|
2734
2768
|
}
|
|
2735
|
-
function indexMemberTree(member, memberQNameToId) {
|
|
2769
|
+
function indexMemberTree(member, memberQNameToId, memberChildrenByQName) {
|
|
2736
2770
|
if (!memberQNameToId.has(member.conceptName))
|
|
2737
2771
|
memberQNameToId.set(member.conceptName, member.id);
|
|
2738
|
-
(member.
|
|
2772
|
+
if (!memberChildrenByQName.has(member.conceptName)) {
|
|
2773
|
+
memberChildrenByQName.set(member.conceptName, (member.children || []).map((child) => child.id));
|
|
2774
|
+
}
|
|
2775
|
+
(member.children || []).forEach((child) => indexMemberTree(child, memberQNameToId, memberChildrenByQName));
|
|
2739
2776
|
}
|
|
2740
2777
|
const PRESENTATION_TOTAL_GROUP_ACCESSORS = {
|
|
2741
2778
|
getId: (concept) => concept.id,
|
|
2742
2779
|
getPreferredLabel: (concept) => concept.preferredLabel,
|
|
2743
|
-
isAbstract: (concept) => concept.
|
|
2780
|
+
isAbstract: (concept) => concept.abstract === true,
|
|
2744
2781
|
getChildren: (concept) => concept.children || []
|
|
2745
2782
|
};
|
|
2746
|
-
function buildFormulaResolutionContext(formData, columns,
|
|
2783
|
+
function buildFormulaResolutionContext(formData, columns, sections, hypercubeRoles, periodStartDate, periodEndDate, typedMemberData) {
|
|
2747
2784
|
const conceptByQName = /* @__PURE__ */ new Map();
|
|
2748
2785
|
const conceptById = /* @__PURE__ */ new Map();
|
|
2786
|
+
const conceptIdByQNamePerRole = /* @__PURE__ */ new Map();
|
|
2749
2787
|
const childrenByRole = /* @__PURE__ */ new Map();
|
|
2750
2788
|
const totalGroupChildrenByRole = /* @__PURE__ */ new Map();
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2789
|
+
const columnsByRole = /* @__PURE__ */ new Map();
|
|
2790
|
+
const roleURIByConceptId = /* @__PURE__ */ new Map();
|
|
2791
|
+
sections.forEach((section2) => {
|
|
2754
2792
|
const childrenIndex = /* @__PURE__ */ new Map();
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2793
|
+
const conceptIdByQName = /* @__PURE__ */ new Map();
|
|
2794
|
+
const conceptIdByQNameHasChildren = /* @__PURE__ */ new Map();
|
|
2795
|
+
section2.concepts.forEach((concept) => indexConceptTree(concept, conceptByQName, conceptById, conceptIdByQName, conceptIdByQNameHasChildren, childrenIndex));
|
|
2796
|
+
childrenByRole.set(section2.roleURI, childrenIndex);
|
|
2797
|
+
conceptIdByQNamePerRole.set(section2.roleURI, conceptIdByQName);
|
|
2798
|
+
totalGroupChildrenByRole.set(section2.roleURI, buildTotalGroupChildrenIndex(section2.concepts, PRESENTATION_TOTAL_GROUP_ACCESSORS));
|
|
2799
|
+
for (const conceptId of childrenIndex.keys()) {
|
|
2800
|
+
roleURIByConceptId.set(conceptId, section2.roleURI);
|
|
2801
|
+
}
|
|
2802
|
+
if (section2.columns)
|
|
2803
|
+
columnsByRole.set(section2.roleURI, section2.columns);
|
|
2758
2804
|
});
|
|
2759
2805
|
const dimensionQNameToId = /* @__PURE__ */ new Map();
|
|
2760
2806
|
const memberQNameToId = /* @__PURE__ */ new Map();
|
|
2807
|
+
const memberChildrenByQName = /* @__PURE__ */ new Map();
|
|
2761
2808
|
(hypercubeRoles || []).forEach((role) => {
|
|
2762
2809
|
role.items.forEach((item) => {
|
|
2763
2810
|
item.dimensions.forEach((dimension) => {
|
|
2764
2811
|
if (!dimensionQNameToId.has(dimension.conceptName)) {
|
|
2765
2812
|
dimensionQNameToId.set(dimension.conceptName, dimension.id);
|
|
2766
2813
|
}
|
|
2767
|
-
(dimension.members || []).forEach((member) => indexMemberTree(member, memberQNameToId));
|
|
2814
|
+
(dimension.members || []).forEach((member) => indexMemberTree(member, memberQNameToId, memberChildrenByQName));
|
|
2768
2815
|
});
|
|
2769
2816
|
});
|
|
2770
2817
|
});
|
|
@@ -2775,11 +2822,16 @@ function buildFormulaResolutionContext(formData, columns, presentationRoles, hyp
|
|
|
2775
2822
|
periodEndDate,
|
|
2776
2823
|
conceptByQName,
|
|
2777
2824
|
conceptById,
|
|
2825
|
+
conceptIdByQNamePerRole,
|
|
2778
2826
|
childrenByRole,
|
|
2779
2827
|
totalGroupChildrenByRole,
|
|
2780
2828
|
dimensionQNameToId,
|
|
2781
2829
|
memberQNameToId,
|
|
2782
|
-
|
|
2830
|
+
memberChildrenByQName,
|
|
2831
|
+
columnsByRole,
|
|
2832
|
+
roleURIByConceptId,
|
|
2833
|
+
allCandidates: collectAllCandidates(formData),
|
|
2834
|
+
typedMemberData: typedMemberData ?? {}
|
|
2783
2835
|
};
|
|
2784
2836
|
}
|
|
2785
2837
|
function collectDescendantConceptIds(childrenIndex, rootConceptId) {
|
|
@@ -2800,10 +2852,13 @@ function candidateKey(candidate) {
|
|
|
2800
2852
|
return `${candidate.conceptId}::${candidate.columnId}`;
|
|
2801
2853
|
}
|
|
2802
2854
|
function resolveMatchedCandidates(variable, ctx) {
|
|
2803
|
-
return intersectCandidates(variable.filters.map((filter2) => resolveFilter(filter2, ctx.allCandidates, ctx)));
|
|
2855
|
+
return intersectCandidates(variable.filters.map((filter2) => resolveFilter(filter2, ctx.allCandidates, ctx, variable.filters)));
|
|
2804
2856
|
}
|
|
2805
|
-
function getColumn(ctx,
|
|
2806
|
-
|
|
2857
|
+
function getColumn(ctx, candidate) {
|
|
2858
|
+
var _a;
|
|
2859
|
+
const roleURI = ctx.roleURIByConceptId.get(candidate.conceptId);
|
|
2860
|
+
const roleColumn = roleURI ? (_a = ctx.columnsByRole.get(roleURI)) == null ? void 0 : _a.find((column2) => column2.id === candidate.columnId) : void 0;
|
|
2861
|
+
return roleColumn ?? ctx.columns.find((column2) => column2.id === candidate.columnId);
|
|
2807
2862
|
}
|
|
2808
2863
|
function columnMatchesExplicitDimension(column2, axisId, memberId) {
|
|
2809
2864
|
if (!(column2 == null ? void 0 : column2.dimensionData))
|
|
@@ -2815,13 +2870,22 @@ function columnMatchesExplicitDimension(column2, axisId, memberId) {
|
|
|
2815
2870
|
(combo) => combo.axisId === axisId && (memberId === void 0 || combo.memberId === memberId)
|
|
2816
2871
|
);
|
|
2817
2872
|
}
|
|
2818
|
-
function
|
|
2873
|
+
function columnDeclaresTypedDimension(column2, axisId) {
|
|
2819
2874
|
if (!(column2 == null ? void 0 : column2.dimensionData))
|
|
2820
2875
|
return false;
|
|
2821
2876
|
if (column2.dimensionData.typedMemberId !== void 0 && column2.dimensionData.axisId === axisId)
|
|
2822
2877
|
return true;
|
|
2823
2878
|
return (column2.dimensionData.typedMembers || []).some((typedMember) => typedMember.axisId === axisId);
|
|
2824
2879
|
}
|
|
2880
|
+
function columnMatchesTypedDimension(ctx, candidate, axisId) {
|
|
2881
|
+
const column2 = getColumn(ctx, candidate);
|
|
2882
|
+
if (!columnDeclaresTypedDimension(column2, axisId))
|
|
2883
|
+
return false;
|
|
2884
|
+
const chosenValues = ctx.typedMemberData[candidate.columnId];
|
|
2885
|
+
if (chosenValues === void 0)
|
|
2886
|
+
return true;
|
|
2887
|
+
return !!chosenValues[axisId];
|
|
2888
|
+
}
|
|
2825
2889
|
function paramTokenName(rawDate) {
|
|
2826
2890
|
if (!rawDate)
|
|
2827
2891
|
return void 0;
|
|
@@ -2866,15 +2930,37 @@ function unionCandidates(lists) {
|
|
|
2866
2930
|
lists.forEach((list) => list.forEach((candidate) => merged.set(candidateKey(candidate), candidate)));
|
|
2867
2931
|
return Array.from(merged.values());
|
|
2868
2932
|
}
|
|
2869
|
-
function
|
|
2870
|
-
var _a;
|
|
2933
|
+
function resolveChildMemberAxisFilter(memberQName, axisId, universe, ctx, siblingFilters) {
|
|
2934
|
+
var _a, _b;
|
|
2935
|
+
const childMemberIds = ctx.memberChildrenByQName.get(memberQName) || [];
|
|
2936
|
+
if (childMemberIds.length > 0) {
|
|
2937
|
+
const matches = universe.filter(
|
|
2938
|
+
(candidate) => childMemberIds.some((childId) => columnMatchesExplicitDimension(getColumn(ctx, candidate), axisId, childId))
|
|
2939
|
+
);
|
|
2940
|
+
if (matches.length > 0)
|
|
2941
|
+
return matches;
|
|
2942
|
+
}
|
|
2943
|
+
const conceptRelationFilter = siblingFilters.find(
|
|
2944
|
+
(sibling) => sibling.type === "CONCEPT_RELATION" && sibling.attributes["axis"] === "descendant"
|
|
2945
|
+
);
|
|
2946
|
+
const linkrole = conceptRelationFilter == null ? void 0 : conceptRelationFilter.attributes["linkrole"];
|
|
2947
|
+
const anchorQName = conceptRelationFilter == null ? void 0 : conceptRelationFilter.attributes["qname"];
|
|
2948
|
+
const anchorConceptId = anchorQName && linkrole ? (_a = ctx.conceptIdByQNamePerRole.get(linkrole)) == null ? void 0 : _a.get(anchorQName) : void 0;
|
|
2949
|
+
const directChildren = new Set(anchorConceptId && linkrole ? ((_b = ctx.childrenByRole.get(linkrole)) == null ? void 0 : _b.get(anchorConceptId)) || [] : []);
|
|
2950
|
+
if (directChildren.size === 0)
|
|
2951
|
+
return [];
|
|
2952
|
+
return universe.filter((candidate) => !directChildren.has(candidate.conceptId));
|
|
2953
|
+
}
|
|
2954
|
+
function matchFilter(filter2, universe, ctx, siblingFilters) {
|
|
2955
|
+
var _a, _b;
|
|
2871
2956
|
switch (filter2.type) {
|
|
2872
2957
|
case "CONCEPT_NAME": {
|
|
2873
2958
|
const qname = filter2.attributes["concept.qname"];
|
|
2874
|
-
const
|
|
2875
|
-
if (!
|
|
2959
|
+
const infos = qname ? ctx.conceptByQName.get(qname) : void 0;
|
|
2960
|
+
if (!infos || infos.length === 0)
|
|
2876
2961
|
return [];
|
|
2877
|
-
|
|
2962
|
+
const conceptIds = new Set(infos.map((info) => info.conceptId));
|
|
2963
|
+
return universe.filter((candidate) => conceptIds.has(candidate.conceptId));
|
|
2878
2964
|
}
|
|
2879
2965
|
case "CONCEPT_BALANCE": {
|
|
2880
2966
|
const balance = filter2.attributes["balance"];
|
|
@@ -2887,16 +2973,16 @@ function matchFilter(filter2, universe, ctx) {
|
|
|
2887
2973
|
const anchorQName = filter2.attributes["qname"];
|
|
2888
2974
|
const linkrole = filter2.attributes["linkrole"];
|
|
2889
2975
|
const axis = filter2.attributes["axis"];
|
|
2890
|
-
const
|
|
2976
|
+
const anchorConceptId = anchorQName && linkrole ? (_a = ctx.conceptIdByQNamePerRole.get(linkrole)) == null ? void 0 : _a.get(anchorQName) : void 0;
|
|
2891
2977
|
const childrenIndex = linkrole ? ctx.childrenByRole.get(linkrole) : void 0;
|
|
2892
|
-
if (!
|
|
2978
|
+
if (!anchorConceptId || !childrenIndex)
|
|
2893
2979
|
return [];
|
|
2894
2980
|
let relatedConceptIds;
|
|
2895
2981
|
if (axis === "descendant") {
|
|
2896
|
-
relatedConceptIds = collectDescendantConceptIds(childrenIndex,
|
|
2982
|
+
relatedConceptIds = collectDescendantConceptIds(childrenIndex, anchorConceptId);
|
|
2897
2983
|
} else {
|
|
2898
|
-
const nestedChildren = childrenIndex.get(
|
|
2899
|
-
relatedConceptIds = nestedChildren.length > 0 ? nestedChildren : ((
|
|
2984
|
+
const nestedChildren = childrenIndex.get(anchorConceptId) || [];
|
|
2985
|
+
relatedConceptIds = nestedChildren.length > 0 ? nestedChildren : ((_b = ctx.totalGroupChildrenByRole.get(linkrole ?? "")) == null ? void 0 : _b.get(anchorConceptId)) || [];
|
|
2900
2986
|
}
|
|
2901
2987
|
const relatedSet = new Set(relatedConceptIds);
|
|
2902
2988
|
return universe.filter((candidate) => relatedSet.has(candidate.conceptId));
|
|
@@ -2905,31 +2991,38 @@ function matchFilter(filter2, universe, ctx) {
|
|
|
2905
2991
|
const dimensionQName = filter2.attributes["dimension.qname"];
|
|
2906
2992
|
const memberQName = filter2.attributes["member.qname"];
|
|
2907
2993
|
const axisId = dimensionQName ? ctx.dimensionQNameToId.get(dimensionQName) : void 0;
|
|
2908
|
-
const memberId = memberQName ? ctx.memberQNameToId.get(memberQName) : void 0;
|
|
2909
2994
|
if (!axisId)
|
|
2910
2995
|
return [];
|
|
2911
|
-
|
|
2996
|
+
if (memberQName && filter2.attributes["member.axis"] === "child") {
|
|
2997
|
+
return resolveChildMemberAxisFilter(memberQName, axisId, universe, ctx, siblingFilters);
|
|
2998
|
+
}
|
|
2999
|
+
const memberId = memberQName ? ctx.memberQNameToId.get(memberQName) : void 0;
|
|
3000
|
+
return universe.filter((candidate) => columnMatchesExplicitDimension(getColumn(ctx, candidate), axisId, memberId));
|
|
2912
3001
|
}
|
|
2913
3002
|
case "TYPED_DIMENSION": {
|
|
2914
3003
|
const dimensionQName = filter2.attributes["dimension.qname"];
|
|
2915
3004
|
const axisId = dimensionQName ? ctx.dimensionQNameToId.get(dimensionQName) : void 0;
|
|
2916
3005
|
if (!axisId)
|
|
2917
3006
|
return [];
|
|
2918
|
-
return universe.filter((candidate) => columnMatchesTypedDimension(
|
|
3007
|
+
return universe.filter((candidate) => columnMatchesTypedDimension(ctx, candidate, axisId));
|
|
2919
3008
|
}
|
|
2920
3009
|
case "PERIOD_INSTANT":
|
|
2921
3010
|
case "PERIOD_END": {
|
|
2922
3011
|
const dateAttribute = filter2.attributes["date"];
|
|
2923
|
-
return universe.filter((candidate) => columnMatchesPeriod(getColumn(ctx, candidate
|
|
3012
|
+
return universe.filter((candidate) => columnMatchesPeriod(getColumn(ctx, candidate), dateAttribute, ctx));
|
|
2924
3013
|
}
|
|
2925
3014
|
case "OR_FILTER":
|
|
2926
|
-
return unionCandidates(filter2.children.map((child) => resolveFilter(child, universe, ctx)));
|
|
3015
|
+
return unionCandidates(filter2.children.map((child) => resolveFilter(child, universe, ctx, siblingFilters)));
|
|
3016
|
+
case "ASPECT_COVER":
|
|
3017
|
+
return universe;
|
|
3018
|
+
case "UNKNOWN":
|
|
3019
|
+
return universe;
|
|
2927
3020
|
default:
|
|
2928
3021
|
return [];
|
|
2929
3022
|
}
|
|
2930
3023
|
}
|
|
2931
|
-
function resolveFilter(filter2, universe, ctx) {
|
|
2932
|
-
const matched = matchFilter(filter2, universe, ctx);
|
|
3024
|
+
function resolveFilter(filter2, universe, ctx, siblingFilters) {
|
|
3025
|
+
const matched = matchFilter(filter2, universe, ctx, siblingFilters);
|
|
2933
3026
|
if (!filter2.complement)
|
|
2934
3027
|
return matched;
|
|
2935
3028
|
const matchedKeys = new Set(matched.map(candidateKey));
|
|
@@ -2943,12 +3036,63 @@ function intersectCandidates(sets) {
|
|
|
2943
3036
|
return acc.filter((candidate) => setKeys.has(candidateKey(candidate)));
|
|
2944
3037
|
});
|
|
2945
3038
|
}
|
|
3039
|
+
function resolveRawValue(raw) {
|
|
3040
|
+
if (raw === void 0 || raw === null || raw === "")
|
|
3041
|
+
return void 0;
|
|
3042
|
+
const numeric = parseFloat(String(raw));
|
|
3043
|
+
return Number.isNaN(numeric) ? String(raw) : numeric;
|
|
3044
|
+
}
|
|
3045
|
+
function hasExplicitPeriodFilter(filters) {
|
|
3046
|
+
return filters.some((filter2) => {
|
|
3047
|
+
if (filter2.type === "PERIOD_INSTANT" || filter2.type === "PERIOD_END")
|
|
3048
|
+
return true;
|
|
3049
|
+
if (filter2.type === "OR_FILTER")
|
|
3050
|
+
return hasExplicitPeriodFilter(filter2.children);
|
|
3051
|
+
return false;
|
|
3052
|
+
});
|
|
3053
|
+
}
|
|
3054
|
+
function narrowToCurrentPeriod(candidates, variable, ctx) {
|
|
3055
|
+
if (candidates.length <= 1 || hasExplicitPeriodFilter(variable.filters))
|
|
3056
|
+
return candidates;
|
|
3057
|
+
const currentPeriodOnly = candidates.filter(
|
|
3058
|
+
(candidate) => columnEffectivePeriod(getColumn(ctx, candidate), ctx).end === ctx.periodEndDate
|
|
3059
|
+
);
|
|
3060
|
+
return currentPeriodOnly.length > 0 ? currentPeriodOnly : candidates;
|
|
3061
|
+
}
|
|
3062
|
+
function narrowByPreferredOccurrence(candidates, ctx) {
|
|
3063
|
+
const groups = /* @__PURE__ */ new Map();
|
|
3064
|
+
candidates.forEach((candidate) => {
|
|
3065
|
+
var _a;
|
|
3066
|
+
const qname = ((_a = ctx.conceptById.get(candidate.conceptId)) == null ? void 0 : _a.qname) ?? candidate.conceptId;
|
|
3067
|
+
const key = `${candidate.columnId}::${qname}`;
|
|
3068
|
+
const group = groups.get(key);
|
|
3069
|
+
if (group)
|
|
3070
|
+
group.push(candidate);
|
|
3071
|
+
else
|
|
3072
|
+
groups.set(key, [candidate]);
|
|
3073
|
+
});
|
|
3074
|
+
const result = [];
|
|
3075
|
+
groups.forEach((group) => {
|
|
3076
|
+
if (group.length <= 1) {
|
|
3077
|
+
result.push(...group);
|
|
3078
|
+
return;
|
|
3079
|
+
}
|
|
3080
|
+
const roles = group.map((candidate) => {
|
|
3081
|
+
var _a;
|
|
3082
|
+
return (_a = ctx.conceptById.get(candidate.conceptId)) == null ? void 0 : _a.effectivePeriodRole;
|
|
3083
|
+
});
|
|
3084
|
+
const endLabelOnly = group.filter((_candidate, index) => roles[index] === "periodEndLabel");
|
|
3085
|
+
const isAmbiguous = new Set(roles).size > 1;
|
|
3086
|
+
result.push(...isAmbiguous && endLabelOnly.length > 0 ? endLabelOnly : group);
|
|
3087
|
+
});
|
|
3088
|
+
return result;
|
|
3089
|
+
}
|
|
2946
3090
|
function resolveFormulaVariable(variable, ctx) {
|
|
2947
|
-
const matched = resolveMatchedCandidates(variable, ctx);
|
|
3091
|
+
const matched = narrowByPreferredOccurrence(narrowToCurrentPeriod(resolveMatchedCandidates(variable, ctx), variable, ctx), ctx);
|
|
2948
3092
|
const values = matched.map((candidate) => {
|
|
2949
3093
|
var _a;
|
|
2950
|
-
return
|
|
2951
|
-
}).filter((value) =>
|
|
3094
|
+
return resolveRawValue((_a = ctx.formData[candidate.conceptId]) == null ? void 0 : _a[candidate.columnId]);
|
|
3095
|
+
}).filter((value) => value !== void 0);
|
|
2952
3096
|
if (variable.bindAsSequence) {
|
|
2953
3097
|
if (values.length === 0 && variable.fallbackValue === "()")
|
|
2954
3098
|
return [];
|
|
@@ -2968,6 +3112,15 @@ function resolveFormulaVariable(variable, ctx) {
|
|
|
2968
3112
|
function resolveFormulaVariableMatchCount(variable, ctx) {
|
|
2969
3113
|
return resolveMatchedCandidates(variable, ctx).length;
|
|
2970
3114
|
}
|
|
3115
|
+
function resolveFormulaVariableSetMatchCount(variables, ctx) {
|
|
3116
|
+
const merged = /* @__PURE__ */ new Map();
|
|
3117
|
+
for (const variable of variables) {
|
|
3118
|
+
for (const candidate of resolveMatchedCandidates(variable, ctx)) {
|
|
3119
|
+
merged.set(candidateKey(candidate), candidate);
|
|
3120
|
+
}
|
|
3121
|
+
}
|
|
3122
|
+
return merged.size;
|
|
3123
|
+
}
|
|
2971
3124
|
function resolveFormulaVariableUsedFallback(variable, ctx) {
|
|
2972
3125
|
const matchCount = resolveFormulaVariableMatchCount(variable, ctx);
|
|
2973
3126
|
if (matchCount > 0)
|
|
@@ -2976,6 +3129,27 @@ function resolveFormulaVariableUsedFallback(variable, ctx) {
|
|
|
2976
3129
|
return true;
|
|
2977
3130
|
return void 0;
|
|
2978
3131
|
}
|
|
3132
|
+
function collectConceptNameQNames(filters) {
|
|
3133
|
+
const qnames = [];
|
|
3134
|
+
for (const filter2 of filters) {
|
|
3135
|
+
if (filter2.type === "CONCEPT_NAME") {
|
|
3136
|
+
const qname = filter2.attributes["concept.qname"];
|
|
3137
|
+
if (qname)
|
|
3138
|
+
qnames.push(qname);
|
|
3139
|
+
} else if (filter2.type === "OR_FILTER") {
|
|
3140
|
+
qnames.push(...collectConceptNameQNames(filter2.children));
|
|
3141
|
+
}
|
|
3142
|
+
}
|
|
3143
|
+
return qnames;
|
|
3144
|
+
}
|
|
3145
|
+
function resolveVariableConceptQNames(variable) {
|
|
3146
|
+
return collectConceptNameQNames(variable.filters);
|
|
3147
|
+
}
|
|
3148
|
+
function resolveVariableColumnsForConcept(variable, targetConceptId, ctx) {
|
|
3149
|
+
const universe = ctx.columns.map((column2) => ({ conceptId: targetConceptId, columnId: column2.id }));
|
|
3150
|
+
const resolved = intersectCandidates(variable.filters.map((filter2) => resolveFilter(filter2, universe, ctx, variable.filters)));
|
|
3151
|
+
return resolved.map((candidate) => candidate.columnId);
|
|
3152
|
+
}
|
|
2979
3153
|
function buildAssertionBindings(assertion, ctx) {
|
|
2980
3154
|
const bindings = /* @__PURE__ */ new Map();
|
|
2981
3155
|
for (const variable of assertion.variables) {
|
|
@@ -2996,6 +3170,14 @@ function buildAssertionFallbackUsage(assertion, ctx) {
|
|
|
2996
3170
|
}
|
|
2997
3171
|
return fallbackUsage;
|
|
2998
3172
|
}
|
|
3173
|
+
function checkAllVariablesBound(assertion, bindings) {
|
|
3174
|
+
for (const variable of assertion.variables) {
|
|
3175
|
+
if (bindings.get(variable.name) === void 0) {
|
|
3176
|
+
return { satisfied: false, skipReason: "unresolvable" };
|
|
3177
|
+
}
|
|
3178
|
+
}
|
|
3179
|
+
return { satisfied: true };
|
|
3180
|
+
}
|
|
2999
3181
|
function evaluateAssertionPreconditions(preconditions, bindings, fallbackUsage = /* @__PURE__ */ new Map()) {
|
|
3000
3182
|
if (preconditions.length === 0)
|
|
3001
3183
|
return { satisfied: true };
|
|
@@ -3066,18 +3248,24 @@ class FormulaValidationService {
|
|
|
3066
3248
|
_evaluateAssertion(assertion, role, ctx, language) {
|
|
3067
3249
|
try {
|
|
3068
3250
|
const bindings = buildAssertionBindings(assertion, ctx);
|
|
3251
|
+
if (assertion.type === "VALUE_ASSERTION") {
|
|
3252
|
+
const bindingCheck = checkAllVariablesBound(assertion, bindings);
|
|
3253
|
+
if (!bindingCheck.satisfied) {
|
|
3254
|
+
return this._skippedResult(assertion, role, bindingCheck.skipReason ?? "unresolvable");
|
|
3255
|
+
}
|
|
3256
|
+
}
|
|
3069
3257
|
const fallbackUsage = buildAssertionFallbackUsage(assertion, ctx);
|
|
3070
3258
|
const precondition = evaluateAssertionPreconditions(assertion.preconditions, bindings, fallbackUsage);
|
|
3071
3259
|
if (!precondition.satisfied) {
|
|
3072
3260
|
return this._skippedResult(assertion, role, precondition.skipReason ?? "unresolvable");
|
|
3073
3261
|
}
|
|
3074
3262
|
if (assertion.type === "EXISTENCE_ASSERTION") {
|
|
3075
|
-
const matchedFactCount = resolveFormulaVariableMatchCount(assertion.variables[0], ctx);
|
|
3263
|
+
const matchedFactCount = assertion.variables.length === 1 ? resolveFormulaVariableMatchCount(assertion.variables[0], ctx) : resolveFormulaVariableSetMatchCount(assertion.variables, ctx);
|
|
3076
3264
|
const satisfied2 = evaluateExistenceTest(assertion.test, matchedFactCount);
|
|
3077
|
-
return this._evaluatedResult(assertion, role, satisfied2, language, matchedFactCount);
|
|
3265
|
+
return this._evaluatedResult(assertion, role, ctx, satisfied2, language, matchedFactCount);
|
|
3078
3266
|
}
|
|
3079
3267
|
const satisfied = evaluateFormulaTest(assertion.test, bindings, fallbackUsage);
|
|
3080
|
-
return this._evaluatedResult(assertion, role, satisfied, language);
|
|
3268
|
+
return this._evaluatedResult(assertion, role, ctx, satisfied, language);
|
|
3081
3269
|
} catch (error2) {
|
|
3082
3270
|
if (error2 instanceof FormulaExpressionError) {
|
|
3083
3271
|
console.warn(`[FormulaValidationService] Assertion "${assertion.id}" could not be evaluated (${error2.reason}): ${error2.message}`);
|
|
@@ -3098,7 +3286,8 @@ class FormulaValidationService {
|
|
|
3098
3286
|
skipReason
|
|
3099
3287
|
};
|
|
3100
3288
|
}
|
|
3101
|
-
_evaluatedResult(assertion, role, satisfied, language, matchedFactCount) {
|
|
3289
|
+
_evaluatedResult(assertion, role, ctx, satisfied, language, matchedFactCount) {
|
|
3290
|
+
const message = satisfied ? void 0 : this._pickMessage(assertion, language);
|
|
3102
3291
|
return {
|
|
3103
3292
|
assertionId: assertion.id,
|
|
3104
3293
|
roleURI: role.roleURI,
|
|
@@ -3107,8 +3296,9 @@ class FormulaValidationService {
|
|
|
3107
3296
|
severity: assertion.severity,
|
|
3108
3297
|
satisfied,
|
|
3109
3298
|
skipped: false,
|
|
3110
|
-
message
|
|
3111
|
-
matchedFactCount
|
|
3299
|
+
message,
|
|
3300
|
+
matchedFactCount,
|
|
3301
|
+
conceptLinks: message ? this._buildConceptLinks(assertion, role.roleURI, ctx, message) : void 0
|
|
3112
3302
|
};
|
|
3113
3303
|
}
|
|
3114
3304
|
_pickMessage(assertion, language) {
|
|
@@ -3116,7 +3306,55 @@ class FormulaValidationService {
|
|
|
3116
3306
|
if (messages.length === 0)
|
|
3117
3307
|
return void 0;
|
|
3118
3308
|
const message = messages.find((m) => m.xmlLang === language) ?? messages.find((m) => m.xmlLang === "en") ?? messages[0];
|
|
3119
|
-
return message.text;
|
|
3309
|
+
return this._stripTechnicalDetail(message.text);
|
|
3310
|
+
}
|
|
3311
|
+
/**
|
|
3312
|
+
* Taxonomy-authored messages append raw XBRL Formula/XPath debug expressions after the
|
|
3313
|
+
* human-readable sentence, delimited by " | {...}" (e.g. `xff:has-fallback-value(...)`,
|
|
3314
|
+
* `varArc_...` variable names) — see the fixture's `*_formula.json` message text. That debug
|
|
3315
|
+
* payload is meaningless to a non-technical filer, so only the sentence preceding it is shown.
|
|
3316
|
+
*/
|
|
3317
|
+
_stripTechnicalDetail(text) {
|
|
3318
|
+
const braceIndex = text.indexOf("{");
|
|
3319
|
+
if (braceIndex === -1)
|
|
3320
|
+
return text.trim();
|
|
3321
|
+
return text.slice(0, braceIndex).replace(/\|\s*$/, "").trim();
|
|
3322
|
+
}
|
|
3323
|
+
/**
|
|
3324
|
+
* JDF-059: resolves every concept the assertion's variables name directly (JDF-045's
|
|
3325
|
+
* `resolveVariableConceptQNames`) to a clickable fact location — but only the ones the
|
|
3326
|
+
* (already-stripped) message text actually mentions by backtick-quoted local name, e.g.
|
|
3327
|
+
* `` `Equity` `` — a concept a variable references internally (say, as a dimensional
|
|
3328
|
+
* exclusion) but the message never calls out by name has nothing for the user to click on, so
|
|
3329
|
+
* it's left out rather than guessed at. Column resolution reuses the assertion's own filters
|
|
3330
|
+
* (`resolveVariableColumnsForConcept`), so the link always points at the exact fact this
|
|
3331
|
+
* specific assertion run cared about — never just "some field for this concept" — including an
|
|
3332
|
+
* empty one for a failed "MUST exist" assertion, so the user can navigate straight to where the
|
|
3333
|
+
* missing value belongs.
|
|
3334
|
+
*/
|
|
3335
|
+
_buildConceptLinks(assertion, roleURI, ctx, message) {
|
|
3336
|
+
var _a, _b, _c;
|
|
3337
|
+
const links = [];
|
|
3338
|
+
const seenLabels = /* @__PURE__ */ new Set();
|
|
3339
|
+
for (const variable of assertion.variables) {
|
|
3340
|
+
for (const qname of resolveVariableConceptQNames(variable)) {
|
|
3341
|
+
const conceptId = ((_a = ctx.conceptIdByQNamePerRole.get(roleURI)) == null ? void 0 : _a.get(qname)) ?? ((_c = (_b = ctx.conceptByQName.get(qname)) == null ? void 0 : _b[0]) == null ? void 0 : _c.conceptId);
|
|
3342
|
+
if (!conceptId)
|
|
3343
|
+
continue;
|
|
3344
|
+
const label = qname.includes(":") ? qname.split(":").pop() : qname;
|
|
3345
|
+
if (seenLabels.has(label))
|
|
3346
|
+
continue;
|
|
3347
|
+
if (!new RegExp("`" + this._escapeRegExp(label) + "`").test(message))
|
|
3348
|
+
continue;
|
|
3349
|
+
const columns = resolveVariableColumnsForConcept(variable, conceptId, ctx);
|
|
3350
|
+
links.push({ label, conceptQName: qname, conceptId, columnId: columns[0] });
|
|
3351
|
+
seenLabels.add(label);
|
|
3352
|
+
}
|
|
3353
|
+
}
|
|
3354
|
+
return links;
|
|
3355
|
+
}
|
|
3356
|
+
_escapeRegExp(text) {
|
|
3357
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3120
3358
|
}
|
|
3121
3359
|
}
|
|
3122
3360
|
class FactMatcher {
|
|
@@ -6162,15 +6400,33 @@ JupiterConceptTree.styles = css`
|
|
|
6162
6400
|
/* Row-level highlight while a field in this row has focus. Applied to every
|
|
6163
6401
|
cell so the row stays identifiable even after the user scrolls the table
|
|
6164
6402
|
horizontally away from the focused input. */
|
|
6165
|
-
.concept-name-cell.row-focused,
|
|
6166
6403
|
.field-cell.row-focused {
|
|
6167
6404
|
background: var(--jupiter-row-focus-background, rgba(25, 118, 210, 0.14));
|
|
6168
6405
|
}
|
|
6169
6406
|
|
|
6407
|
+
/* .concept-name-cell is position:sticky with left:0, so it renders on top of
|
|
6408
|
+
field-cells that have scrolled underneath it as the user tabs across columns.
|
|
6409
|
+
It must stay fully opaque, or that translucent tint lets the scrolled-under
|
|
6410
|
+
cells' content (inputs, icons) show through and visually overlap with this
|
|
6411
|
+
cell's label/badge. Layer the tint as a background-image over an explicit
|
|
6412
|
+
opaque background-color instead of replacing the background outright. */
|
|
6170
6413
|
.concept-name-cell.row-focused {
|
|
6414
|
+
background-image: linear-gradient(
|
|
6415
|
+
var(--jupiter-row-focus-background, rgba(25, 118, 210, 0.14)),
|
|
6416
|
+
var(--jupiter-row-focus-background, rgba(25, 118, 210, 0.14))
|
|
6417
|
+
);
|
|
6418
|
+
background-color: var(--jupiter-concept-background, #f8f9fa);
|
|
6171
6419
|
box-shadow: inset 3px 0 0 0 var(--jupiter-primary-color, #1976d2);
|
|
6172
6420
|
}
|
|
6173
6421
|
|
|
6422
|
+
.concept-name-cell.abstract.row-focused {
|
|
6423
|
+
background-color: var(--bg-color-1, var(--jupiter-abstract-background, #f0f2f5));
|
|
6424
|
+
}
|
|
6425
|
+
|
|
6426
|
+
.concept-name-cell.leaf.row-focused {
|
|
6427
|
+
background-color: var(--bg-color-2, var(--jupiter-leaf-background, #fff));
|
|
6428
|
+
}
|
|
6429
|
+
|
|
6174
6430
|
.concept-info-btn {
|
|
6175
6431
|
flex-shrink: 0;
|
|
6176
6432
|
width: 22px;
|
|
@@ -10290,6 +10546,35 @@ let JupiterFormulaValidationDialog = class extends LitElement {
|
|
|
10290
10546
|
}
|
|
10291
10547
|
this.requestUpdate();
|
|
10292
10548
|
}
|
|
10549
|
+
// JDF-059: navigates the host form to the exact fact this assertion's variable resolved to
|
|
10550
|
+
// (or its empty target cell, for a "must exist" failure) and closes this dialog, mirroring the
|
|
10551
|
+
// existing "click a validation error, land on its field" flow this feature deliberately reuses
|
|
10552
|
+
// rather than building a second focus mechanism.
|
|
10553
|
+
_handleConceptLinkClick(link) {
|
|
10554
|
+
this.dispatchEvent(new CustomEvent("formula-concept-click", {
|
|
10555
|
+
detail: { conceptId: link.conceptId, conceptQName: link.conceptQName, columnId: link.columnId },
|
|
10556
|
+
bubbles: true,
|
|
10557
|
+
composed: true
|
|
10558
|
+
}));
|
|
10559
|
+
}
|
|
10560
|
+
// Splits a message on its backtick-quoted concept mentions and renders the ones this
|
|
10561
|
+
// assertion resolved a fact location for (`result.conceptLinks`) as click-to-focus buttons;
|
|
10562
|
+
// any other backtick mention (e.g. an enumerated value like `Na`, not a concept) renders as
|
|
10563
|
+
// plain text with the backticks dropped, since there is nothing to link it to.
|
|
10564
|
+
_renderMessage(result) {
|
|
10565
|
+
const message = result.message ?? "";
|
|
10566
|
+
const linkByLabel = new Map((result.conceptLinks ?? []).map((link) => [link.label, link]));
|
|
10567
|
+
const parts = message.split(/(`[^`]+`)/g);
|
|
10568
|
+
return html`${parts.map((part) => {
|
|
10569
|
+
const match = /^`([^`]+)`$/.exec(part);
|
|
10570
|
+
if (!match)
|
|
10571
|
+
return part;
|
|
10572
|
+
const link = linkByLabel.get(match[1]);
|
|
10573
|
+
if (!link)
|
|
10574
|
+
return match[1];
|
|
10575
|
+
return html`<button type="button" class="concept-link" @click="${() => this._handleConceptLinkClick(link)}">${match[1]}</button>`;
|
|
10576
|
+
})}`;
|
|
10577
|
+
}
|
|
10293
10578
|
_groupByRole(failures) {
|
|
10294
10579
|
const grouped = failures.reduce((groups, result) => {
|
|
10295
10580
|
var _a;
|
|
@@ -10338,7 +10623,7 @@ let JupiterFormulaValidationDialog = class extends LitElement {
|
|
|
10338
10623
|
<span class="severity-badge ${result.severity === "ERROR" ? "error" : "warning"}">
|
|
10339
10624
|
${result.severity === "ERROR" ? I18n.t("formulaValidation.severityError") : I18n.t("formulaValidation.severityWarning")}
|
|
10340
10625
|
</span>
|
|
10341
|
-
<span class="assertion-message">${result
|
|
10626
|
+
<span class="assertion-message">${this._renderMessage(result)}</span>
|
|
10342
10627
|
</div>
|
|
10343
10628
|
`)}
|
|
10344
10629
|
</div>
|
|
@@ -10533,6 +10818,23 @@ JupiterFormulaValidationDialog.styles = css`
|
|
|
10533
10818
|
line-height: 1.4;
|
|
10534
10819
|
}
|
|
10535
10820
|
|
|
10821
|
+
.concept-link {
|
|
10822
|
+
font: inherit;
|
|
10823
|
+
font-weight: 600;
|
|
10824
|
+
color: var(--buttonBgColor, var(--jupiter-primary-color, #1976d2));
|
|
10825
|
+
background: none;
|
|
10826
|
+
border: none;
|
|
10827
|
+
padding: 0;
|
|
10828
|
+
margin: 0;
|
|
10829
|
+
cursor: pointer;
|
|
10830
|
+
text-decoration: underline;
|
|
10831
|
+
text-underline-offset: 2px;
|
|
10832
|
+
}
|
|
10833
|
+
|
|
10834
|
+
.concept-link:hover {
|
|
10835
|
+
opacity: 0.8;
|
|
10836
|
+
}
|
|
10837
|
+
|
|
10536
10838
|
.dialog-actions {
|
|
10537
10839
|
display: flex;
|
|
10538
10840
|
gap: 12px;
|
|
@@ -11381,6 +11683,14 @@ let JupiterDynamicForm = class extends LitElement {
|
|
|
11381
11683
|
_handleFormulaValidationDialogCancel() {
|
|
11382
11684
|
this._showFormulaValidationDialog = false;
|
|
11383
11685
|
}
|
|
11686
|
+
// JDF-059: a concept name inside a formula-validation message was clicked — close the popup
|
|
11687
|
+
// (mirrors the existing "click a validation error, land on its field" UX from the host-rendered
|
|
11688
|
+
// show-validation-results flow) and jump straight to the exact fact the assertion's own variable
|
|
11689
|
+
// resolution identified, via `columnId` rather than re-deriving it from dimensions.
|
|
11690
|
+
_handleFormulaConceptClick(event) {
|
|
11691
|
+
this._showFormulaValidationDialog = false;
|
|
11692
|
+
this.scrollToConcept(event.detail.conceptQName, void 0, void 0, event.detail.columnId, event.detail.conceptId);
|
|
11693
|
+
}
|
|
11384
11694
|
_handleRoleFilterApply(event) {
|
|
11385
11695
|
var _a, _b;
|
|
11386
11696
|
const { selectedRoleIds, periodPreferences } = event.detail;
|
|
@@ -12519,7 +12829,6 @@ let JupiterDynamicForm = class extends LitElement {
|
|
|
12519
12829
|
return;
|
|
12520
12830
|
}
|
|
12521
12831
|
this._correctLegacyPeriodStartInstantDates();
|
|
12522
|
-
this._runFormulaValidationPreflight();
|
|
12523
12832
|
const submissionData = this._generateSubmissionData();
|
|
12524
12833
|
console.log("📊 Form Submission Data:", JSON.stringify(submissionData, null, 2));
|
|
12525
12834
|
console.log("📊 Submission Data Summary:");
|
|
@@ -12541,6 +12850,16 @@ let JupiterDynamicForm = class extends LitElement {
|
|
|
12541
12850
|
this._submitDisabled = false;
|
|
12542
12851
|
}, 1e3);
|
|
12543
12852
|
}
|
|
12853
|
+
/**
|
|
12854
|
+
* Handler for the standalone "Quick Validate" button (visible only when `formulaEnabled` is
|
|
12855
|
+
* true). Runs the formula-assertion pre-flight in isolation from the original Validate/submit
|
|
12856
|
+
* button, which no longer triggers formula validation as a side effect.
|
|
12857
|
+
*/
|
|
12858
|
+
_handleQuickValidate() {
|
|
12859
|
+
if (this.mode === "admin")
|
|
12860
|
+
return;
|
|
12861
|
+
this._runFormulaValidationPreflight();
|
|
12862
|
+
}
|
|
12544
12863
|
/**
|
|
12545
12864
|
* JDF-049: runs FormulaValidationService against the live form state and stores the results
|
|
12546
12865
|
* for the results dialog (JDF-050). Graceful no-op — per JDF-048's gating and this ticket's
|
|
@@ -12550,21 +12869,44 @@ let JupiterDynamicForm = class extends LitElement {
|
|
|
12550
12869
|
* cost when off" requirement).
|
|
12551
12870
|
*/
|
|
12552
12871
|
_runFormulaValidationPreflight() {
|
|
12553
|
-
var _a, _b, _c, _d, _e, _f, _g, _h
|
|
12872
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
12554
12873
|
if (!this.formulaEnabled)
|
|
12555
12874
|
return;
|
|
12556
12875
|
const formulaRoles = (_c = (_b = (_a = this.xbrlInput) == null ? void 0 : _a.formula) == null ? void 0 : _b[0]) == null ? void 0 : _c.roles;
|
|
12557
12876
|
if (!formulaRoles || formulaRoles.length === 0)
|
|
12558
12877
|
return;
|
|
12878
|
+
const scopeSections = ((_e = (_d = this._currentSchema) == null ? void 0 : _d.sections) == null ? void 0 : _e.length) ? this._currentSchema.sections : this._allSections;
|
|
12879
|
+
if (!scopeSections || scopeSections.length === 0)
|
|
12880
|
+
return;
|
|
12881
|
+
const visibleRoleURIs = new Set(scopeSections.map((section2) => {
|
|
12882
|
+
var _a2;
|
|
12883
|
+
return ((_a2 = section2.metadata) == null ? void 0 : _a2.roleURI) || section2.id;
|
|
12884
|
+
}));
|
|
12885
|
+
const scopedFormulaRoles = formulaRoles.filter((role) => visibleRoleURIs.has(role.roleURI));
|
|
12886
|
+
if (scopedFormulaRoles.length === 0)
|
|
12887
|
+
return;
|
|
12888
|
+
const conceptSections = scopeSections.map((section2) => {
|
|
12889
|
+
var _a2;
|
|
12890
|
+
return {
|
|
12891
|
+
roleURI: ((_a2 = section2.metadata) == null ? void 0 : _a2.roleURI) || section2.id,
|
|
12892
|
+
concepts: section2.concepts,
|
|
12893
|
+
columns: section2.columns
|
|
12894
|
+
};
|
|
12895
|
+
});
|
|
12559
12896
|
const ctx = buildFormulaResolutionContext(
|
|
12560
12897
|
this._formData,
|
|
12561
12898
|
this._mergeAllSectionColumns(),
|
|
12562
|
-
|
|
12563
|
-
(
|
|
12899
|
+
conceptSections,
|
|
12900
|
+
(_h = (_g = (_f = this.xbrlInput) == null ? void 0 : _f.hypercubes) == null ? void 0 : _g[0]) == null ? void 0 : _h.roles,
|
|
12564
12901
|
this.periodStartDate,
|
|
12565
|
-
this.periodEndDate
|
|
12902
|
+
this.periodEndDate,
|
|
12903
|
+
this._typedMemberData
|
|
12566
12904
|
);
|
|
12567
|
-
const
|
|
12905
|
+
const scopedXbrlInput = {
|
|
12906
|
+
...this.xbrlInput,
|
|
12907
|
+
formula: [{ ...this.xbrlInput.formula[0], roles: scopedFormulaRoles }]
|
|
12908
|
+
};
|
|
12909
|
+
const results = this._formulaValidationService.evaluate(scopedXbrlInput, ctx, this.language);
|
|
12568
12910
|
const summary = this._formulaValidationService.summarize(results);
|
|
12569
12911
|
this._formulaValidationResults = results;
|
|
12570
12912
|
this._formulaValidationSummary = summary;
|
|
@@ -14712,6 +15054,59 @@ let JupiterDynamicForm = class extends LitElement {
|
|
|
14712
15054
|
}
|
|
14713
15055
|
return null;
|
|
14714
15056
|
}
|
|
15057
|
+
// Same taxonomy concept can be placed at multiple points in a role's presentation tree
|
|
15058
|
+
// with different preferred labels (e.g. a rollforward's periodStartLabel/periodEndLabel
|
|
15059
|
+
// rows) — each such occurrence gets its own ConceptTree row/id. Unlike _findConceptByName,
|
|
15060
|
+
// this collects every matching row (DFS order) so the caller can disambiguate between them
|
|
15061
|
+
// instead of always taking the first one found.
|
|
15062
|
+
_findAllConceptsByName(concepts, name) {
|
|
15063
|
+
var _a;
|
|
15064
|
+
const localName = name.includes(":") ? name.split(":").pop() : name;
|
|
15065
|
+
const matches = [];
|
|
15066
|
+
for (const c2 of concepts) {
|
|
15067
|
+
const cLocal = c2.name.includes(":") ? c2.name.split(":").pop() : c2.name;
|
|
15068
|
+
if (c2.name === name || cLocal === localName)
|
|
15069
|
+
matches.push(c2);
|
|
15070
|
+
if ((_a = c2.children) == null ? void 0 : _a.length) {
|
|
15071
|
+
matches.push(...this._findAllConceptsByName(c2.children, name));
|
|
15072
|
+
}
|
|
15073
|
+
}
|
|
15074
|
+
return matches;
|
|
15075
|
+
}
|
|
15076
|
+
_findConceptById(concepts, id) {
|
|
15077
|
+
var _a;
|
|
15078
|
+
for (const c2 of concepts) {
|
|
15079
|
+
if (c2.id === id)
|
|
15080
|
+
return c2;
|
|
15081
|
+
if ((_a = c2.children) == null ? void 0 : _a.length) {
|
|
15082
|
+
const hit = this._findConceptById(c2.children, id);
|
|
15083
|
+
if (hit)
|
|
15084
|
+
return hit;
|
|
15085
|
+
}
|
|
15086
|
+
}
|
|
15087
|
+
return null;
|
|
15088
|
+
}
|
|
15089
|
+
// Loose value comparison used to disambiguate rows that share a concept name: exact
|
|
15090
|
+
// string match, numeric equality, or numeric equality up to a power-of-ten scale
|
|
15091
|
+
// (the form may store a display value while the caller passes the raw XBRL value).
|
|
15092
|
+
_valueLooselyMatches(fieldValue, targetValue) {
|
|
15093
|
+
const fv = String(fieldValue ?? "");
|
|
15094
|
+
if (fv === targetValue)
|
|
15095
|
+
return true;
|
|
15096
|
+
const fNum = Number(fv);
|
|
15097
|
+
const tNum = Number(targetValue);
|
|
15098
|
+
if (isNaN(fNum) || isNaN(tNum))
|
|
15099
|
+
return false;
|
|
15100
|
+
if (fNum === tNum)
|
|
15101
|
+
return true;
|
|
15102
|
+
if (fNum !== 0 && tNum !== 0) {
|
|
15103
|
+
const ratio = Math.abs(tNum / fNum);
|
|
15104
|
+
const log = Math.log10(ratio);
|
|
15105
|
+
if (Math.abs(log - Math.round(log)) < 1e-4)
|
|
15106
|
+
return true;
|
|
15107
|
+
}
|
|
15108
|
+
return false;
|
|
15109
|
+
}
|
|
14715
15110
|
_findColumnByDimensions(columns, dims) {
|
|
14716
15111
|
var _a;
|
|
14717
15112
|
return (_a = columns.find((col) => {
|
|
@@ -14728,9 +15123,9 @@ let JupiterDynamicForm = class extends LitElement {
|
|
|
14728
15123
|
return dims.length === 1 && this._normalizeAxisId(col.dimensionData.axisId ?? "") === this._normalizeAxisId(dims[0].axis) && this._normalizeMemberId(col.dimensionData.memberId ?? "") === this._normalizeMemberId(dims[0].member);
|
|
14729
15124
|
})) == null ? void 0 : _a.id;
|
|
14730
15125
|
}
|
|
14731
|
-
async scrollToConcept(conceptName, dimensions, match) {
|
|
15126
|
+
async scrollToConcept(conceptName, dimensions, match, columnId, exactConceptId) {
|
|
14732
15127
|
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l;
|
|
14733
|
-
console.log(`[scrollToConcept] ▶ START conceptName=${conceptName} value=${match == null ? void 0 : match.value} dims=${JSON.stringify(dimensions)}`);
|
|
15128
|
+
console.log(`[scrollToConcept] ▶ START conceptName=${conceptName} value=${match == null ? void 0 : match.value} dims=${JSON.stringify(dimensions)} columnId=${columnId} exactConceptId=${exactConceptId}`);
|
|
14734
15129
|
let targetSection = null;
|
|
14735
15130
|
let targetConcept = null;
|
|
14736
15131
|
const sectionsToSearch = [
|
|
@@ -14741,14 +15136,62 @@ let JupiterDynamicForm = class extends LitElement {
|
|
|
14741
15136
|
})
|
|
14742
15137
|
];
|
|
14743
15138
|
console.log(`[scrollToConcept] Searching ${sectionsToSearch.length} sections`);
|
|
14744
|
-
|
|
14745
|
-
|
|
14746
|
-
|
|
14747
|
-
|
|
14748
|
-
|
|
14749
|
-
|
|
14750
|
-
console.log(`[scrollToConcept] Section "${section2.id}"
|
|
14751
|
-
|
|
15139
|
+
const hasValueMatch = (match == null ? void 0 : match.value) !== void 0 && (match == null ? void 0 : match.value) !== null;
|
|
15140
|
+
const targetValue = hasValueMatch ? String(match.value) : null;
|
|
15141
|
+
if (exactConceptId) {
|
|
15142
|
+
for (const section2 of sectionsToSearch) {
|
|
15143
|
+
const found = this._findConceptById(section2.concepts, exactConceptId);
|
|
15144
|
+
if (found) {
|
|
15145
|
+
console.log(`[scrollToConcept] Section "${section2.id}" matched exactConceptId="${exactConceptId}"`);
|
|
15146
|
+
targetSection = section2;
|
|
15147
|
+
targetConcept = found;
|
|
15148
|
+
break;
|
|
15149
|
+
}
|
|
15150
|
+
}
|
|
15151
|
+
}
|
|
15152
|
+
if (!targetSection || !targetConcept) {
|
|
15153
|
+
for (const section2 of sectionsToSearch) {
|
|
15154
|
+
const candidates = this._findAllConceptsByName(section2.concepts, conceptName);
|
|
15155
|
+
if (!candidates.length)
|
|
15156
|
+
continue;
|
|
15157
|
+
const cols = section2.columns ?? this._columns;
|
|
15158
|
+
const resolvedColumnId = columnId ?? ((dimensions == null ? void 0 : dimensions.length) ? this._findColumnByDimensions(cols, dimensions) : void 0);
|
|
15159
|
+
let picked;
|
|
15160
|
+
if (candidates.length > 1) {
|
|
15161
|
+
picked = candidates.find((c2) => {
|
|
15162
|
+
const rowData = this._formData[c2.id];
|
|
15163
|
+
if (!rowData)
|
|
15164
|
+
return false;
|
|
15165
|
+
if (resolvedColumnId) {
|
|
15166
|
+
const cellValue = rowData[resolvedColumnId];
|
|
15167
|
+
if (cellValue === void 0 || cellValue === null || cellValue === "")
|
|
15168
|
+
return false;
|
|
15169
|
+
return !hasValueMatch || this._valueLooselyMatches(cellValue, targetValue);
|
|
15170
|
+
}
|
|
15171
|
+
if (hasValueMatch) {
|
|
15172
|
+
return Object.values(rowData).some(
|
|
15173
|
+
(v) => v !== void 0 && v !== null && v !== "" && this._valueLooselyMatches(v, targetValue)
|
|
15174
|
+
);
|
|
15175
|
+
}
|
|
15176
|
+
return false;
|
|
15177
|
+
});
|
|
15178
|
+
console.log(`[scrollToConcept] Section "${section2.id}" has ${candidates.length} rows named "${conceptName}" | disambiguated=${(picked == null ? void 0 : picked.id) ?? "none (using first)"}`);
|
|
15179
|
+
}
|
|
15180
|
+
const found = picked ?? candidates[0];
|
|
15181
|
+
if (columnId) {
|
|
15182
|
+
const hasColumn = cols.some((c2) => c2.id === columnId);
|
|
15183
|
+
console.log(`[scrollToConcept] Section "${section2.id}" has concept "${found.id}" | columnId="${columnId}" present=${hasColumn}`);
|
|
15184
|
+
if (hasColumn) {
|
|
15185
|
+
targetSection = section2;
|
|
15186
|
+
targetConcept = found;
|
|
15187
|
+
break;
|
|
15188
|
+
} else if (!targetSection) {
|
|
15189
|
+
targetSection = section2;
|
|
15190
|
+
targetConcept = found;
|
|
15191
|
+
}
|
|
15192
|
+
} else if (dimensions == null ? void 0 : dimensions.length) {
|
|
15193
|
+
console.log(`[scrollToConcept] Section "${section2.id}" has concept "${found.id}" | colMatch=${resolvedColumnId ?? "null"} | cols=${cols.map((c2) => c2.id).join(",")}`);
|
|
15194
|
+
if (resolvedColumnId) {
|
|
14752
15195
|
targetSection = section2;
|
|
14753
15196
|
targetConcept = found;
|
|
14754
15197
|
break;
|
|
@@ -14775,9 +15218,9 @@ let JupiterDynamicForm = class extends LitElement {
|
|
|
14775
15218
|
}
|
|
14776
15219
|
const columns = targetSection.columns ?? this._columns;
|
|
14777
15220
|
let targetColumnId = null;
|
|
14778
|
-
|
|
14779
|
-
|
|
14780
|
-
if (dimensions == null ? void 0 : dimensions.length) {
|
|
15221
|
+
if (columnId) {
|
|
15222
|
+
targetColumnId = columnId;
|
|
15223
|
+
} else if (dimensions == null ? void 0 : dimensions.length) {
|
|
14781
15224
|
targetColumnId = this._findColumnByDimensions(columns, dimensions) ?? null;
|
|
14782
15225
|
} else if (!hasValueMatch) {
|
|
14783
15226
|
targetColumnId = ((_b = columns[0]) == null ? void 0 : _b.id) ?? null;
|
|
@@ -15023,6 +15466,15 @@ let JupiterDynamicForm = class extends LitElement {
|
|
|
15023
15466
|
</button>
|
|
15024
15467
|
` : ""}
|
|
15025
15468
|
|
|
15469
|
+
${this.formulaEnabled && this.mode !== "admin" ? html`
|
|
15470
|
+
<button
|
|
15471
|
+
class="btn-secondary btn-quick-validate"
|
|
15472
|
+
@click="${this._handleQuickValidate}"
|
|
15473
|
+
?disabled="${this.disabled || this.readonly || this._validationStatus === "inProgress"}"
|
|
15474
|
+
>
|
|
15475
|
+
${I18n.t("form.quickValidate")}
|
|
15476
|
+
</button>
|
|
15477
|
+
` : ""}
|
|
15026
15478
|
|
|
15027
15479
|
<button
|
|
15028
15480
|
class="btn-primary"
|
|
@@ -15060,6 +15512,7 @@ let JupiterDynamicForm = class extends LitElement {
|
|
|
15060
15512
|
.results="${this._formulaValidationResults}"
|
|
15061
15513
|
.summary="${this._formulaValidationSummary}"
|
|
15062
15514
|
@dialog-cancel="${this._handleFormulaValidationDialogCancel}"
|
|
15515
|
+
@formula-concept-click="${this._handleFormulaConceptClick}"
|
|
15063
15516
|
@click="${this._handleFormulaValidationDialogCancel}"
|
|
15064
15517
|
></jupiter-formula-validation-dialog>
|
|
15065
15518
|
` : ""}
|