@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.
- package/NOTICE +175 -19
- package/esm/index.d.ts +4 -2
- package/esm/index.d.ts.map +1 -1
- package/esm/index.js +3 -2
- package/esm/internal/components/configurationControls/conditionalFormatting/ConditionEditor.d.ts.map +1 -1
- package/esm/internal/components/configurationControls/conditionalFormatting/ConditionEditor.js +8 -5
- package/esm/internal/components/configurationControls/conditionalFormatting/ConditionalFormattingDialog.d.ts +20 -5
- package/esm/internal/components/configurationControls/conditionalFormatting/ConditionalFormattingDialog.d.ts.map +1 -1
- package/esm/internal/components/configurationControls/conditionalFormatting/ConditionalFormattingDialog.js +143 -33
- package/esm/internal/components/configurationControls/conditionalFormatting/ConditionalFormattingSection.d.ts +19 -1
- package/esm/internal/components/configurationControls/conditionalFormatting/ConditionalFormattingSection.d.ts.map +1 -1
- package/esm/internal/components/configurationControls/conditionalFormatting/ConditionalFormattingSection.js +146 -46
- package/esm/internal/components/configurationControls/conditionalFormatting/conditionalFormattingModel.d.ts +34 -7
- package/esm/internal/components/configurationControls/conditionalFormatting/conditionalFormattingModel.d.ts.map +1 -1
- package/esm/internal/components/configurationControls/conditionalFormatting/conditionalFormattingModel.js +38 -7
- package/esm/internal/components/configurationPanels/PivotTableConfigurationPanel.d.ts.map +1 -1
- package/esm/internal/components/configurationPanels/PivotTableConfigurationPanel.js +9 -4
- package/esm/internal/components/dialogs/userManagementDialogs/UserGroups/backend/loadUserGroupOptionsPromise.d.ts.map +1 -1
- package/esm/internal/components/dialogs/userManagementDialogs/UserGroups/backend/loadUserGroupOptionsPromise.js +1 -1
- package/esm/internal/components/pluggableVisualizations/baseChart/PluggableBaseChart.d.ts.map +1 -1
- package/esm/internal/components/pluggableVisualizations/pivotTableNext/PluggablePivotTableNext.d.ts.map +1 -1
- package/esm/internal/components/pluggableVisualizations/pivotTableNext/PluggablePivotTableNext.js +18 -18
- package/esm/internal/components/pluggableVisualizations/pivotTableNext/pivotTableNextConfigFromInsight.d.ts +1 -2
- package/esm/internal/components/pluggableVisualizations/pivotTableNext/pivotTableNextConfigFromInsight.d.ts.map +1 -1
- package/esm/internal/components/pluggableVisualizations/pivotTableNext/pivotTableNextConfigFromInsight.js +3 -9
- package/esm/internal/components/pluggableVisualizations/repeater/PluggableRepeater.d.ts.map +1 -1
- package/esm/internal/index.d.ts +1 -0
- package/esm/internal/index.d.ts.map +1 -1
- package/esm/internal/index.js +1 -0
- package/esm/internal/translations/de-DE.localization-bundle.d.ts +5 -2
- package/esm/internal/translations/de-DE.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/de-DE.localization-bundle.js +5 -2
- package/esm/internal/translations/en-AU.localization-bundle.d.ts +5 -2
- package/esm/internal/translations/en-AU.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/en-AU.localization-bundle.js +5 -2
- package/esm/internal/translations/en-GB.localization-bundle.d.ts +5 -2
- package/esm/internal/translations/en-GB.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/en-GB.localization-bundle.js +5 -2
- package/esm/internal/translations/en-US.localization-bundle.d.ts +14 -2
- package/esm/internal/translations/en-US.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/en-US.localization-bundle.js +18 -6
- package/esm/internal/translations/es-419.localization-bundle.d.ts +5 -2
- package/esm/internal/translations/es-419.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/es-419.localization-bundle.js +5 -2
- package/esm/internal/translations/es-ES.localization-bundle.d.ts +5 -2
- package/esm/internal/translations/es-ES.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/es-ES.localization-bundle.js +5 -2
- package/esm/internal/translations/fi-FI.localization-bundle.d.ts +5 -2
- package/esm/internal/translations/fi-FI.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/fi-FI.localization-bundle.js +5 -2
- package/esm/internal/translations/fr-CA.localization-bundle.d.ts +5 -2
- package/esm/internal/translations/fr-CA.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/fr-CA.localization-bundle.js +5 -2
- package/esm/internal/translations/fr-FR.localization-bundle.d.ts +5 -2
- package/esm/internal/translations/fr-FR.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/fr-FR.localization-bundle.js +5 -2
- package/esm/internal/translations/id-ID.localization-bundle.d.ts +5 -2
- package/esm/internal/translations/id-ID.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/id-ID.localization-bundle.js +5 -2
- package/esm/internal/translations/it-IT.localization-bundle.d.ts +5 -2
- package/esm/internal/translations/it-IT.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/it-IT.localization-bundle.js +5 -2
- package/esm/internal/translations/ja-JP.localization-bundle.d.ts +5 -2
- package/esm/internal/translations/ja-JP.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/ja-JP.localization-bundle.js +5 -2
- package/esm/internal/translations/ko-KR.localization-bundle.d.ts +5 -2
- package/esm/internal/translations/ko-KR.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/ko-KR.localization-bundle.js +5 -2
- package/esm/internal/translations/nl-NL.localization-bundle.d.ts +5 -2
- package/esm/internal/translations/nl-NL.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/nl-NL.localization-bundle.js +5 -2
- package/esm/internal/translations/pl-PL.localization-bundle.d.ts +5 -2
- package/esm/internal/translations/pl-PL.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/pl-PL.localization-bundle.js +5 -2
- package/esm/internal/translations/pt-BR.localization-bundle.d.ts +5 -2
- package/esm/internal/translations/pt-BR.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/pt-BR.localization-bundle.js +5 -2
- package/esm/internal/translations/pt-PT.localization-bundle.d.ts +5 -2
- package/esm/internal/translations/pt-PT.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/pt-PT.localization-bundle.js +5 -2
- package/esm/internal/translations/ru-RU.localization-bundle.d.ts +5 -2
- package/esm/internal/translations/ru-RU.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/ru-RU.localization-bundle.js +5 -2
- package/esm/internal/translations/sl-SI.localization-bundle.d.ts +5 -2
- package/esm/internal/translations/sl-SI.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/sl-SI.localization-bundle.js +5 -2
- package/esm/internal/translations/th-TH.localization-bundle.d.ts +5 -2
- package/esm/internal/translations/th-TH.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/th-TH.localization-bundle.js +5 -2
- package/esm/internal/translations/tr-TR.localization-bundle.d.ts +5 -2
- package/esm/internal/translations/tr-TR.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/tr-TR.localization-bundle.js +5 -2
- package/esm/internal/translations/uk-UA.localization-bundle.d.ts +5 -2
- package/esm/internal/translations/uk-UA.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/uk-UA.localization-bundle.js +5 -2
- package/esm/internal/translations/vi-VN.localization-bundle.d.ts +5 -2
- package/esm/internal/translations/vi-VN.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/vi-VN.localization-bundle.js +5 -2
- package/esm/internal/translations/zh-HK.localization-bundle.d.ts +5 -2
- package/esm/internal/translations/zh-HK.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/zh-HK.localization-bundle.js +6 -3
- package/esm/internal/translations/zh-Hans.localization-bundle.d.ts +5 -2
- package/esm/internal/translations/zh-Hans.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/zh-Hans.localization-bundle.js +5 -2
- package/esm/internal/translations/zh-Hant.localization-bundle.d.ts +5 -2
- package/esm/internal/translations/zh-Hant.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/zh-Hant.localization-bundle.js +5 -2
- package/esm/internal/utils/embeddingInsightViewCodeGenerator/insightViewConfig.d.ts.map +1 -1
- package/esm/internal/utils/embeddingInsightViewCodeGenerator/insightViewConfig.js +2 -1
- package/esm/internal/utils/propertiesHelper.d.ts +33 -1
- package/esm/internal/utils/propertiesHelper.d.ts.map +1 -1
- package/esm/internal/utils/propertiesHelper.js +43 -1
- package/esm/locales.d.ts +11 -2
- package/esm/locales.d.ts.map +1 -1
- package/esm/locales.js +5 -2
- package/esm/sdk-ui-ext.d.ts +194 -4
- package/esm/share/ObjectShareDialog.d.ts +19 -0
- package/esm/share/ObjectShareDialog.d.ts.map +1 -1
- package/esm/share/ObjectShareDialog.js +8 -1
- package/esm/share/objectShareController.helpers.d.ts +8 -52
- package/esm/share/objectShareController.helpers.d.ts.map +1 -1
- package/esm/share/objectShareController.helpers.js +23 -0
- package/esm/share/objectShareController.types.d.ts +76 -3
- package/esm/share/objectShareController.types.d.ts.map +1 -1
- package/esm/share/useAccessList.d.ts +14 -3
- package/esm/share/useAccessList.d.ts.map +1 -1
- package/esm/share/useAccessList.js +26 -7
- package/esm/share/useApplyObjectPermissions.d.ts +13 -0
- package/esm/share/useApplyObjectPermissions.d.ts.map +1 -0
- package/esm/share/useApplyObjectPermissions.js +33 -0
- package/esm/share/useObjectShareController.d.ts.map +1 -1
- package/esm/share/useObjectShareController.js +20 -9
- package/esm/share/useObjectShareDialog.d.ts +12 -3
- package/esm/share/useObjectShareDialog.d.ts.map +1 -1
- package/esm/share/useObjectShareDialog.js +16 -3
- package/package.json +24 -25
- package/styles/css/main.css +38 -4
- package/styles/css/main.css.map +1 -1
- package/styles/internal/css/attribute_hierarchies.css.map +1 -1
- package/styles/internal/css/charts.css.map +1 -1
- package/styles/internal/css/conditional_formatting.css +38 -4
- package/styles/internal/css/conditional_formatting.css.map +1 -1
- package/styles/internal/css/config_panel.css.map +1 -1
- package/styles/internal/css/dashboard_embedding.css.map +1 -1
- package/styles/internal/css/headline.css.map +1 -1
- 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 {
|
|
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,
|
|
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 =
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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:
|
|
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("
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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,
|
|
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
|
|
24
|
-
//
|
|
25
|
-
|
|
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
|
-
:
|
|
47
|
+
: dialog;
|
|
36
48
|
}
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
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.
|
|
43
|
-
? `${dialog.rule.id}:${JSON.stringify(dialog.rule.conditions)}`
|
|
44
|
-
: dialog.rule.id;
|
|
55
|
+
return dialog.rule.id;
|
|
45
56
|
}
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
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
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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,
|
|
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:
|
|
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 `
|
|
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
|
-
* `
|
|
72
|
-
*
|
|
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
|
-
*
|
|
77
|
-
*
|
|
78
|
-
* for
|
|
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[];
|