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/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
- if (Array.isArray(binding))
2526
- return binding.reduce((total, value) => total + value, 0);
2527
- return binding;
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 }) => total + sign * evalValue(term, bindings, test), 0);
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.conceptName,
2722
- balance: concept.balance
2750
+ qname: concept.name,
2751
+ balance: concept.balance,
2752
+ effectivePeriodRole: concept.effectivePeriodRole
2723
2753
  };
2724
- if (!conceptById.has(concept.id))
2725
- conceptById.set(concept.id, info);
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.children || []).forEach((child) => indexMemberTree(child, memberQNameToId));
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.elementAbstract,
2780
+ isAbstract: (concept) => concept.abstract === true,
2744
2781
  getChildren: (concept) => concept.children || []
2745
2782
  };
2746
- function buildFormulaResolutionContext(formData, columns, presentationRoles, hypercubeRoles, periodStartDate, periodEndDate) {
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
- presentationRoles.forEach((role) => {
2752
- var _a;
2753
- const concepts = ((_a = role.presentationLinkbase) == null ? void 0 : _a.concepts) || [];
2789
+ const columnsByRole = /* @__PURE__ */ new Map();
2790
+ const roleURIByConceptId = /* @__PURE__ */ new Map();
2791
+ sections.forEach((section2) => {
2754
2792
  const childrenIndex = /* @__PURE__ */ new Map();
2755
- concepts.forEach((concept) => indexConceptTree(concept, conceptByQName, conceptById, childrenIndex));
2756
- childrenByRole.set(role.roleURI, childrenIndex);
2757
- totalGroupChildrenByRole.set(role.roleURI, buildTotalGroupChildrenIndex(concepts, PRESENTATION_TOTAL_GROUP_ACCESSORS));
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
- allCandidates: collectAllCandidates(formData)
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, columnId) {
2806
- return ctx.columns.find((column2) => column2.id === columnId);
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 columnMatchesTypedDimension(column2, axisId) {
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 matchFilter(filter2, universe, ctx) {
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 info = qname ? ctx.conceptByQName.get(qname) : void 0;
2875
- if (!info)
2959
+ const infos = qname ? ctx.conceptByQName.get(qname) : void 0;
2960
+ if (!infos || infos.length === 0)
2876
2961
  return [];
2877
- return universe.filter((candidate) => candidate.conceptId === info.conceptId);
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 anchorInfo = anchorQName ? ctx.conceptByQName.get(anchorQName) : void 0;
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 (!anchorInfo || !childrenIndex)
2978
+ if (!anchorConceptId || !childrenIndex)
2893
2979
  return [];
2894
2980
  let relatedConceptIds;
2895
2981
  if (axis === "descendant") {
2896
- relatedConceptIds = collectDescendantConceptIds(childrenIndex, anchorInfo.conceptId);
2982
+ relatedConceptIds = collectDescendantConceptIds(childrenIndex, anchorConceptId);
2897
2983
  } else {
2898
- const nestedChildren = childrenIndex.get(anchorInfo.conceptId) || [];
2899
- relatedConceptIds = nestedChildren.length > 0 ? nestedChildren : ((_a = ctx.totalGroupChildrenByRole.get(linkrole ?? "")) == null ? void 0 : _a.get(anchorInfo.conceptId)) || [];
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
- return universe.filter((candidate) => columnMatchesExplicitDimension(getColumn(ctx, candidate.columnId), axisId, memberId));
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(getColumn(ctx, candidate.columnId), axisId));
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.columnId), dateAttribute, ctx));
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 parseFloat((_a = ctx.formData[candidate.conceptId]) == null ? void 0 : _a[candidate.columnId]);
2951
- }).filter((value) => !Number.isNaN(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: satisfied ? void 0 : this._pickMessage(assertion, language),
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.message}</span>
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, _i;
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
- ((_f = (_e = (_d = this.xbrlInput) == null ? void 0 : _d.presentation) == null ? void 0 : _e[0]) == null ? void 0 : _f.roles) || [],
12563
- (_i = (_h = (_g = this.xbrlInput) == null ? void 0 : _g.hypercubes) == null ? void 0 : _h[0]) == null ? void 0 : _i.roles,
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 results = this._formulaValidationService.evaluate(this.xbrlInput, ctx, this.language);
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
- for (const section2 of sectionsToSearch) {
14745
- const found = this._findConceptByName(section2.concepts, conceptName);
14746
- if (found) {
14747
- if (dimensions == null ? void 0 : dimensions.length) {
14748
- const cols = section2.columns ?? this._columns;
14749
- const colId = this._findColumnByDimensions(cols, dimensions);
14750
- console.log(`[scrollToConcept] Section "${section2.id}" has concept "${found.id}" | colMatch=${colId ?? "null"} | cols=${cols.map((c2) => c2.id).join(",")}`);
14751
- if (colId) {
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
- const hasValueMatch = (match == null ? void 0 : match.value) !== void 0 && (match == null ? void 0 : match.value) !== null;
14779
- const targetValue = hasValueMatch ? String(match.value) : null;
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
  ` : ""}