@gooddata/sdk-ui-ext 11.55.0-alpha.3 → 11.55.0-alpha.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.
Files changed (146) hide show
  1. package/NOTICE +175 -19
  2. package/esm/index.d.ts +4 -2
  3. package/esm/index.d.ts.map +1 -1
  4. package/esm/index.js +3 -2
  5. package/esm/internal/components/configurationControls/conditionalFormatting/ConditionEditor.d.ts.map +1 -1
  6. package/esm/internal/components/configurationControls/conditionalFormatting/ConditionEditor.js +8 -5
  7. package/esm/internal/components/configurationControls/conditionalFormatting/ConditionalFormattingDialog.d.ts +20 -5
  8. package/esm/internal/components/configurationControls/conditionalFormatting/ConditionalFormattingDialog.d.ts.map +1 -1
  9. package/esm/internal/components/configurationControls/conditionalFormatting/ConditionalFormattingDialog.js +143 -33
  10. package/esm/internal/components/configurationControls/conditionalFormatting/ConditionalFormattingSection.d.ts +19 -1
  11. package/esm/internal/components/configurationControls/conditionalFormatting/ConditionalFormattingSection.d.ts.map +1 -1
  12. package/esm/internal/components/configurationControls/conditionalFormatting/ConditionalFormattingSection.js +146 -46
  13. package/esm/internal/components/configurationControls/conditionalFormatting/conditionalFormattingModel.d.ts +34 -7
  14. package/esm/internal/components/configurationControls/conditionalFormatting/conditionalFormattingModel.d.ts.map +1 -1
  15. package/esm/internal/components/configurationControls/conditionalFormatting/conditionalFormattingModel.js +38 -7
  16. package/esm/internal/components/configurationPanels/PivotTableConfigurationPanel.d.ts.map +1 -1
  17. package/esm/internal/components/configurationPanels/PivotTableConfigurationPanel.js +9 -4
  18. package/esm/internal/components/dialogs/userManagementDialogs/UserGroups/backend/loadUserGroupOptionsPromise.d.ts.map +1 -1
  19. package/esm/internal/components/dialogs/userManagementDialogs/UserGroups/backend/loadUserGroupOptionsPromise.js +1 -1
  20. package/esm/internal/components/pluggableVisualizations/baseChart/PluggableBaseChart.d.ts.map +1 -1
  21. package/esm/internal/components/pluggableVisualizations/pivotTableNext/PluggablePivotTableNext.d.ts.map +1 -1
  22. package/esm/internal/components/pluggableVisualizations/pivotTableNext/PluggablePivotTableNext.js +18 -18
  23. package/esm/internal/components/pluggableVisualizations/pivotTableNext/pivotTableNextConfigFromInsight.d.ts +1 -2
  24. package/esm/internal/components/pluggableVisualizations/pivotTableNext/pivotTableNextConfigFromInsight.d.ts.map +1 -1
  25. package/esm/internal/components/pluggableVisualizations/pivotTableNext/pivotTableNextConfigFromInsight.js +3 -9
  26. package/esm/internal/components/pluggableVisualizations/repeater/PluggableRepeater.d.ts.map +1 -1
  27. package/esm/internal/index.d.ts +1 -0
  28. package/esm/internal/index.d.ts.map +1 -1
  29. package/esm/internal/index.js +1 -0
  30. package/esm/internal/translations/de-DE.localization-bundle.d.ts +5 -2
  31. package/esm/internal/translations/de-DE.localization-bundle.d.ts.map +1 -1
  32. package/esm/internal/translations/de-DE.localization-bundle.js +5 -2
  33. package/esm/internal/translations/en-AU.localization-bundle.d.ts +5 -2
  34. package/esm/internal/translations/en-AU.localization-bundle.d.ts.map +1 -1
  35. package/esm/internal/translations/en-AU.localization-bundle.js +5 -2
  36. package/esm/internal/translations/en-GB.localization-bundle.d.ts +5 -2
  37. package/esm/internal/translations/en-GB.localization-bundle.d.ts.map +1 -1
  38. package/esm/internal/translations/en-GB.localization-bundle.js +5 -2
  39. package/esm/internal/translations/en-US.localization-bundle.d.ts +14 -2
  40. package/esm/internal/translations/en-US.localization-bundle.d.ts.map +1 -1
  41. package/esm/internal/translations/en-US.localization-bundle.js +18 -6
  42. package/esm/internal/translations/es-419.localization-bundle.d.ts +5 -2
  43. package/esm/internal/translations/es-419.localization-bundle.d.ts.map +1 -1
  44. package/esm/internal/translations/es-419.localization-bundle.js +5 -2
  45. package/esm/internal/translations/es-ES.localization-bundle.d.ts +5 -2
  46. package/esm/internal/translations/es-ES.localization-bundle.d.ts.map +1 -1
  47. package/esm/internal/translations/es-ES.localization-bundle.js +5 -2
  48. package/esm/internal/translations/fi-FI.localization-bundle.d.ts +5 -2
  49. package/esm/internal/translations/fi-FI.localization-bundle.d.ts.map +1 -1
  50. package/esm/internal/translations/fi-FI.localization-bundle.js +5 -2
  51. package/esm/internal/translations/fr-CA.localization-bundle.d.ts +5 -2
  52. package/esm/internal/translations/fr-CA.localization-bundle.d.ts.map +1 -1
  53. package/esm/internal/translations/fr-CA.localization-bundle.js +5 -2
  54. package/esm/internal/translations/fr-FR.localization-bundle.d.ts +5 -2
  55. package/esm/internal/translations/fr-FR.localization-bundle.d.ts.map +1 -1
  56. package/esm/internal/translations/fr-FR.localization-bundle.js +5 -2
  57. package/esm/internal/translations/id-ID.localization-bundle.d.ts +5 -2
  58. package/esm/internal/translations/id-ID.localization-bundle.d.ts.map +1 -1
  59. package/esm/internal/translations/id-ID.localization-bundle.js +5 -2
  60. package/esm/internal/translations/it-IT.localization-bundle.d.ts +5 -2
  61. package/esm/internal/translations/it-IT.localization-bundle.d.ts.map +1 -1
  62. package/esm/internal/translations/it-IT.localization-bundle.js +5 -2
  63. package/esm/internal/translations/ja-JP.localization-bundle.d.ts +5 -2
  64. package/esm/internal/translations/ja-JP.localization-bundle.d.ts.map +1 -1
  65. package/esm/internal/translations/ja-JP.localization-bundle.js +5 -2
  66. package/esm/internal/translations/ko-KR.localization-bundle.d.ts +5 -2
  67. package/esm/internal/translations/ko-KR.localization-bundle.d.ts.map +1 -1
  68. package/esm/internal/translations/ko-KR.localization-bundle.js +5 -2
  69. package/esm/internal/translations/nl-NL.localization-bundle.d.ts +5 -2
  70. package/esm/internal/translations/nl-NL.localization-bundle.d.ts.map +1 -1
  71. package/esm/internal/translations/nl-NL.localization-bundle.js +5 -2
  72. package/esm/internal/translations/pl-PL.localization-bundle.d.ts +5 -2
  73. package/esm/internal/translations/pl-PL.localization-bundle.d.ts.map +1 -1
  74. package/esm/internal/translations/pl-PL.localization-bundle.js +5 -2
  75. package/esm/internal/translations/pt-BR.localization-bundle.d.ts +5 -2
  76. package/esm/internal/translations/pt-BR.localization-bundle.d.ts.map +1 -1
  77. package/esm/internal/translations/pt-BR.localization-bundle.js +5 -2
  78. package/esm/internal/translations/pt-PT.localization-bundle.d.ts +5 -2
  79. package/esm/internal/translations/pt-PT.localization-bundle.d.ts.map +1 -1
  80. package/esm/internal/translations/pt-PT.localization-bundle.js +5 -2
  81. package/esm/internal/translations/ru-RU.localization-bundle.d.ts +5 -2
  82. package/esm/internal/translations/ru-RU.localization-bundle.d.ts.map +1 -1
  83. package/esm/internal/translations/ru-RU.localization-bundle.js +5 -2
  84. package/esm/internal/translations/sl-SI.localization-bundle.d.ts +5 -2
  85. package/esm/internal/translations/sl-SI.localization-bundle.d.ts.map +1 -1
  86. package/esm/internal/translations/sl-SI.localization-bundle.js +5 -2
  87. package/esm/internal/translations/th-TH.localization-bundle.d.ts +5 -2
  88. package/esm/internal/translations/th-TH.localization-bundle.d.ts.map +1 -1
  89. package/esm/internal/translations/th-TH.localization-bundle.js +5 -2
  90. package/esm/internal/translations/tr-TR.localization-bundle.d.ts +5 -2
  91. package/esm/internal/translations/tr-TR.localization-bundle.d.ts.map +1 -1
  92. package/esm/internal/translations/tr-TR.localization-bundle.js +5 -2
  93. package/esm/internal/translations/uk-UA.localization-bundle.d.ts +5 -2
  94. package/esm/internal/translations/uk-UA.localization-bundle.d.ts.map +1 -1
  95. package/esm/internal/translations/uk-UA.localization-bundle.js +5 -2
  96. package/esm/internal/translations/vi-VN.localization-bundle.d.ts +5 -2
  97. package/esm/internal/translations/vi-VN.localization-bundle.d.ts.map +1 -1
  98. package/esm/internal/translations/vi-VN.localization-bundle.js +5 -2
  99. package/esm/internal/translations/zh-HK.localization-bundle.d.ts +5 -2
  100. package/esm/internal/translations/zh-HK.localization-bundle.d.ts.map +1 -1
  101. package/esm/internal/translations/zh-HK.localization-bundle.js +6 -3
  102. package/esm/internal/translations/zh-Hans.localization-bundle.d.ts +5 -2
  103. package/esm/internal/translations/zh-Hans.localization-bundle.d.ts.map +1 -1
  104. package/esm/internal/translations/zh-Hans.localization-bundle.js +5 -2
  105. package/esm/internal/translations/zh-Hant.localization-bundle.d.ts +5 -2
  106. package/esm/internal/translations/zh-Hant.localization-bundle.d.ts.map +1 -1
  107. package/esm/internal/translations/zh-Hant.localization-bundle.js +5 -2
  108. package/esm/internal/utils/embeddingInsightViewCodeGenerator/insightViewConfig.d.ts.map +1 -1
  109. package/esm/internal/utils/embeddingInsightViewCodeGenerator/insightViewConfig.js +2 -1
  110. package/esm/internal/utils/propertiesHelper.d.ts +33 -1
  111. package/esm/internal/utils/propertiesHelper.d.ts.map +1 -1
  112. package/esm/internal/utils/propertiesHelper.js +43 -1
  113. package/esm/locales.d.ts +11 -2
  114. package/esm/locales.d.ts.map +1 -1
  115. package/esm/locales.js +5 -2
  116. package/esm/sdk-ui-ext.d.ts +194 -4
  117. package/esm/share/ObjectShareDialog.d.ts +19 -0
  118. package/esm/share/ObjectShareDialog.d.ts.map +1 -1
  119. package/esm/share/ObjectShareDialog.js +8 -1
  120. package/esm/share/objectShareController.helpers.d.ts +8 -52
  121. package/esm/share/objectShareController.helpers.d.ts.map +1 -1
  122. package/esm/share/objectShareController.helpers.js +23 -0
  123. package/esm/share/objectShareController.types.d.ts +76 -3
  124. package/esm/share/objectShareController.types.d.ts.map +1 -1
  125. package/esm/share/useAccessList.d.ts +14 -3
  126. package/esm/share/useAccessList.d.ts.map +1 -1
  127. package/esm/share/useAccessList.js +26 -7
  128. package/esm/share/useApplyObjectPermissions.d.ts +13 -0
  129. package/esm/share/useApplyObjectPermissions.d.ts.map +1 -0
  130. package/esm/share/useApplyObjectPermissions.js +33 -0
  131. package/esm/share/useObjectShareController.d.ts.map +1 -1
  132. package/esm/share/useObjectShareController.js +20 -9
  133. package/esm/share/useObjectShareDialog.d.ts +12 -3
  134. package/esm/share/useObjectShareDialog.d.ts.map +1 -1
  135. package/esm/share/useObjectShareDialog.js +16 -3
  136. package/package.json +24 -25
  137. package/styles/css/main.css +38 -4
  138. package/styles/css/main.css.map +1 -1
  139. package/styles/internal/css/attribute_hierarchies.css.map +1 -1
  140. package/styles/internal/css/charts.css.map +1 -1
  141. package/styles/internal/css/conditional_formatting.css +38 -4
  142. package/styles/internal/css/conditional_formatting.css.map +1 -1
  143. package/styles/internal/css/config_panel.css.map +1 -1
  144. package/styles/internal/css/dashboard_embedding.css.map +1 -1
  145. package/styles/internal/css/headline.css.map +1 -1
  146. package/styles/internal/scss/conditional_formatting.scss +67 -18
@@ -4,10 +4,12 @@ import { useState } from "react";
4
4
  import cx from "classnames";
5
5
  import { isEqual } from "lodash-es";
6
6
  import { useIntl } from "react-intl";
7
- import { Button, Overlay } from "@gooddata/sdk-ui-kit";
7
+ import { v4 as uuid } from "uuid";
8
+ import { Button, Overlay, UiCheckbox, UiIconButton, UiTooltip } from "@gooddata/sdk-ui-kit";
8
9
  import { conditionalFormattingMessages } from "../../../../locales.js";
10
+ import { DisabledBubbleMessage } from "../../DisabledBubbleMessage.js";
9
11
  import { CfSelect } from "./CfSelect.js";
10
- import { findTargetOption, isDateTarget, isRuleComplete, newCondition, ruleWithTarget, sanitizeRuleForEditing, targetIcon, } from "./conditionalFormattingModel.js";
12
+ import { findTargetOption, isDateTarget, isRuleComplete, newCondition, ruleWithTarget, sanitizeRuleForEditing, targetIcon, targetLocalId, } from "./conditionalFormattingModel.js";
11
13
  import { ConditionEditor } from "./ConditionEditor.js";
12
14
  import { ReorderList } from "./ReorderList.js";
13
15
  // The config panel sits on the right edge of the screen, so open the popover to the LEFT of the
@@ -16,31 +18,85 @@ const POPOVER_ALIGN_POINTS = [
16
18
  { align: "cl cr", offset: { x: -5, y: 0 } },
17
19
  { align: "cr cl", offset: { x: 5, y: 0 } },
18
20
  ];
21
+ // Small "?" info affordance next to the "Use default rule" checkbox. A real `<button>` anchor (not
22
+ // the older BubbleHoverTrigger/Bubble pair, which only wires mouse enter/leave and never forwards
23
+ // focus-related props to its span) so keyboard/screen-reader users can reach it via Tab too; the
24
+ // full text lives in the button's own aria-label, so the floating tooltip is purely visual.
25
+ function InfoTooltip({ text }) {
26
+ return (_jsx(UiTooltip, { anchor: _jsx("button", { type: "button", className: "gd-cf-dialog__use-default-info gd-icon-circle-question", "aria-label": text }), content: text, accessibilityHidden: true, triggerBy: ["hover", "focus"], arrowPlacement: "right" }));
27
+ }
19
28
  /**
20
29
  * @internal
21
30
  */
22
- export function ConditionalFormattingDialog({ rule: initialRule, isNew, targetOptions, separators, dateFilterOptions, dateSettings, alignTo, readOnly = false, fixedTarget = false, onSave, onClose, }) {
31
+ export function ConditionalFormattingDialog({ rule: initialRule, isNew, targetOptions, separators, dateFilterOptions, dateSettings, alignTo, fixedTarget = false, semanticByTarget, onDelete, onRevertToDefault, onSave, onClose, }) {
23
32
  const intl = useIntl();
24
- const initial = readOnly
25
- ? initialRule
26
- : sanitizeRuleForEditing(initialRule, isDateTarget(findTargetOption(targetOptions, initialRule.target)));
33
+ const initial = sanitizeRuleForEditing(initialRule, isDateTarget(findTargetOption(targetOptions, initialRule.target)));
27
34
  const [rule, setRule] = useState(initial);
28
- const updateCondition = (id, next) => setRule((current) => ({
29
- ...current,
30
- conditions: current.conditions.map((condition) => (condition.id === id ? next : condition)),
31
- }));
32
- const removeCondition = (id) => setRule((current) => ({
33
- ...current,
34
- conditions: current.conditions.filter((condition) => condition.id !== id),
35
- }));
36
- const addCondition = () => setRule((current) => ({
37
- ...current,
38
- conditions: [
39
- ...current.conditions,
40
- newCondition(isDateTarget(findTargetOption(targetOptions, current.target))),
41
- ],
42
- }));
43
- const reorderConditions = (conditions) => setRule((current) => ({ ...current, conditions }));
35
+ // The blank multi-target Add-Rule flow (isNew, no fixedTarget) must keep its target picker fully
36
+ // usable — starting "Use default rule" checked there just because targetOptions[0] happens to
37
+ // have a semantic entry would lock the very picker the user needs to choose a different target.
38
+ // Editing an EXISTING rule (isNew false) never starts locked regardless of fixedTarget, so this
39
+ // only needs to suppress the brand-new-blank-rule case specifically.
40
+ const activeSemantic = isNew && !fixedTarget ? undefined : semanticByTarget?.[targetLocalId(rule.target)];
41
+ // Only meaningful the first time the dialog opens: a target with no authored rule yet (isNew)
42
+ // and a semantic default to fall back to starts "on" (Inherited); retargeting mid-authoring
43
+ // never revisits this, matching `ruleWithTarget`'s existing "values reset, mode doesn't" behavior.
44
+ const [wantsDefault, setWantsDefault] = useState(() => isNew && activeSemantic !== undefined);
45
+ // A target's semantic default can disappear while this stays open and checked (the catalog
46
+ // object's rule was deleted elsewhere, or a data-view blip dropped the field) — the checkbox
47
+ // itself vanishes with it (rendered only when `activeSemantic` exists), which would otherwise
48
+ // leave fields silently locked with no visible way to unlock them, and Save would revert/delete
49
+ // against a default that no longer exists. Adjusting `wantsDefault` itself here (during render,
50
+ // not in an effect, so there's never a paint where the checkbox is still stale) rather than only
51
+ // deriving `usingDefault` as false makes the unlock permanent: if the default later reappears
52
+ // while the user has since edited the now-unlocked fields, it must not silently re-lock and
53
+ // discard that edit via Save's revert branch.
54
+ if (activeSemantic === undefined && wantsDefault) {
55
+ setWantsDefault(false);
56
+ }
57
+ const usingDefault = wantsDefault && activeSemantic !== undefined;
58
+ const locked = usingDefault;
59
+ // While using the default, always render the LIVE semantic conditions (never a stale draft);
60
+ // the user's own in-progress edits stay in `rule` untouched so unchecking restores them exactly.
61
+ const displayRule = usingDefault && activeSemantic ? { ...rule, conditions: activeSemantic.conditions } : rule;
62
+ // Tracks whether `rule.conditions` is still just a mirror of the semantic default (safe to
63
+ // silently resync/discard) versus genuinely the user's own content (an existing authored rule,
64
+ // or anything they've edited since) — which must never be clobbered by a later resync. NOT
65
+ // derivable from `isDirty` two lines below: `isDirty` is unconditionally true whenever `isNew`
66
+ // (nothing to compare against yet), but a fresh isNew dialog that opens already checked can be
67
+ // unchecked before any edit — `draftIsDefault` must still read true there (to reseed from the
68
+ // live default) while `isDirty` already reads true for an unrelated reason. `setConditions`
69
+ // below sets both together, so a future condition-mutator can't add itself while forgetting
70
+ // to flip this — the only way to touch `rule.conditions` here goes through it.
71
+ const [draftIsDefault, setDraftIsDefault] = useState(() => isNew);
72
+ const setConditions = (updater) => {
73
+ setDraftIsDefault(false);
74
+ setRule(updater);
75
+ };
76
+ const updateCondition = (id, next) => {
77
+ setConditions((current) => ({
78
+ ...current,
79
+ conditions: current.conditions.map((condition) => (condition.id === id ? next : condition)),
80
+ }));
81
+ };
82
+ const removeCondition = (id) => {
83
+ setConditions((current) => ({
84
+ ...current,
85
+ conditions: current.conditions.filter((condition) => condition.id !== id),
86
+ }));
87
+ };
88
+ const addCondition = () => {
89
+ setConditions((current) => ({
90
+ ...current,
91
+ conditions: [
92
+ ...current.conditions,
93
+ newCondition(isDateTarget(findTargetOption(targetOptions, current.target))),
94
+ ],
95
+ }));
96
+ };
97
+ const reorderConditions = (conditions) => {
98
+ setConditions((current) => ({ ...current, conditions }));
99
+ };
44
100
  const changeTarget = (value) => {
45
101
  const option = targetOptions.find((candidate) => candidate.value === value);
46
102
  if (!option) {
@@ -54,11 +110,23 @@ export function ConditionalFormattingDialog({ rule: initialRule, isNew, targetOp
54
110
  const suggestions = selectedTarget?.elements ?? [];
55
111
  const complete = isRuleComplete(rule, selectedTarget?.date);
56
112
  const isDirty = isNew || !isEqual(rule, initial);
57
- const title = intl.formatMessage(readOnly
58
- ? conditionalFormattingMessages.dialogViewTitle
59
- : isNew
113
+ // A target with a semantic default reads as a plain "Rule" regardless of which side of the
114
+ // checkbox it's on — the Add/Edit split only applies to targets with no default to switch to.
115
+ const title = intl.formatMessage(activeSemantic === undefined
116
+ ? isNew
60
117
  ? conditionalFormattingMessages.dialogAddTitle
61
- : conditionalFormattingMessages.dialogEditTitle);
118
+ : conditionalFormattingMessages.dialogEditTitle
119
+ : conditionalFormattingMessages.dialogRuleTitle);
120
+ // Nothing authored yet to delete while still on the default — the disabled state points the
121
+ // user at the (separate) rule-list toggle instead, per the mock's tooltip. Also disabled whenever
122
+ // the caller hasn't actually given us a way to delete anything: `onDelete` is only real when the
123
+ // caller (Section today) found an authored rule for this dialog's synthetic-or-real id, so its
124
+ // absence must disable the button too, not just leave it clickable-but-inert.
125
+ const deleteDisabled = isNew || !onDelete;
126
+ // The blank multi-target Add-Rule flow (isNew, no fixedTarget) has nothing to show a delete
127
+ // affordance for at all — everywhere else (a semantic-fallback view, or an existing authored
128
+ // rule) shows one, disabled per `deleteDisabled` above when there's genuinely nothing to delete.
129
+ const showDelete = fixedTarget || !isNew;
62
130
  return (_jsx(Overlay, { alignTo: alignTo, alignPoints: POPOVER_ALIGN_POINTS, closeOnParentScroll: true, closeOnMouseDrag: true, onClose: onClose, children: _jsxs("div", { className: "gd-cf-dialog", "aria-label": title, children: [
63
131
  _jsxs("div", { className: "gd-cf-dialog__header", children: [
64
132
  _jsx("h3", { className: "gd-cf-dialog__title", children: title }), _jsx(Button, { className: "gd-button-link gd-button-icon-only gd-icon-cross", accessibilityConfig: {
@@ -67,15 +135,57 @@ export function ConditionalFormattingDialog({ rule: initialRule, isNew, targetOp
67
135
  ] }), _jsxs("div", { className: "gd-cf-dialog__body", children: [
68
136
  _jsx("span", { className: "gd-cf-dialog__label", children: intl.formatMessage(conditionalFormattingMessages.dialogTarget) }), singleTarget ? (_jsxs("div", { className: "gd-cf-dialog__static-target", children: [
69
137
  _jsx("span", { className: cx("gd-cf-type-icon", targetIcon(singleTarget.target.kind, isDateTarget(singleTarget))), "aria-hidden": "true" }), _jsx("span", { className: "gd-cf-dialog__static-target-title", children: singleTarget.title })
70
- ] })) : (_jsx("fieldset", { className: "gd-cf-dialog__target-picker", disabled: readOnly, children: _jsx(CfSelect, { value: selectedTarget?.value, items: targetOptions.map((option) => ({
138
+ ] })) : (_jsx("fieldset", { className: "gd-cf-dialog__target-picker", disabled: locked, children: _jsx(CfSelect, { value: selectedTarget?.value, items: targetOptions.map((option) => ({
71
139
  value: option.value,
72
140
  title: option.title,
73
141
  icon: targetIcon(option.target.kind, isDateTarget(option)),
74
- })), onSelect: changeTarget, placeholder: intl.formatMessage(conditionalFormattingMessages.dialogSelectTarget) }) })), _jsxs("fieldset", { disabled: readOnly, className: cx("gd-cf-dialog__editable", {
75
- "gd-cf-dialog__editable--readonly": readOnly,
76
- }), children: [_jsx(ReorderList, { items: rule.conditions, getKey: (condition) => condition.id, onReorder: reorderConditions, disabled: readOnly, renderItem: (condition, slot) => (_jsx(ConditionEditor, { condition: condition, kind: rule.target.kind, isPercent: isPercent, separators: separators, suggestions: suggestions, date: selectedTarget?.date, dateFilterOptions: dateFilterOptions, dateSettings: dateSettings, removable: rule.conditions.length > 1, slot: slot, readOnly: readOnly, onChange: (next) => updateCondition(condition.id, next), onRemove: () => removeCondition(condition.id) })) }), _jsx(Button, { className: "gd-button-secondary gd-cf-dialog__add-condition", iconLeft: "gd-icon-plus", value: intl.formatMessage(conditionalFormattingMessages.dialogAddCondition), onClick: addCondition })
77
- ] })
78
- ] }), _jsxs("div", { className: "gd-cf-dialog__footer", children: [
79
- _jsx(Button, { className: "gd-button-secondary", value: intl.formatMessage(conditionalFormattingMessages.dialogCancel), onClick: onClose }), readOnly ? null : (_jsx(Button, { className: "gd-button-action", value: intl.formatMessage(conditionalFormattingMessages.dialogSave), disabled: !complete || !isDirty, onClick: () => onSave(rule) }))] })
142
+ })), onSelect: changeTarget, placeholder: intl.formatMessage(conditionalFormattingMessages.dialogSelectTarget) }) })), activeSemantic ? (_jsxs("div", { className: "gd-cf-dialog__use-default", children: [
143
+ _jsx(UiCheckbox, { checked: usingDefault, onChange: (e) => {
144
+ const nextUseDefault = e.target.checked;
145
+ // Reseed from the live semantic value exactly at the moment of
146
+ // unchecking but only while the draft is still just a mirror of
147
+ // the default (draftIsDefault): an existing authored rule, or
148
+ // anything the user has actually edited, must never be clobbered.
149
+ // This still fixes the "fresher payload arrived while this stayed
150
+ // checked" case, since fields are locked (nothing to edit) for as
151
+ // long as it's checked, so draftIsDefault can't have gone false then.
152
+ if (!nextUseDefault && activeSemantic && draftIsDefault) {
153
+ // Sanitize like any other externally-authored rule: a
154
+ // semantic condition can carry an operator/value shape this
155
+ // editor doesn't support (e.g. from an AAC-authored default),
156
+ // which would otherwise dead-end with Save disabled.
157
+ setRule((current) => sanitizeRuleForEditing({ ...current, conditions: activeSemantic.conditions }, isDateTarget(selectedTarget)));
158
+ }
159
+ setWantsDefault(nextUseDefault);
160
+ }, label: intl.formatMessage(conditionalFormattingMessages.dialogUseDefaultRule) }), _jsx(InfoTooltip, { text: intl.formatMessage(conditionalFormattingMessages.dialogUseDefaultRuleInfo) })
161
+ ] })) : null, _jsxs("fieldset", { disabled: locked, className: cx("gd-cf-dialog__editable", {
162
+ "gd-cf-dialog__editable--readonly": locked,
163
+ }), children: [_jsx(ReorderList, { items: displayRule.conditions, getKey: (condition) => condition.id, onReorder: reorderConditions, disabled: locked, renderItem: (condition, slot) => (_jsx(ConditionEditor, { condition: condition, kind: rule.target.kind, isPercent: isPercent, separators: separators, suggestions: suggestions, date: selectedTarget?.date, dateFilterOptions: dateFilterOptions, dateSettings: dateSettings, removable: displayRule.conditions.length > 1, slot: slot, readOnly: locked, onChange: (next) => updateCondition(condition.id, next), onRemove: () => removeCondition(condition.id) })) }), locked ? null : (_jsx(Button, { className: "gd-button-secondary gd-cf-dialog__add-condition", iconLeft: "gd-icon-plus", value: intl.formatMessage(conditionalFormattingMessages.dialogAddCondition), onClick: addCondition }))] })
164
+ ] }), _jsxs("div", { className: "gd-cf-dialog__footer", children: [showDelete ? (_jsx(DisabledBubbleMessage, { className: "gd-cf-dialog__delete", showDisabledMessage: deleteDisabled, messageId: conditionalFormattingMessages.dialogRuleCannotBeDeleted.id, children: _jsx(UiIconButton, { icon: "trash", size: "small", variant: "tertiary", isDesctructive: true, isDisabled: deleteDisabled, label: intl.formatMessage(conditionalFormattingMessages.ruleDelete), onClick: () => {
165
+ onDelete?.();
166
+ onClose();
167
+ } }) })) : null, _jsx(Button, { className: "gd-button-secondary", value: intl.formatMessage(conditionalFormattingMessages.dialogCancel), onClick: onClose }), _jsx(Button, { className: "gd-button-action", value: intl.formatMessage(conditionalFormattingMessages.dialogSave), disabled: usingDefault ? false : !complete || !isDirty, onClick: () => {
168
+ if (usingDefault) {
169
+ // Revert (not delete-this-one): clears every rule already stored
170
+ // against the CURRENT target, plus this rule's own id — needed
171
+ // together because a retarget-then-revert (no fixedTarget) means
172
+ // the rule being edited may still sit under its OLD target in
173
+ // `rules`, not yet the one now selected in this draft. Gated on
174
+ // `isNew` alone, deliberately NOT on `deleteDisabled` — `onDelete` and
175
+ // `onRevertToDefault` are independent optional props, and tying revert
176
+ // eligibility to whether a delete callback happens to be provided
177
+ // would silently skip a valid revert for a caller that only wired one.
178
+ if (!isNew) {
179
+ onRevertToDefault?.(rule.target, rule.id);
180
+ }
181
+ onClose();
182
+ return;
183
+ }
184
+ // A dialog opened straight from a semantic row (`semanticRuleFor`) still
185
+ // carries that display-only synthetic id — give it a real one the instant
186
+ // it's actually persisted, exactly like every other freshly authored rule.
187
+ onSave(isNew ? { ...rule, id: uuid() } : rule);
188
+ } })
189
+ ] })
80
190
  ] }) }));
81
191
  }
@@ -14,6 +14,24 @@ export interface IConditionalFormattingSectionProps {
14
14
  separators?: ISeparators;
15
15
  isLoading?: boolean;
16
16
  pushData?: (data: unknown) => void;
17
+ /**
18
+ * Whether insight-level (authored) rules can apply at all — mirrors
19
+ * `isConditionalFormattingEnabled` in PluggablePivotTableNext.tsx. When `false`, the master
20
+ * toggle, "Add rule" button, and any authored rule list are hidden entirely: those controls edit
21
+ * `properties.controls.conditionalFormatting`, which the engine ignores while this is off, so
22
+ * showing them would let a user "author" formatting that silently never paints. The semantic
23
+ * block is unaffected — it has its own independent flag below. Defaults to `true` (existing
24
+ * behavior): most callers only ever render this section when insight-level CF is on.
25
+ */
26
+ enableConditionalFormatting?: boolean;
27
+ /**
28
+ * Mirrors the engine's own `enableSemanticConditionalFormatting` (set in
29
+ * PluggablePivotTableNext.tsx from the same setting, by design, today) — a distinct prop rather
30
+ * than an assumption baked into this component, so the "From semantic layer" block can't drift
31
+ * out of sync with what the table actually paints if semantic inheritance ever gets its own flag.
32
+ * Defaults to `false`: never show inherited rows the caller hasn't confirmed the engine will paint.
33
+ */
34
+ enableSemanticConditionalFormatting?: boolean;
17
35
  }
18
- export declare function ConditionalFormattingSection({ properties, propertiesMeta, insight, targetData, backend, workspace, dateSettings, separators, isLoading, pushData }: IConditionalFormattingSectionProps): import("react/jsx-runtime").JSX.Element;
36
+ export declare function ConditionalFormattingSection({ properties, propertiesMeta, insight, targetData, backend, workspace, dateSettings, separators, isLoading, pushData, enableConditionalFormatting, enableSemanticConditionalFormatting }: IConditionalFormattingSectionProps): import("react/jsx-runtime").JSX.Element;
19
37
  //# sourceMappingURL=ConditionalFormattingSection.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ConditionalFormattingSection.d.ts","sourceRoot":"","sources":["../../../../../src/internal/components/configurationControls/conditionalFormatting/ConditionalFormattingSection.tsx"],"names":[],"mappings":"AAQA,OAAO,EAAE,KAAK,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AACpE,OAAO,EACH,KAAK,kBAAkB,EAEvB,KAAK,WAAW,EACnB,MAAM,qBAAqB,CAAC;AAK7B,OAAO,EAAE,KAAK,wBAAwB,EAAE,MAAM,sCAAsC,CAAC;AAIrF,OAAO,EACH,KAAK,eAAe,EACpB,KAAK,aAAa,EAWrB,MAAM,iCAAiC,CAAC;AAYzC,MAAM,WAAW,kCAAkC;IAC/C,UAAU,CAAC,EAAE,wBAAwB,CAAC;IACtC,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACzC,OAAO,CAAC,EAAE,kBAAkB,CAAC;IAC7B,UAAU,CAAC,EAAE,aAAa,CAAC;IAC3B,yFAAyF;IACzF,OAAO,CAAC,EAAE,kBAAkB,CAAC;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,eAAe,CAAC;IAC/B,UAAU,CAAC,EAAE,WAAW,CAAC;IACzB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;CACtC;AA8GD,wBAAgB,4BAA4B,CAAC,EACzC,UAAU,EACV,cAAc,EACd,OAAO,EACP,UAAU,EACV,OAAO,EACP,SAAS,EACT,YAAY,EACZ,UAAU,EACV,SAAS,EACT,QAAQ,EACX,EAAE,kCAAkC,2CA+KpC"}
1
+ {"version":3,"file":"ConditionalFormattingSection.d.ts","sourceRoot":"","sources":["../../../../../src/internal/components/configurationControls/conditionalFormatting/ConditionalFormattingSection.tsx"],"names":[],"mappings":"AAQA,OAAO,EAAE,KAAK,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AACpE,OAAO,EACH,KAAK,kBAAkB,EAEvB,KAAK,WAAW,EACnB,MAAM,qBAAqB,CAAC;AAK7B,OAAO,EAAE,KAAK,wBAAwB,EAAE,MAAM,sCAAsC,CAAC;AAIrF,OAAO,EACH,KAAK,eAAe,EACpB,KAAK,aAAa,EAcrB,MAAM,iCAAiC,CAAC;AAYzC,MAAM,WAAW,kCAAkC;IAC/C,UAAU,CAAC,EAAE,wBAAwB,CAAC;IACtC,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACzC,OAAO,CAAC,EAAE,kBAAkB,CAAC;IAC7B,UAAU,CAAC,EAAE,aAAa,CAAC;IAC3B,yFAAyF;IACzF,OAAO,CAAC,EAAE,kBAAkB,CAAC;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,eAAe,CAAC;IAC/B,UAAU,CAAC,EAAE,WAAW,CAAC;IACzB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;IACnC;;;;;;;;OAQG;IACH,2BAA2B,CAAC,EAAE,OAAO,CAAC;IACtC;;;;;;OAMG;IACH,mCAAmC,CAAC,EAAE,OAAO,CAAC;CACjD;AAkJD,wBAAgB,4BAA4B,CAAC,EACzC,UAAU,EACV,cAAc,EACd,OAAO,EACP,UAAU,EACV,OAAO,EACP,SAAS,EACT,YAAY,EACZ,UAAU,EACV,SAAS,EACT,QAAQ,EACR,2BAAkC,EAClC,mCAA2C,EAC9C,EAAE,kCAAkC,2CAkSpC"}
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  // (C) 2026 GoodData Corporation
3
3
  import { useState } from "react";
4
4
  import cx from "classnames";
@@ -8,7 +8,7 @@ import { Button, UiIconButton } from "@gooddata/sdk-ui-kit";
8
8
  import { conditionalFormattingMessages } from "../../../../locales.js";
9
9
  import { ConfigSection } from "../ConfigSection.js";
10
10
  import { ConditionalFormattingDialog } from "./ConditionalFormattingDialog.js";
11
- import { buildTargetOptions, findTargetOption, isCustomTarget, isDateTarget, isRuleComplete, newRule, semanticRuleFor, targetIcon, targetLocalId, } from "./conditionalFormattingModel.js";
11
+ import { buildTargetOptions, findTargetOption, isCustomTarget, isDateTarget, isRuleComplete, isSuppressedTarget, newRule, semanticRuleFor, targetIcon, targetLocalId, targetToValue, withSuppressedTargets, } from "./conditionalFormattingModel.js";
12
12
  import { ReorderList } from "./ReorderList.js";
13
13
  import { useCfDateFilterOptions } from "./useCfDateFilterOptions.js";
14
14
  const SECTION_ID = "conditionalFormatting_section";
@@ -20,37 +20,50 @@ function RuleChip({ rule, option, invalid, editable, labels, slot, onEdit, onDel
20
20
  }
21
21
  // A held-open semantic dialog is a snapshot from when "View" was clicked — re-derive it against the
22
22
  // current semantic payload every render so it tracks edits, and disappears (rather than showing stale
23
- // or unrelated rules) once loading clears `targetData.semantic`, the target stops being Inherited (its
24
- // row left `semanticRows` because it became Custom), or its semantic rule itself is no longer present.
25
- function resolveActiveDialog(dialog, semanticMap, config) {
23
+ // or unrelated rules) once the target stops being Inherited (an authored rule appeared elsewhere,
24
+ // making the target genuinely Custom) or the target itself is no longer part of the insight at all
25
+ // (removed from its bucket — there's nothing left for a Save to even target). Deliberately does NOT
26
+ // also close it just because the semantic DEFAULT vanished while the target itself is still there
27
+ // (catalog rule removed, a data-view blip) — the dialog's own wantsDefault-recovery logic
28
+ // (ConditionalFormattingDialog.tsx) already handles that gracefully (unlocks, falls back to whatever's
29
+ // in the draft); unmounting it here first would discard an edit made after unchecking "Use default
30
+ // rule" before that logic ever runs. `targetData.semantic` is kept stable across loading transitions
31
+ // so isCustomTarget/target-presence below are the only things that fire on a re-execution, not every
32
+ // one of them.
33
+ function resolveActiveDialog(dialog, semanticMap, config, targetOptions) {
26
34
  if (dialog?.kind !== "semantic") {
27
35
  return dialog;
28
36
  }
29
37
  if (isCustomTarget(config, dialog.option.target)) {
30
38
  return null;
31
39
  }
40
+ const value = targetToValue(dialog.option.target);
41
+ if (!targetOptions.some((option) => targetToValue(option.target) === value)) {
42
+ return null;
43
+ }
32
44
  const current = semanticMap[targetLocalId(dialog.option.target)];
33
45
  return current
34
46
  ? { kind: "semantic", option: dialog.option, rule: semanticRuleFor(dialog.option, current) }
35
- : null;
47
+ : dialog;
36
48
  }
37
- // `activeDialog.rule.id` alone is stable across a semantic-rule content change (it's target-based, not
38
- // content-based, per semanticRuleFor) ConditionalFormattingDialog seeds its local draft from `rule` via
39
- // useState and won't re-sync on a prop change, so without remounting on content change too, a rule that
40
- // changes while the (read-only) dialog is open would keep showing what it looked like when opened.
49
+ // Just the (target-based, per semanticRuleFor) id no need to key on content too: the dialog is
50
+ // handed the live `semanticByTarget` map and re-derives the current semantic conditions from it every
51
+ // render (via `displayRule`), so a semantic-rule update while the dialog is open shows up without a
52
+ // remount. Keying on content as well would risk discarding an in-progress unchecked-and-editing draft
53
+ // if fresh data happened to arrive mid-edit.
41
54
  function dialogKey(dialog) {
42
- return dialog.kind === "semantic"
43
- ? `${dialog.rule.id}:${JSON.stringify(dialog.rule.conditions)}`
44
- : dialog.rule.id;
55
+ return dialog.rule.id;
45
56
  }
46
- // Read-only: no drag handle, no delete nothing on the row is editable here, only the View
47
- // button opens the (readOnly) dialog. Turning it off/customizing it is PR-6, not this one.
48
- function SemanticRuleRow({ option, viewLabel, onView }) {
49
- return (_jsxs("div", { className: "gd-cf-rule gd-cf-rule--semantic", children: [
50
- _jsx("span", { className: `gd-cf-type-icon ${targetIcon(option.target.kind, isDateTarget(option))}`, "aria-hidden": "true" }), _jsx("span", { className: "gd-cf-rule__title", children: option.title }), _jsx(Button, { className: "gd-button-link gd-cf-rule__view", value: viewLabel, onClick: onView })
51
- ] }));
57
+ // No drag handle, no delete row here the target's own dialog (opened via Edit) carries the
58
+ // "Use default rule" checkbox that switches it to Custom. The row itself only ever offers the
59
+ // on/off toggle (`suppressedTargets` suppression) and, while on, a way to open that dialog.
60
+ function SemanticRuleRow({ option, editLabel, toggleAriaLabel, off, onToggleOff, onEdit, }) {
61
+ return (_jsxs("div", { className: cx("gd-cf-rule", "gd-cf-rule--semantic", { "gd-cf-rule--off": off }), children: [
62
+ _jsxs("label", { className: "input-checkbox-toggle s-checkbox-toggle-label gd-cf-rule__toggle", children: [
63
+ _jsx("input", { "aria-label": toggleAriaLabel, type: "checkbox", checked: !off, onChange: (e) => onToggleOff(!e.target.checked), className: `s-checkbox-toggle ${off ? "s-disabled" : "s-enabled"}` }), _jsx("span", { className: "input-label-text" })
64
+ ] }), _jsx("span", { className: `gd-cf-type-icon ${targetIcon(option.target.kind, isDateTarget(option))}`, "aria-hidden": "true" }), _jsx("span", { className: "gd-cf-rule__title", children: option.title }), off ? null : (_jsx(Button, { className: "gd-button-link gd-cf-rule__view", value: editLabel, onClick: onEdit }))] }));
52
65
  }
53
- export function ConditionalFormattingSection({ properties, propertiesMeta, insight, targetData, backend, workspace, dateSettings, separators, isLoading, pushData, }) {
66
+ export function ConditionalFormattingSection({ properties, propertiesMeta, insight, targetData, backend, workspace, dateSettings, separators, isLoading, pushData, enableConditionalFormatting = true, enableSemanticConditionalFormatting = false, }) {
54
67
  const intl = useIntl();
55
68
  const [dialog, setDialog] = useState(null);
56
69
  const config = properties?.controls?.["conditionalFormatting"];
@@ -59,16 +72,36 @@ export function ConditionalFormattingSection({ properties, propertiesMeta, insig
59
72
  const targetOptions = insight ? buildTargetOptions(insight, targetData) : [];
60
73
  // Only date pickers consume the catalog — skip the backend query when no target is date-eligible.
61
74
  const dateFilterOptions = useCfDateFilterOptions(backend, workspace, targetOptions.some(isDateTarget));
62
- // Targets inheriting a semantic-layer rule, minus any already Custom (an authored rule or a
63
- // customTargets entry). NOT gated on the toggle above: the engine's `enabled` only ever
64
- // deactivates the insight's OWN authored rules — an untouched (Inherited) target keeps
65
- // painting regardless, so hiding it here would desync the panel from what's on screen.
66
75
  const semanticMap = targetData?.semantic ?? {};
67
- const semanticRows = targetOptions.flatMap((option) => {
68
- const rule = semanticMap[targetLocalId(option.target)];
69
- return !rule || isCustomTarget(config, option.target) ? [] : [{ option, semantic: rule }];
70
- });
71
- const activeDialog = resolveActiveDialog(dialog, semanticMap, config);
76
+ // Targets with a semantic-layer rule, minus any that have their own authored rule (those render
77
+ // as a RuleChip above instead). A `suppressedTargets`-suppressed target still renders here (tagged
78
+ // `off`) it needs a row to turn back on. NOT gated on the toggle above: the engine's `enabled`
79
+ // only ever deactivates the insight's OWN authored rules — an untouched (Inherited) target keeps
80
+ // painting regardless, so hiding it here would desync the panel from what's on screen. IS gated
81
+ // on `enableSemanticConditionalFormatting`, for the opposite reason: an inherited row the engine
82
+ // won't actually paint (flag off) would be the desync the paragraph above is trying to avoid. ALSO
83
+ // gated on `semanticFresh`: unlike an already-open dialog (which the user is already looking at
84
+ // and which the dialog's own recovery logic protects), a brand-new row opened from a stale map
85
+ // could let the user View/suppress/customize a rule that's about to be superseded — offer no new
86
+ // ones while the map is confirmed stale, rather than only guarding the persistence side.
87
+ const semanticRows = enableSemanticConditionalFormatting && targetData?.semanticFresh !== false
88
+ ? targetOptions.flatMap((option) => {
89
+ const rule = semanticMap[targetLocalId(option.target)];
90
+ const value = targetToValue(option.target);
91
+ // When insight-level CF is off, the engine never sees `rules` at all (the renderer
92
+ // passes it `undefined`) — every target resolves as Inherited regardless of what's
93
+ // still sitting in persisted properties, so a stale authored rule must not hide the
94
+ // semantic row here either, or the panel would show nothing for a target the table
95
+ // is actually painting.
96
+ const hasAuthoredRule = enableConditionalFormatting &&
97
+ rules.some((existing) => targetToValue(existing.target) === value);
98
+ if (!rule || hasAuthoredRule) {
99
+ return [];
100
+ }
101
+ return [{ option, semantic: rule, off: isSuppressedTarget(config, option.target) }];
102
+ })
103
+ : [];
104
+ const activeDialog = resolveActiveDialog(dialog, semanticMap, config, targetOptions);
72
105
  // `resolveActiveDialog` only hides an invalidated semantic dialog from render — without also
73
106
  // clearing the underlying state here, a later data view that happens to carry a rule for the same
74
107
  // (possibly stale) target would silently reopen it, with no new View click.
@@ -79,13 +112,48 @@ export function ConditionalFormattingSection({ properties, propertiesMeta, insig
79
112
  // it's unknown — don't flash a false "Invalid" badge, don't author a plain-text rule on a date attr.
80
113
  const targetDataReady = targetData?.dates !== undefined;
81
114
  const canAddRule = !isLoading && targetOptions.length > 0 && targetDataReady;
82
- const commit = (rulesNext, enabledNext) => {
115
+ const commit = (rulesNext, enabledNext, suppressedTargetsNext = config?.suppressedTargets) => {
116
+ // An entry with no live semantic rule behind it (the catalog's rule was itself removed or
117
+ // detached elsewhere) suppresses nothing and can never resurface in the panel to be turned
118
+ // back on — drop it opportunistically on any edit rather than leaving it to accumulate
119
+ // forever. Guarded three ways: before the first data view, an empty/absent map means "unknown
120
+ // yet", not "confirmed gone"; `semanticFresh === false` means the map still belongs to a
121
+ // superseded execution (deliberately NOT gated on `isLoading` instead, which can already read
122
+ // false before a fresh data view actually lands, or never, if that execution fails — the gap
123
+ // `semanticFresh` is specifically tracked to close, see ICfTargetData's doc); and a target
124
+ // temporarily missing from `targetOptions` itself (e.g. removed from the bucket, not from the
125
+ // catalog) is left untouched — its semantic entry being absent from THIS execution says
126
+ // nothing about whether its catalog rule still exists, only that it wasn't queried this time.
127
+ const prunedSuppressedTargets = targetData?.semanticFresh === false || targetData?.semantic === undefined
128
+ ? suppressedTargetsNext
129
+ : suppressedTargetsNext?.filter((target) => {
130
+ const value = targetToValue(target);
131
+ const inCurrentExecution = targetOptions.some((option) => targetToValue(option.target) === value);
132
+ return !inCurrentExecution || semanticMap[targetLocalId(target)] !== undefined;
133
+ });
83
134
  // Spread the existing config so version (and any future cross-stack fields) survive an edit.
84
- const next = { ...config, enabled: enabledNext, rules: rulesNext };
135
+ const next = withSuppressedTargets({ ...config, enabled: enabledNext, rules: rulesNext }, prunedSuppressedTargets);
85
136
  const cloned = cloneDeep(properties ?? {});
86
137
  set(cloned, "controls.conditionalFormatting", next);
87
138
  pushData?.({ properties: cloned });
88
139
  };
140
+ // The one place that edits `suppressedTargets` membership for a single target — every other commit
141
+ // path that touches it (turning suppression on/off, reverting to Inherited, deleting a rule that
142
+ // leaves a stale entry behind) funnels through here instead of hand-rolling the same add/remove
143
+ // filter, so `rulesNext` is the only thing each caller still has to think about for itself.
144
+ const setTargetMode = (target, mode, rulesNext = rules) => {
145
+ const value = targetToValue(target);
146
+ const suppressedTargets = config?.suppressedTargets ?? [];
147
+ const suppressedTargetsNext = mode === "off"
148
+ ? suppressedTargets.some((existing) => targetToValue(existing) === value)
149
+ ? suppressedTargets
150
+ : [...suppressedTargets, target]
151
+ : suppressedTargets.filter((existing) => targetToValue(existing) !== value);
152
+ commit(rulesNext, enabled, suppressedTargetsNext);
153
+ };
154
+ // Turn a still-Inherited target off (Custom-with-zero-rules, per the engine's suppression
155
+ // convention) or back on — never touches `rules`, only `suppressedTargets` membership.
156
+ const setSuppressed = (target, suppressed) => setTargetMode(target, suppressed ? "off" : "inherited");
89
157
  const saveRule = (rule, isNew) => {
90
158
  const rulesNext = isNew
91
159
  ? [...rules, rule]
@@ -95,31 +163,63 @@ export function ConditionalFormattingSection({ properties, propertiesMeta, insig
95
163
  commit(rulesNext, isNew ? true : enabled);
96
164
  setDialog(null);
97
165
  };
98
- const deleteRule = (id) => commit(rules.filter((rule) => rule.id !== id), enabled);
166
+ // Also strips the deleted rule's target from `suppressedTargets`, in case it's stale-listed there
167
+ // too (e.g. externally-authored data, or a target once suppressed then later given its own
168
+ // rule without cleanup) — otherwise the target would stay Custom-with-zero-rules (Off) instead
169
+ // of reverting to Inherited once its last rule is gone, contradicting decision 4's "free
170
+ // consequence" and the "Use default rule" revert flow this also backs.
171
+ const deleteRule = (id) => {
172
+ const rulesNext = rules.filter((rule) => rule.id !== id);
173
+ const target = rules.find((rule) => rule.id === id)?.target;
174
+ if (target) {
175
+ setTargetMode(target, "inherited", rulesNext);
176
+ }
177
+ else {
178
+ commit(rulesNext, enabled);
179
+ }
180
+ };
181
+ // Distinct from `deleteRule`: a target can have several stacked custom rules (the Add flow
182
+ // doesn't prevent authoring more than one against the same target), so reverting it to
183
+ // Inherited must clear ALL of them — deleting just the one open in the dialog would leave the
184
+ // rest still marking the target Custom in `resolvePerTargetConditionalFormatting`. Also clears
185
+ // `ruleId` specifically (regardless of what target it's currently stored under) so a
186
+ // retarget-then-revert doesn't strand the edited rule under its original target.
187
+ const revertToInherited = (target, ruleId) => {
188
+ const value = targetToValue(target);
189
+ const rulesNext = rules.filter((rule) => rule.id !== ruleId && targetToValue(rule.target) !== value);
190
+ setTargetMode(target, "inherited", rulesNext);
191
+ };
99
192
  const chipLabels = {
100
193
  invalid: intl.formatMessage(conditionalFormattingMessages.ruleInvalid),
101
194
  invalidValue: intl.formatMessage(conditionalFormattingMessages.ruleInvalidValue),
102
195
  edit: intl.formatMessage(conditionalFormattingMessages.ruleEdit),
103
196
  delete: intl.formatMessage(conditionalFormattingMessages.ruleDelete),
104
197
  };
105
- const viewLabel = intl.formatMessage(conditionalFormattingMessages.ruleView);
106
- return (_jsxs(ConfigSection, { id: SECTION_ID, className: "gd-cf-section", title: conditionalFormattingMessages.sectionTitle.id, propertiesMeta: propertiesMeta, properties: properties, pushData: pushData, canBeToggled: true, toggledOn: enabled, toggleDisabled: isLoading, onToggle: (checked) => commit(rules, checked), children: [
107
- _jsx("span", { className: "s-cf-popover-anchor gd-cf-section__anchor" }), _jsxs("div", { className: "gd-cf-section__subcategory", children: [
108
- _jsx("span", { className: "gd-cf-section__rules-label", children: intl.formatMessage(conditionalFormattingMessages.rulesLabel) }), _jsx("span", { className: "gd-cf-section__divider" }), _jsx(Button, { className: "gd-button-link gd-cf-section__add", iconLeft: "gd-icon-plus", value: intl.formatMessage(conditionalFormattingMessages.addRule), disabled: !canAddRule, onClick: () => setDialog({ kind: "custom", rule: newRule(targetOptions[0]), isNew: true }) })
109
- ] }), rules.length === 0 ? (
110
- // The empty-state hint would contradict an inherited row rendered just below it.
111
- semanticRows.length === 0 ? (_jsx("div", { className: "gd-cf-section__empty", children: intl.formatMessage(conditionalFormattingMessages.empty) })) : null) : (_jsx("div", { className: "gd-cf-section__rules", children: _jsx(ReorderList, { items: rules, getKey: (rule) => rule.id, onReorder: (next) => commit(next, enabled), renderItem: (rule, slot) => {
112
- const option = findTargetOption(targetOptions, rule.target);
113
- // A missing target flags immediately; value validation waits for date metadata.
114
- const invalid = !option || (targetDataReady && !isRuleComplete(rule, option.date));
115
- return (_jsx(RuleChip, { rule: rule, option: option, invalid: invalid, editable: targetDataReady, labels: chipLabels, slot: slot, onEdit: () => setDialog({ kind: "custom", rule, isNew: false }), onDelete: () => deleteRule(rule.id) }));
116
- } }) })), semanticRows.length > 0 ? (_jsxs("div", { className: "gd-cf-section__semantic", children: [
198
+ const editLabel = intl.formatMessage(conditionalFormattingMessages.ruleEdit);
199
+ const dialogIsNew = activeDialog?.kind === "custom" ? activeDialog.isNew : true;
200
+ // A "semantic" dialog's synthetic id never matches anything in `rules` pass a real callback
201
+ // only when there's an actual rule to delete; the dialog itself (from its own isNew/fixedTarget)
202
+ // decides whether to render a disabled delete affordance for the "nothing authored yet" case,
203
+ // not a no-op we hand it.
204
+ const hasAuthoredRule = activeDialog ? rules.some((rule) => rule.id === activeDialog.rule.id) : false;
205
+ return (_jsxs(ConfigSection, { id: SECTION_ID, className: "gd-cf-section", title: conditionalFormattingMessages.sectionTitle.id, propertiesMeta: propertiesMeta, properties: properties, pushData: pushData, canBeToggled: enableConditionalFormatting, toggledOn: enabled, toggleDisabled: isLoading, onToggle: (checked) => commit(rules, checked), children: [
206
+ _jsx("span", { className: "s-cf-popover-anchor gd-cf-section__anchor" }), enableConditionalFormatting ? (_jsxs(_Fragment, { children: [
207
+ _jsxs("div", { className: "gd-cf-section__subcategory", children: [
208
+ _jsx("span", { className: "gd-cf-section__rules-label", children: intl.formatMessage(conditionalFormattingMessages.rulesLabel) }), _jsx("span", { className: "gd-cf-section__divider" }), _jsx(Button, { className: "gd-button-link gd-cf-section__add", iconLeft: "gd-icon-plus", value: intl.formatMessage(conditionalFormattingMessages.addRule), disabled: !canAddRule, onClick: () => setDialog({ kind: "custom", rule: newRule(targetOptions[0]), isNew: true }) })
209
+ ] }), rules.length === 0 ? (
210
+ // The empty-state hint would contradict an inherited row rendered just below it.
211
+ semanticRows.length === 0 ? (_jsx("div", { className: "gd-cf-section__empty", children: intl.formatMessage(conditionalFormattingMessages.empty) })) : null) : (_jsx("div", { className: "gd-cf-section__rules", children: _jsx(ReorderList, { items: rules, getKey: (rule) => rule.id, onReorder: (next) => commit(next, enabled), renderItem: (rule, slot) => {
212
+ const option = findTargetOption(targetOptions, rule.target);
213
+ // A missing target flags immediately; value validation waits for date metadata.
214
+ const invalid = !option || (targetDataReady && !isRuleComplete(rule, option.date));
215
+ return (_jsx(RuleChip, { rule: rule, option: option, invalid: invalid, editable: targetDataReady, labels: chipLabels, slot: slot, onEdit: () => setDialog({ kind: "custom", rule, isNew: false }), onDelete: () => deleteRule(rule.id) }));
216
+ } }) }))] })) : null, semanticRows.length > 0 ? (_jsxs("div", { className: "gd-cf-section__semantic", children: [
117
217
  _jsxs("div", { className: "gd-cf-section__subcategory", children: [
118
218
  _jsx("span", { className: "gd-cf-section__rules-label", children: intl.formatMessage(conditionalFormattingMessages.semanticLabel) }), _jsx("span", { className: "gd-cf-section__divider" })
119
- ] }), _jsx("div", { className: "gd-cf-section__rules", children: semanticRows.map(({ option, semantic }) => (_jsx(SemanticRuleRow, { option: option, viewLabel: viewLabel, onView: () => setDialog({
219
+ ] }), _jsx("div", { className: "gd-cf-section__rules", children: semanticRows.map(({ option, semantic, off }) => (_jsx(SemanticRuleRow, { option: option, editLabel: editLabel, toggleAriaLabel: intl.formatMessage(conditionalFormattingMessages.semanticToggleAriaLabel, { title: option.title }), off: off, onToggleOff: (nextOff) => setSuppressed(option.target, nextOff), onEdit: () => setDialog({
120
220
  kind: "semantic",
121
221
  rule: semanticRuleFor(option, semantic),
122
222
  option,
123
223
  }) }, option.value))) })
124
- ] })) : null, activeDialog ? (_jsx(ConditionalFormattingDialog, { rule: activeDialog.rule, isNew: activeDialog.kind === "custom" && activeDialog.isNew, readOnly: activeDialog.kind === "semantic", fixedTarget: activeDialog.kind === "semantic", targetOptions: targetOptions, separators: separators, dateFilterOptions: dateFilterOptions, dateSettings: dateSettings, alignTo: ".s-cf-popover-anchor", onSave: (rule) => activeDialog.kind === "custom" && saveRule(rule, activeDialog.isNew), onClose: () => setDialog(null) }, dialogKey(activeDialog))) : null] }));
224
+ ] })) : null, activeDialog ? (_jsx(ConditionalFormattingDialog, { rule: activeDialog.rule, isNew: dialogIsNew, fixedTarget: activeDialog.kind === "semantic", semanticByTarget: semanticMap, targetOptions: targetOptions, separators: separators, dateFilterOptions: dateFilterOptions, dateSettings: dateSettings, alignTo: ".s-cf-popover-anchor", onDelete: hasAuthoredRule ? () => deleteRule(activeDialog.rule.id) : undefined, onRevertToDefault: revertToInherited, onSave: (rule) => saveRule(rule, dialogIsNew), onClose: () => setDialog(null) }, dialogKey(activeDialog))) : null] }));
125
225
  }
@@ -51,13 +51,21 @@ export interface ICfTargetData {
51
51
  dates?: Record<string, ICfDateMeta>;
52
52
  /** Semantic-layer conditional formatting carried on the execution result header, by localId. */
53
53
  semantic?: Record<string, ISemanticConditionalFormatting>;
54
+ /**
55
+ * False while `semantic` still belongs to a superseded execution — distinct from `isLoading`,
56
+ * which can flip back to false before the new data view actually lands (or never, if that
57
+ * execution fails), leaving a gap `isLoading` alone can't see. Set false the instant a new
58
+ * execution starts, true again only once a fresh data view actually rebuilds this object.
59
+ * Undefined (e.g. in tests that construct this directly) is treated as fresh.
60
+ */
61
+ semanticFresh?: boolean;
54
62
  }
55
63
  /**
56
64
  * Extracts {@link ICfTargetData} from the executed data view. Titles must come from here — attribute
57
65
  * titles live only on the result headers, and measures resolve to their final header name; formats
58
66
  * living on metric metadata also resolve only here (insight-level formats win in buildTargetOptions).
59
67
  */
60
- export declare function buildCfTargetData(dataView: DataViewFacade): Required<ICfTargetData>;
68
+ export declare function buildCfTargetData(dataView: DataViewFacade): Required<Omit<ICfTargetData, "semanticFresh">>;
61
69
  export declare function buildTargetOptions(insight: IInsightDefinition, data?: ICfTargetData): ITargetOption[];
62
70
  export declare const findTargetOption: (options: ITargetOption[], target: ConditionalFormattingTarget) => ITargetOption | undefined;
63
71
  /** Date-eligible target: the option carries execution-resolved date metadata. */
@@ -65,17 +73,36 @@ export declare const isDateTarget: (option: ITargetOption | undefined) => boolea
65
73
  export declare const targetLocalId: (target: ConditionalFormattingTarget) => string;
66
74
  /**
67
75
  * Whether `target` is already in Custom mode on this insight config — an insight rule targets it,
68
- * or it's explicitly listed in `customTargets`. Mirrors the `customModeTargets` set built by
76
+ * or it's explicitly listed in `suppressedTargets`. Mirrors the `customModeTargets` set built by
69
77
  * sdk-ui-pivot's `resolvePerTargetConditionalFormatting` (semanticConditionalFormatting.ts); the two
70
78
  * packages don't share this helper, so keep them in sync by hand if either changes. Honoring
71
- * `customTargets` here matters even though this PR never writes it: if some other path already
72
- * marked a target Custom, the semantic block must still hide it as Inherited would be wrong.
79
+ * `suppressedTargets` here matters regardless of source: the Section's own "Turn off" toggle writes
80
+ * it (`setSuppressed`/`setTargetMode`), and it can also arrive from AAC or an older panel version
81
+ * either way, if a target is already marked Custom, the semantic block must still hide it as
82
+ * Inherited would be wrong.
73
83
  */
74
84
  export declare const isCustomTarget: (config: IConditionalFormatting | undefined, target: ConditionalFormattingTarget) => boolean;
75
85
  /**
76
- * Synthesizes a read-only rule for the View dialog from an inherited semantic-layer condition set.
77
- * The id mirrors the engine's own `semantic:<kind>:<localId>` convention (semanticConditionalFormatting.ts)
78
- * for consistency onlyit's never persisted or compared.
86
+ * Whether `target` is explicitly suppressed (Custom-with-zero-rules, per the engine's
87
+ * `resolvePerTargetConditionalFormatting`): listed in `suppressedTargets` with no authored rule.
88
+ * This is the "Turn off" state for an otherwise-Inherited target distinct from an authored-Custom
89
+ * target, which is never in `suppressedTargets` in practice (its own rule already makes it Custom).
90
+ */
91
+ export declare const isSuppressedTarget: (config: IConditionalFormatting | undefined, target: ConditionalFormattingTarget) => boolean;
92
+ /**
93
+ * `suppressedTargets` must be OMITTED entirely when empty, never set to `undefined` — persisting the
94
+ * config as JSON drops an `undefined` value either way, but an in-memory object with an explicit
95
+ * `suppressedTargets: undefined` key is NOT deep-equal to one where the key is simply absent, which
96
+ * creates spurious dirty-state noise in the session before that JSON round-trip ever happens. Shared
97
+ * by every place that builds an `IConditionalFormatting` payload (Section today, a future catalog
98
+ * authoring adapter next) so this normalization can't be forgotten in one of them.
99
+ */
100
+ export declare const withSuppressedTargets: (config: IConditionalFormatting, suppressedTargets: readonly ConditionalFormattingTarget[] | undefined) => IConditionalFormatting;
101
+ /**
102
+ * Synthesizes a rule for the dialog from an inherited semantic-layer condition set, seeding it with
103
+ * a display-only id (mirroring the engine's own `semantic:<kind>:<localId>` convention in
104
+ * semanticConditionalFormatting.ts, for consistency only) — the dialog swaps this for a real uuid()
105
+ * the moment the user customizes and saves, so this synthetic id itself is never persisted.
79
106
  */
80
107
  export declare const semanticRuleFor: (option: ITargetOption, semantic: ISemanticConditionalFormatting) => IConditionalFormattingRule;
81
108
  export declare const operatorsForTarget: (kind: "attribute" | "measure", isDate: boolean) => ConditionalFormattingOperator[];