@gooddata/sdk-ui-ext 11.47.0-alpha.3 → 11.47.0-alpha.4

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 (42) hide show
  1. package/NOTICE +3 -3
  2. package/esm/internal/components/configurationControls/conditionalFormatting/CfSelect.d.ts +14 -0
  3. package/esm/internal/components/configurationControls/conditionalFormatting/CfSelect.d.ts.map +1 -0
  4. package/esm/internal/components/configurationControls/conditionalFormatting/CfSelect.js +25 -0
  5. package/esm/internal/components/configurationControls/conditionalFormatting/ConditionEditor.d.ts +18 -0
  6. package/esm/internal/components/configurationControls/conditionalFormatting/ConditionEditor.d.ts.map +1 -0
  7. package/esm/internal/components/configurationControls/conditionalFormatting/ConditionEditor.js +132 -0
  8. package/esm/internal/components/configurationControls/conditionalFormatting/ConditionalFormattingDialog.d.ts +4 -1
  9. package/esm/internal/components/configurationControls/conditionalFormatting/ConditionalFormattingDialog.d.ts.map +1 -1
  10. package/esm/internal/components/configurationControls/conditionalFormatting/ConditionalFormattingDialog.js +15 -116
  11. package/esm/internal/components/configurationControls/conditionalFormatting/ConditionalFormattingSection.d.ts +5 -3
  12. package/esm/internal/components/configurationControls/conditionalFormatting/ConditionalFormattingSection.d.ts.map +1 -1
  13. package/esm/internal/components/configurationControls/conditionalFormatting/ConditionalFormattingSection.js +10 -10
  14. package/esm/internal/components/configurationControls/conditionalFormatting/ReorderList.d.ts +25 -0
  15. package/esm/internal/components/configurationControls/conditionalFormatting/ReorderList.d.ts.map +1 -0
  16. package/esm/internal/components/configurationControls/conditionalFormatting/ReorderList.js +94 -0
  17. package/esm/internal/components/configurationControls/conditionalFormatting/conditionalFormattingModel.d.ts +66 -7
  18. package/esm/internal/components/configurationControls/conditionalFormatting/conditionalFormattingModel.d.ts.map +1 -1
  19. package/esm/internal/components/configurationControls/conditionalFormatting/conditionalFormattingModel.js +165 -21
  20. package/esm/internal/components/configurationControls/conditionalFormatting/reorderListModel.d.ts +4 -0
  21. package/esm/internal/components/configurationControls/conditionalFormatting/reorderListModel.d.ts.map +1 -0
  22. package/esm/internal/components/configurationControls/conditionalFormatting/reorderListModel.js +16 -0
  23. package/esm/internal/components/configurationPanels/ConfigurationPanelContent.d.ts +0 -1
  24. package/esm/internal/components/configurationPanels/ConfigurationPanelContent.d.ts.map +1 -1
  25. package/esm/internal/components/configurationPanels/PivotTableConfigurationPanel.d.ts +9 -2
  26. package/esm/internal/components/configurationPanels/PivotTableConfigurationPanel.d.ts.map +1 -1
  27. package/esm/internal/components/configurationPanels/PivotTableConfigurationPanel.js +4 -4
  28. package/esm/internal/components/pluggableVisualizations/pivotTableNext/PluggablePivotTableNext.d.ts +1 -1
  29. package/esm/internal/components/pluggableVisualizations/pivotTableNext/PluggablePivotTableNext.d.ts.map +1 -1
  30. package/esm/internal/components/pluggableVisualizations/pivotTableNext/PluggablePivotTableNext.js +8 -7
  31. package/esm/internal/translations/en-US.localization-bundle.d.ts +8 -0
  32. package/esm/internal/translations/en-US.localization-bundle.d.ts.map +1 -1
  33. package/esm/internal/translations/en-US.localization-bundle.js +8 -0
  34. package/esm/locales.d.ts +6 -0
  35. package/esm/locales.d.ts.map +1 -1
  36. package/esm/locales.js +2 -0
  37. package/package.json +21 -21
  38. package/styles/css/main.css +102 -13
  39. package/styles/css/main.css.map +1 -1
  40. package/styles/internal/css/conditional_formatting.css +102 -13
  41. package/styles/internal/css/conditional_formatting.css.map +1 -1
  42. package/styles/internal/scss/conditional_formatting.scss +138 -13
package/NOTICE CHANGED
@@ -7,9 +7,9 @@
7
7
 
8
8
  The following 3rd-party software packages may be used by or distributed with gooddata-ui-sdk. Any information relevant to third-party vendors listed below are collected using common, reasonable means.
9
9
 
10
- Date generated: 2026-7-10
10
+ Date generated: 2026-7-14
11
11
 
12
- Revision ID: 49e9a8adde3ee1f908b34090cae5065e788ef0dd
12
+ Revision ID: 2ce9ab6aef1d6be52eb66b2f005add0bb3b08196
13
13
 
14
14
  ================================================================================
15
15
  ================================================================================
@@ -29272,4 +29272,4 @@ POSSIBILITY OF SUCH DAMAGE.
29272
29272
  --------------------------------------------------------------------------------
29273
29273
  --------------------------------------------------------------------------------
29274
29274
 
29275
- Report Generated by FOSSA on 2026-7-10
29275
+ Report Generated by FOSSA on 2026-7-14
@@ -0,0 +1,14 @@
1
+ interface ISelectItem<T extends string> {
2
+ value: T;
3
+ title: string;
4
+ icon?: string;
5
+ }
6
+ interface ICfSelectProps<T extends string> {
7
+ value?: T;
8
+ items: readonly ISelectItem<T>[];
9
+ onSelect: (value: T) => void;
10
+ placeholder?: string;
11
+ }
12
+ export declare function CfSelect<T extends string>({ value, items, onSelect, placeholder }: ICfSelectProps<T>): import("react/jsx-runtime").JSX.Element;
13
+ export {};
14
+ //# sourceMappingURL=CfSelect.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CfSelect.d.ts","sourceRoot":"","sources":["../../../../../src/internal/components/configurationControls/conditionalFormatting/CfSelect.tsx"],"names":[],"mappings":"AAaA,UAAU,WAAW,CAAC,CAAC,SAAS,MAAM;IAClC,KAAK,EAAE,CAAC,CAAC;IACT,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,UAAU,cAAc,CAAC,CAAC,SAAS,MAAM;IACrC,KAAK,CAAC,EAAE,CAAC,CAAC;IACV,KAAK,EAAE,SAAS,WAAW,CAAC,CAAC,CAAC,EAAE,CAAC;IACjC,QAAQ,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,IAAI,CAAC;IAC7B,WAAW,CAAC,EAAE,MAAM,CAAC;CACxB;AAmCD,wBAAgB,QAAQ,CAAC,CAAC,SAAS,MAAM,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,WAAW,EAAE,EAAE,cAAc,CAAC,CAAC,CAAC,2CAyCpG"}
@@ -0,0 +1,25 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ // (C) 2026 GoodData Corporation
3
+ import cx from "classnames";
4
+ import { DropdownButton, SingleSelectListItem, UiDropdown, UiListbox, } from "@gooddata/sdk-ui-kit";
5
+ // One listbox option; the iconRenderer sizes the type (attribute/metric) and wide operator glyphs.
6
+ function CfListItem({ item, isSelected, isFocused, onSelect, }) {
7
+ const { title, icon } = item.data;
8
+ return (_jsx("div", { className: "gd-cf-select__option", children: _jsx(SingleSelectListItem, { title: title, icon: icon, iconRenderer: (ic) => {
9
+ if (typeof ic !== "string" || !ic) {
10
+ return null;
11
+ }
12
+ const isTypeIcon = ic === "gd-icon-attribute" || ic === "gd-icon-metric";
13
+ return (_jsx("span", { "aria-hidden": "true", className: cx("gd-list-icon", ic, { "gd-cf-type-icon": isTypeIcon }) }));
14
+ }, isSelected: isSelected, isFocused: isFocused, onClick: onSelect }) }));
15
+ }
16
+ export function CfSelect({ value, items, onSelect, placeholder }) {
17
+ const selected = items.find((item) => item.value === value);
18
+ const listItems = items.map((item) => ({
19
+ type: "interactive",
20
+ id: item.value,
21
+ stringTitle: item.title,
22
+ data: item,
23
+ }));
24
+ return (_jsx(UiDropdown, { width: "same-as-anchor", fullWidthButton: true, closeOnParentScroll: true, closeOnMouseDrag: true, closeOnEscape: true, autofocusOnOpen: true, accessibilityConfig: { triggerRole: "combobox", popupRole: "listbox" }, renderButton: ({ isOpen, toggleDropdown, ref, dropdownId }) => (_jsx(DropdownButton, { value: selected?.title ?? placeholder ?? "", iconLeft: selected?.icon, isOpen: isOpen, isFullWidth: true, onClick: toggleDropdown, buttonRef: ref, dropdownId: dropdownId, accessibilityConfig: { popupType: "listbox" } })), renderBody: ({ closeDropdown, ariaAttributes }) => (_jsx(UiListbox, { items: listItems, selectedItemId: value, ariaAttributes: ariaAttributes, InteractiveItemComponent: CfListItem, onClose: closeDropdown, onSelect: (listItem) => onSelect(listItem.data.value) })) }));
25
+ }
@@ -0,0 +1,18 @@
1
+ import { type IntlShape } from "react-intl";
2
+ import { type ISeparators } from "@gooddata/sdk-model";
3
+ import { type ConditionalFormattingTarget, type IConditionalFormattingCondition } from "@gooddata/sdk-ui-pivot/next";
4
+ import { type IReorderSlot } from "./ReorderList.js";
5
+ export interface IConditionEditorProps {
6
+ condition: IConditionalFormattingCondition;
7
+ kind: ConditionalFormattingTarget["kind"];
8
+ isPercent: boolean;
9
+ separators: ISeparators | undefined;
10
+ suggestions: readonly string[];
11
+ removable: boolean;
12
+ slot: IReorderSlot;
13
+ intl: IntlShape;
14
+ onChange: (condition: IConditionalFormattingCondition) => void;
15
+ onRemove: () => void;
16
+ }
17
+ export declare function ConditionEditor({ condition, kind, isPercent, separators, suggestions, removable, slot, intl, onChange, onRemove }: IConditionEditorProps): import("react/jsx-runtime").JSX.Element;
18
+ //# sourceMappingURL=ConditionEditor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ConditionEditor.d.ts","sourceRoot":"","sources":["../../../../../src/internal/components/configurationControls/conditionalFormatting/ConditionEditor.tsx"],"names":[],"mappings":"AAKA,OAAO,EAAE,KAAK,SAAS,EAA0B,MAAM,YAAY,CAAC;AAEpE,OAAO,EAAe,KAAK,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAiBpE,OAAO,EAEH,KAAK,2BAA2B,EAEhC,KAAK,+BAA+B,EAEvC,MAAM,6BAA6B,CAAC;AAiBrC,OAAO,EAAE,KAAK,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAoUrD,MAAM,WAAW,qBAAqB;IAClC,SAAS,EAAE,+BAA+B,CAAC;IAC3C,IAAI,EAAE,2BAA2B,CAAC,MAAM,CAAC,CAAC;IAC1C,SAAS,EAAE,OAAO,CAAC;IACnB,UAAU,EAAE,WAAW,GAAG,SAAS,CAAC;IACpC,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;IAC/B,SAAS,EAAE,OAAO,CAAC;IACnB,IAAI,EAAE,YAAY,CAAC;IACnB,IAAI,EAAE,SAAS,CAAC;IAChB,QAAQ,EAAE,CAAC,SAAS,EAAE,+BAA+B,KAAK,IAAI,CAAC;IAC/D,QAAQ,EAAE,MAAM,IAAI,CAAC;CACxB;AAED,wBAAgB,eAAe,CAAC,EAC5B,SAAS,EACT,IAAI,EACJ,SAAS,EACT,UAAU,EACV,WAAW,EACX,SAAS,EACT,IAAI,EACJ,IAAI,EACJ,QAAQ,EACR,QAAQ,EACX,EAAE,qBAAqB,2CAqEvB"}
@@ -0,0 +1,132 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // (C) 2026 GoodData Corporation
3
+ import { useId } from "react";
4
+ import cx from "classnames";
5
+ import { Button, Dropdown, Input, InputWithNumberFormat, UiButton, UiButtonSegmentedControl, UiCombobox, UiComboboxInput, UiComboboxList, UiComboboxListItem, UiComboboxListItemLabel, UiComboboxPopup, } from "@gooddata/sdk-ui-kit";
6
+ import { conditionalFormattingMessages, conditionalFormattingOperatorMessages } from "../../../../locales.js";
7
+ import { ColorDropdown } from "../colors/colorDropdown/ColorDropdown.js";
8
+ import { CfSelect } from "./CfSelect.js";
9
+ import { CF_COLOR_PALETTE, colorToHex, hexToColor } from "./conditionalFormattingColors.js";
10
+ import { displayToRawNumber, emptyValueForOperator, operatorIcon, operatorsForKind, rawToDisplayNumber, validateCondition, valueEditorKind, } from "./conditionalFormattingModel.js";
11
+ // An entered operand survives operator changes within the same shape (e.g. > to >=).
12
+ const valueForOperator = (operator, previous) => {
13
+ const empty = emptyValueForOperator(operator);
14
+ return previous.kind === empty.kind ? previous : empty;
15
+ };
16
+ const literalText = (value) => value.kind === "literal" ? String(value.value) : "";
17
+ const literalRaw = (value) => {
18
+ if (value.kind !== "literal" || String(value.value).trim() === "") {
19
+ return null;
20
+ }
21
+ const n = Number(value.value);
22
+ return Number.isFinite(n) ? n : null;
23
+ };
24
+ const rangeRaw = (value, bound) => {
25
+ if (value.kind !== "literalRange") {
26
+ return null;
27
+ }
28
+ const n = value[bound];
29
+ return Number.isFinite(n) ? n : null;
30
+ };
31
+ function CfMeasureValueInput({ raw, isPercent, separators, placeholder, intl, onChangeRaw, hasError, accessibilityConfig, }) {
32
+ return (_jsx(InputWithNumberFormat, { value: raw === null ? undefined : rawToDisplayNumber(raw, isPercent), suffix: isPercent ? "%" : undefined, separators: separators, placeholder: placeholder, onChange: (display) => onChangeRaw(display === null ? null : displayToRawNumber(display, isPercent)), hasError: hasError, accessibilityConfig: {
33
+ // InputPure renders the suffix aria-hidden; this is its accessible name.
34
+ ...(isPercent
35
+ ? {
36
+ suffixAriaLabel: intl.formatMessage(conditionalFormattingMessages.dialogUnitPercent),
37
+ }
38
+ : {}),
39
+ ...accessibilityConfig,
40
+ } }));
41
+ }
42
+ // Free-text combobox suggesting current-result element values; `creatable` = typed text is a valid value.
43
+ function CfValueCombobox({ value, suggestions, placeholder, onChangeText }) {
44
+ const idPrefix = useId();
45
+ const options = suggestions.map((suggestion, index) => ({
46
+ // aria-activedescendant needs a valid, document-unique DOM id (element values aren't); matching is by label.
47
+ id: `${idPrefix}-${index}`,
48
+ label: suggestion,
49
+ }));
50
+ return (_jsxs(UiCombobox, { options: options, value: value, creatable: true, onValueChange: onChangeText, children: [
51
+ _jsx(UiComboboxInput, { placeholder: placeholder, accessibilityConfig: { ariaLabel: placeholder } }), _jsx(UiComboboxPopup, { children: _jsx(UiComboboxList, { children: (option, index) => (_jsx(UiComboboxListItem, { option: option, index: index, children: _jsx(UiComboboxListItemLabel, { children: option.label }) })) }) })
52
+ ] }));
53
+ }
54
+ const FIELD_ERROR_MESSAGE = {
55
+ rangeOrder: conditionalFormattingMessages.dialogErrorRangeOrder,
56
+ };
57
+ function CfFieldError({ id, error, intl, }) {
58
+ if (!error) {
59
+ return null;
60
+ }
61
+ return (_jsx("span", { id: id, role: "alert", className: "gd-cf-condition__error", children: intl.formatMessage(FIELD_ERROR_MESSAGE[error]) }));
62
+ }
63
+ function ConditionValueInput({ condition, kind, isPercent, separators, suggestions, intl, onChange, }) {
64
+ const errorId = useId();
65
+ const setValue = (value) => onChange({ ...condition, value });
66
+ switch (valueEditorKind(condition, kind, suggestions.length > 0)) {
67
+ case "none":
68
+ return null;
69
+ case "number":
70
+ return (_jsx(CfMeasureValueInput, { raw: literalRaw(condition.value), isPercent: isPercent, separators: separators, placeholder: intl.formatMessage(conditionalFormattingMessages.dialogValuePlaceholder), intl: intl, onChangeRaw: (raw) => setValue({ kind: "literal", value: raw ?? "" }) }));
71
+ case "combobox":
72
+ return (_jsx(CfValueCombobox, { value: literalText(condition.value), suggestions: suggestions, placeholder: intl.formatMessage(conditionalFormattingMessages.dialogValuePlaceholder), onChangeText: (v) => setValue({ kind: "literal", value: v }) }));
73
+ case "text":
74
+ return (_jsx(Input, { type: "text", value: literalText(condition.value), placeholder: intl.formatMessage(conditionalFormattingMessages.dialogValuePlaceholder), onChange: (v) => setValue({ kind: "literal", value: String(v) }) }));
75
+ case "range": {
76
+ const range = condition.value.kind === "literalRange" ? condition.value : { from: NaN, to: NaN };
77
+ const setRange = (from, to) => setValue({ kind: "literalRange", from, to });
78
+ const { errors } = validateCondition(condition, kind);
79
+ const invalidInputProps = (error) => error
80
+ ? { hasError: true, accessibilityConfig: { ariaDescribedBy: errorId, ariaInvalid: true } }
81
+ : {};
82
+ return (_jsxs(_Fragment, { children: [
83
+ _jsxs("div", { className: "gd-cf-condition__range", children: [
84
+ _jsx(CfMeasureValueInput, { raw: rangeRaw(condition.value, "from"), isPercent: isPercent, separators: separators, placeholder: intl.formatMessage(conditionalFormattingMessages.dialogFromPlaceholder), intl: intl, onChangeRaw: (raw) => setRange(raw ?? NaN, range.to), ...invalidInputProps(errors.range) }), _jsx(CfMeasureValueInput, { raw: rangeRaw(condition.value, "to"), isPercent: isPercent, separators: separators, placeholder: intl.formatMessage(conditionalFormattingMessages.dialogToPlaceholder), intl: intl, onChangeRaw: (raw) => setRange(range.from, raw ?? NaN), ...invalidInputProps(errors.range) })
85
+ ] }), _jsx(CfFieldError, { id: errorId, error: errors.range, intl: intl })
86
+ ] }));
87
+ }
88
+ }
89
+ }
90
+ // The ColorDropdown trigger swatch. ColorDropdown clones it with selectable-child props the swatch
91
+ // ignores. No color renders a "none" slash rather than a misleading white.
92
+ function CfSwatch({ hex }) {
93
+ return (_jsx("span", { className: `gd-cf-swatch${hex ? "" : " gd-cf-swatch--none"}`, style: hex ? { backgroundColor: hex } : undefined }));
94
+ }
95
+ function CfColorPick({ hex, noneColorLabel, onChange }) {
96
+ return (_jsx(ColorDropdown, { colorPalette: CF_COLOR_PALETTE, selectedColorItem: hexToColor(hex), onColorSelected: (color) => onChange(colorToHex(color)), onClear: () => onChange(undefined), noneColorAriaLabel: noneColorLabel, children: _jsx(CfSwatch, { hex: hex }) }));
97
+ }
98
+ const PREVIEW_SAMPLE = {
99
+ measure: "1,234.56",
100
+ attribute: "Abc",
101
+ };
102
+ // Row scope shows two sample cells to convey that the whole row is formatted.
103
+ const ROW_PREVIEW_SAMPLE = ["Abc", "123"];
104
+ function FormatEditor({ format, intl, onChange }) {
105
+ return (_jsx(Dropdown, { closeOnParentScroll: true, closeOnMouseDrag: true, closeOnEscape: true, renderButton: ({ toggleDropdown, accessibilityConfig }) => (_jsx(Button, { className: "gd-button-link gd-button-icon-only gd-icon-pencil gd-cf-condition__edit", accessibilityConfig: {
106
+ ...accessibilityConfig,
107
+ ariaLabel: intl.formatMessage(conditionalFormattingMessages.dialogFormat),
108
+ }, onClick: toggleDropdown })), renderBody: () => (_jsxs("div", { className: "gd-cf-format", children: [
109
+ _jsx("span", { className: "gd-cf-format__label", children: intl.formatMessage(conditionalFormattingMessages.dialogTextColor) }), _jsx(CfColorPick, { hex: format.color, noneColorLabel: intl.formatMessage(conditionalFormattingMessages.dialogNoColor), onChange: (color) => onChange({ ...format, color }) }), _jsx("span", { className: "gd-cf-format__label", children: intl.formatMessage(conditionalFormattingMessages.dialogFillColor) }), _jsx(CfColorPick, { hex: format.backgroundColor, noneColorLabel: intl.formatMessage(conditionalFormattingMessages.dialogNoColor), onChange: (backgroundColor) => onChange({ ...format, backgroundColor }) }), _jsxs(UiButtonSegmentedControl, { children: [
110
+ _jsx(UiButton, { size: "small", variant: "secondary", label: intl.formatMessage(conditionalFormattingMessages.dialogScopeCell), isSelected: format.scope === "cell", onClick: () => onChange({ ...format, scope: "cell" }) }), _jsx(UiButton, { size: "small", variant: "secondary", label: intl.formatMessage(conditionalFormattingMessages.dialogScopeRow), isSelected: format.scope === "row", onClick: () => onChange({ ...format, scope: "row" }) })
111
+ ] })
112
+ ] })) }));
113
+ }
114
+ export function ConditionEditor({ condition, kind, isPercent, separators, suggestions, removable, slot, intl, onChange, onRemove, }) {
115
+ const operatorItems = operatorsForKind(kind).map((operator) => ({
116
+ value: operator,
117
+ title: intl.formatMessage(conditionalFormattingOperatorMessages[operator]),
118
+ icon: operatorIcon(operator),
119
+ }));
120
+ const setFormat = (format) => onChange({ ...condition, format });
121
+ const previewStyle = {
122
+ ...(condition.format.color ? { color: condition.format.color } : {}),
123
+ ...(condition.format.backgroundColor ? { backgroundColor: condition.format.backgroundColor } : {}),
124
+ };
125
+ return (_jsxs("div", { className: cx("gd-cf-condition", slot.className), ...slot.rootProps, children: [slot.handle, _jsxs("div", { className: "gd-cf-condition__header", children: [
126
+ _jsx("span", { className: "gd-cf-condition__title", children: intl.formatMessage(conditionalFormattingMessages.dialogCondition) }), removable ? (_jsx(Button, { className: "gd-button-link gd-button-icon-only gd-icon-cross", accessibilityConfig: {
127
+ ariaLabel: intl.formatMessage(conditionalFormattingMessages.dialogRemoveCondition),
128
+ }, onClick: onRemove })) : null] }), _jsx(CfSelect, { value: condition.operator, items: operatorItems, onSelect: (operator) => onChange({ ...condition, operator, value: valueForOperator(operator, condition.value) }) }), _jsx(ConditionValueInput, { condition: condition, kind: kind, isPercent: isPercent, separators: separators, suggestions: suggestions, intl: intl, onChange: onChange }), _jsxs("div", { className: "gd-cf-condition__format-row", children: [
129
+ _jsx("span", { className: "gd-cf-dialog__label", children: intl.formatMessage(conditionalFormattingMessages.dialogFormat) }), _jsx("div", { className: "gd-cf-preview", children: condition.format.scope === "row" ? (ROW_PREVIEW_SAMPLE.map((sample, index) => (_jsx("span", { className: "gd-cf-preview__cell", style: previewStyle, children: sample }, index)))) : (_jsx("span", { className: "gd-cf-preview__cell", style: previewStyle, children: PREVIEW_SAMPLE[kind] })) }), _jsx(FormatEditor, { format: condition.format, intl: intl, onChange: setFormat })
130
+ ] })
131
+ ] }));
132
+ }
@@ -1,13 +1,16 @@
1
+ import { type ISeparators } from "@gooddata/sdk-model";
1
2
  import { type IConditionalFormattingRule } from "@gooddata/sdk-ui-pivot/next";
2
3
  import { type ITargetOption } from "./conditionalFormattingModel.js";
3
4
  export interface IConditionalFormattingDialogProps {
4
5
  rule: IConditionalFormattingRule;
5
6
  isNew: boolean;
6
7
  targetOptions: ITargetOption[];
8
+ /** Workspace number separators; measure value inputs validate and format with them. */
9
+ separators?: ISeparators;
7
10
  /** CSS selector of a small, stable element the popover anchors to. */
8
11
  alignTo: string;
9
12
  onSave: (rule: IConditionalFormattingRule) => void;
10
13
  onClose: () => void;
11
14
  }
12
- export declare function ConditionalFormattingDialog({ rule: initialRule, isNew, targetOptions, alignTo, onSave, onClose }: IConditionalFormattingDialogProps): import("react/jsx-runtime").JSX.Element;
15
+ export declare function ConditionalFormattingDialog({ rule: initialRule, isNew, targetOptions, separators, alignTo, onSave, onClose }: IConditionalFormattingDialogProps): import("react/jsx-runtime").JSX.Element;
13
16
  //# sourceMappingURL=ConditionalFormattingDialog.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ConditionalFormattingDialog.d.ts","sourceRoot":"","sources":["../../../../../src/internal/components/configurationControls/conditionalFormatting/ConditionalFormattingDialog.tsx"],"names":[],"mappings":"AAsBA,OAAO,EAMH,KAAK,0BAA0B,EAClC,MAAM,6BAA6B,CAAC;AAMrC,OAAO,EACH,KAAK,aAAa,EAQrB,MAAM,iCAAiC,CAAC;AA0UzC,MAAM,WAAW,iCAAiC;IAC9C,IAAI,EAAE,0BAA0B,CAAC;IACjC,KAAK,EAAE,OAAO,CAAC;IACf,aAAa,EAAE,aAAa,EAAE,CAAC;IAC/B,sEAAsE;IACtE,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,CAAC,IAAI,EAAE,0BAA0B,KAAK,IAAI,CAAC;IACnD,OAAO,EAAE,MAAM,IAAI,CAAC;CACvB;AAED,wBAAgB,2BAA2B,CAAC,EACxC,IAAI,EAAE,WAAW,EACjB,KAAK,EACL,aAAa,EACb,OAAO,EACP,MAAM,EACN,OAAO,EACV,EAAE,iCAAiC,2CA0GnC"}
1
+ {"version":3,"file":"ConditionalFormattingDialog.d.ts","sourceRoot":"","sources":["../../../../../src/internal/components/configurationControls/conditionalFormatting/ConditionalFormattingDialog.tsx"],"names":[],"mappings":"AAMA,OAAO,EAAE,KAAK,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAEvD,OAAO,EAEH,KAAK,0BAA0B,EAClC,MAAM,6BAA6B,CAAC;AAKrC,OAAO,EACH,KAAK,aAAa,EAOrB,MAAM,iCAAiC,CAAC;AAWzC,MAAM,WAAW,iCAAiC;IAC9C,IAAI,EAAE,0BAA0B,CAAC;IACjC,KAAK,EAAE,OAAO,CAAC;IACf,aAAa,EAAE,aAAa,EAAE,CAAC;IAC/B,uFAAuF;IACvF,UAAU,CAAC,EAAE,WAAW,CAAC;IACzB,sEAAsE;IACtE,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,CAAC,IAAI,EAAE,0BAA0B,KAAK,IAAI,CAAC;IACnD,OAAO,EAAE,MAAM,IAAI,CAAC;CACvB;AAED,wBAAgB,2BAA2B,CAAC,EACxC,IAAI,EAAE,WAAW,EACjB,KAAK,EACL,aAAa,EACb,UAAU,EACV,OAAO,EACP,MAAM,EACN,OAAO,EACV,EAAE,iCAAiC,2CAmHnC"}
@@ -1,121 +1,22 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  // (C) 2026 GoodData Corporation
3
- import { useId, useState } from "react";
4
- import cx from "classnames";
3
+ import { useState } from "react";
5
4
  import { useIntl } from "react-intl";
6
- import { Button, Dropdown, DropdownButton, Input, Overlay, SingleSelectListItem, UiButton, UiButtonSegmentedControl, UiListbox, } from "@gooddata/sdk-ui-kit";
7
- import { conditionalFormattingMessages, conditionalFormattingOperatorMessages } from "../../../../locales.js";
8
- import { ColorDropdown } from "../colors/colorDropdown/ColorDropdown.js";
9
- import { CF_COLOR_PALETTE, colorToHex, hexToColor } from "./conditionalFormattingColors.js";
10
- import { findTargetOption, isRuleComplete, newCondition, operatorArity, operatorIcon, operatorsForKind, targetIcon, } from "./conditionalFormattingModel.js";
11
- // One listbox option; the iconRenderer sizes the type (attribute/metric) and wide operator glyphs.
12
- function CfListItem({ item, isSelected, isFocused, onSelect, }) {
13
- const { title, icon } = item.data;
14
- return (_jsx(SingleSelectListItem, { title: title, icon: icon, iconRenderer: (ic) => {
15
- if (typeof ic !== "string" || !ic) {
16
- return null;
17
- }
18
- const isTypeIcon = ic === "gd-icon-attribute" || ic === "gd-icon-metric";
19
- return (_jsx("span", { "aria-hidden": "true", className: cx("gd-list-icon", ic, { "gd-cf-type-icon": isTypeIcon }) }));
20
- }, isSelected: isSelected, isFocused: isFocused, onClick: onSelect }));
21
- }
22
- function CfSelect({ value, items, onSelect, placeholder, width = 200 }) {
23
- const listboxId = useId();
24
- const selected = items.find((item) => item.value === value);
25
- const listItems = items.map((item) => ({
26
- type: "interactive",
27
- id: item.value,
28
- stringTitle: item.title,
29
- data: { title: item.title, icon: item.icon },
30
- }));
31
- return (_jsx(Dropdown, { closeOnParentScroll: true, closeOnMouseDrag: true, renderButton: ({ isOpen, toggleDropdown }) => (_jsx(DropdownButton, { value: selected?.title ?? placeholder ?? "", iconLeft: selected?.icon, isOpen: isOpen, onClick: toggleDropdown })), renderBody: ({ closeDropdown }) => (_jsx(UiListbox, { items: listItems, width: width, maxHeight: 300, selectedItemId: value, ariaAttributes: { id: listboxId }, InteractiveItemComponent: CfListItem, onClose: closeDropdown, onSelect: (listItem) => {
32
- const original = items.find((candidate) => candidate.value === listItem.id);
33
- if (original) {
34
- onSelect(original.value);
35
- }
36
- closeDropdown();
37
- } })) }));
38
- }
39
- const valueForOperator = (operator, previous) => {
40
- switch (operatorArity(operator)) {
41
- case "none":
42
- return { kind: "none" };
43
- case "range":
44
- // NaN = a bound not yet entered (rendered blank by rangeText). isRuleComplete blocks Save
45
- // until both bounds are finite, so NaN stays dialog-local and never persists (it would
46
- // JSON-serialize to null); the contract type can stay a tight `{ from: number; to: number }`.
47
- return previous.kind === "literalRange" ? previous : { kind: "literalRange", from: NaN, to: NaN };
48
- case "single":
49
- return previous.kind === "literal" ? previous : { kind: "literal", value: "" };
50
- }
51
- };
52
- const literalText = (value) => value.kind === "literal" ? String(value.value) : "";
53
- const rangeText = (value, bound) => {
54
- if (value.kind !== "literalRange") {
55
- return "";
56
- }
57
- const n = value[bound];
58
- return Number.isFinite(n) ? String(n) : "";
59
- };
60
- // The ColorDropdown trigger swatch. ColorDropdown clones it with selectable-child props the swatch
61
- // ignores. No color renders a "none" slash rather than a misleading white.
62
- function CfSwatch({ hex }) {
63
- return (_jsx("span", { className: `gd-cf-swatch${hex ? "" : " gd-cf-swatch--none"}`, style: hex ? { backgroundColor: hex } : undefined }));
64
- }
65
- function CfColorPick({ hex, noneColorLabel, onChange }) {
66
- return (_jsx(ColorDropdown, { colorPalette: CF_COLOR_PALETTE, selectedColorItem: hexToColor(hex), onColorSelected: (color) => onChange(colorToHex(color)), onClear: () => onChange(undefined), noneColorAriaLabel: noneColorLabel, children: _jsx(CfSwatch, { hex: hex }) }));
67
- }
68
- const PREVIEW_SAMPLE = {
69
- measure: "1,234.56",
70
- attribute: "Abc",
71
- };
72
- // Row scope shows two sample cells to convey that the whole row is formatted.
73
- const ROW_PREVIEW_SAMPLE = ["Abc", "123"];
74
- function FormatEditor({ format, intl, onChange }) {
75
- return (_jsx(Dropdown, { closeOnParentScroll: true, closeOnMouseDrag: true, renderButton: ({ toggleDropdown }) => (_jsx(Button, { className: "gd-button-link gd-button-icon-only gd-icon-pencil gd-cf-condition__edit", accessibilityConfig: {
76
- ariaLabel: intl.formatMessage(conditionalFormattingMessages.dialogFormat),
77
- }, onClick: toggleDropdown })), renderBody: () => (_jsxs("div", { className: "gd-cf-format", children: [
78
- _jsx("span", { className: "gd-cf-format__label", children: intl.formatMessage(conditionalFormattingMessages.dialogTextColor) }), _jsx(CfColorPick, { hex: format.color, noneColorLabel: intl.formatMessage(conditionalFormattingMessages.dialogNoColor), onChange: (color) => onChange({ ...format, color }) }), _jsx("span", { className: "gd-cf-format__label", children: intl.formatMessage(conditionalFormattingMessages.dialogFillColor) }), _jsx(CfColorPick, { hex: format.backgroundColor, noneColorLabel: intl.formatMessage(conditionalFormattingMessages.dialogNoColor), onChange: (backgroundColor) => onChange({ ...format, backgroundColor }) }), _jsxs(UiButtonSegmentedControl, { children: [
79
- _jsx(UiButton, { size: "small", variant: "secondary", label: intl.formatMessage(conditionalFormattingMessages.dialogScopeCell), isSelected: format.scope === "cell", onClick: () => onChange({ ...format, scope: "cell" }) }), _jsx(UiButton, { size: "small", variant: "secondary", label: intl.formatMessage(conditionalFormattingMessages.dialogScopeRow), isSelected: format.scope === "row", onClick: () => onChange({ ...format, scope: "row" }) })
80
- ] })
81
- ] })) }));
82
- }
83
- function ConditionEditor({ condition, kind, removable, intl, onChange, onRemove }) {
84
- const operatorItems = operatorsForKind(kind).map((operator) => ({
85
- value: operator,
86
- title: intl.formatMessage(conditionalFormattingOperatorMessages[operator]),
87
- icon: operatorIcon(operator),
88
- }));
89
- const arity = operatorArity(condition.operator);
90
- const setValue = (value) => onChange({ ...condition, value });
91
- const setFormat = (format) => onChange({ ...condition, format });
92
- const parseBound = (v) => (v === "" ? NaN : Number(v));
93
- const range = condition.value.kind === "literalRange" ? condition.value : { from: NaN, to: NaN };
94
- const setRange = (from, to) => setValue({ kind: "literalRange", from, to });
95
- const previewStyle = {
96
- ...(condition.format.color ? { color: condition.format.color } : {}),
97
- ...(condition.format.backgroundColor ? { backgroundColor: condition.format.backgroundColor } : {}),
98
- };
99
- return (_jsxs("div", { className: "gd-cf-condition", children: [
100
- _jsxs("div", { className: "gd-cf-condition__header", children: [
101
- _jsx("span", { className: "gd-cf-condition__title", children: intl.formatMessage(conditionalFormattingMessages.dialogCondition) }), removable ? (_jsx(Button, { className: "gd-button-link gd-button-icon-only gd-icon-cross", accessibilityConfig: {
102
- ariaLabel: intl.formatMessage(conditionalFormattingMessages.dialogRemoveCondition),
103
- }, onClick: onRemove })) : null] }), _jsx(CfSelect, { value: condition.operator, items: operatorItems, onSelect: (operator) => onChange({ ...condition, operator, value: valueForOperator(operator, condition.value) }) }), arity === "single" ? (_jsx(Input, { type: kind === "measure" ? "number" : "text", value: literalText(condition.value), placeholder: intl.formatMessage(conditionalFormattingMessages.dialogValuePlaceholder), onChange: (v) => setValue({ kind: "literal", value: String(v) }) })) : null, arity === "range" ? (_jsxs("div", { className: "gd-cf-condition__range", children: [
104
- _jsx(Input, { type: "number", value: rangeText(condition.value, "from"), placeholder: intl.formatMessage(conditionalFormattingMessages.dialogFromPlaceholder), onChange: (v) => setRange(parseBound(v), range.to) }), _jsx(Input, { type: "number", value: rangeText(condition.value, "to"), placeholder: intl.formatMessage(conditionalFormattingMessages.dialogToPlaceholder), onChange: (v) => setRange(range.from, parseBound(v)) })
105
- ] })) : null, _jsxs("div", { className: "gd-cf-condition__format-row", children: [
106
- _jsx("span", { className: "gd-cf-dialog__label", children: intl.formatMessage(conditionalFormattingMessages.dialogFormat) }), _jsx("div", { className: "gd-cf-preview", children: condition.format.scope === "row" ? (ROW_PREVIEW_SAMPLE.map((sample, index) => (_jsx("span", { className: "gd-cf-preview__cell", style: previewStyle, children: sample }, index)))) : (_jsx("span", { className: "gd-cf-preview__cell", style: previewStyle, children: PREVIEW_SAMPLE[kind] })) }), _jsx(FormatEditor, { format: condition.format, intl: intl, onChange: setFormat })
107
- ] })
108
- ] }));
109
- }
5
+ import { Button, Overlay } from "@gooddata/sdk-ui-kit";
6
+ import { conditionalFormattingMessages } from "../../../../locales.js";
7
+ import { CfSelect } from "./CfSelect.js";
8
+ import { findTargetOption, isRuleComplete, newCondition, ruleWithTarget, sanitizeRuleForEditing, targetIcon, } from "./conditionalFormattingModel.js";
9
+ import { ConditionEditor } from "./ConditionEditor.js";
10
+ import { ReorderList } from "./ReorderList.js";
110
11
  // The config panel sits on the right edge of the screen, so open the popover to the LEFT of the
111
12
  // anchor (self's right edge to the anchor's left edge); fall back to the right when there's no room.
112
13
  const POPOVER_ALIGN_POINTS = [
113
14
  { align: "cl cr", offset: { x: -5, y: 0 } },
114
15
  { align: "cr cl", offset: { x: 5, y: 0 } },
115
16
  ];
116
- export function ConditionalFormattingDialog({ rule: initialRule, isNew, targetOptions, alignTo, onSave, onClose, }) {
17
+ export function ConditionalFormattingDialog({ rule: initialRule, isNew, targetOptions, separators, alignTo, onSave, onClose, }) {
117
18
  const intl = useIntl();
118
- const [rule, setRule] = useState(initialRule);
19
+ const [rule, setRule] = useState(() => sanitizeRuleForEditing(initialRule));
119
20
  const updateCondition = (id, next) => setRule((current) => ({
120
21
  ...current,
121
22
  conditions: current.conditions.map((condition) => (condition.id === id ? next : condition)),
@@ -125,19 +26,17 @@ export function ConditionalFormattingDialog({ rule: initialRule, isNew, targetOp
125
26
  conditions: current.conditions.filter((condition) => condition.id !== id),
126
27
  }));
127
28
  const addCondition = () => setRule((current) => ({ ...current, conditions: [...current.conditions, newCondition()] }));
29
+ const reorderConditions = (conditions) => setRule((current) => ({ ...current, conditions }));
128
30
  const changeTarget = (value) => {
129
31
  const option = targetOptions.find((candidate) => candidate.value === value);
130
32
  if (!option) {
131
33
  return;
132
34
  }
133
- setRule((current) =>
134
- // Switching target kind (measure <-> attribute) invalidates the operators, so reset the
135
- // conditions; same-kind switches keep the existing conditions.
136
- option.target.kind === current.target.kind
137
- ? { ...current, target: option.target }
138
- : { ...current, target: option.target, conditions: [newCondition()] });
35
+ setRule((current) => ruleWithTarget(current, option, findTargetOption(targetOptions, current.target)));
139
36
  };
140
37
  const selectedTarget = findTargetOption(targetOptions, rule.target);
38
+ const isPercent = selectedTarget?.isPercent ?? false;
39
+ const suggestions = selectedTarget?.elements ?? [];
141
40
  const complete = isRuleComplete(rule);
142
41
  const title = intl.formatMessage(isNew ? conditionalFormattingMessages.dialogAddTitle : conditionalFormattingMessages.dialogEditTitle);
143
42
  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: [
@@ -146,11 +45,11 @@ export function ConditionalFormattingDialog({ rule: initialRule, isNew, targetOp
146
45
  ariaLabel: intl.formatMessage(conditionalFormattingMessages.dialogCancel),
147
46
  }, onClick: onClose })
148
47
  ] }), _jsxs("div", { className: "gd-cf-dialog__body", children: [
149
- _jsx("span", { className: "gd-cf-dialog__label", children: intl.formatMessage(conditionalFormattingMessages.dialogTarget) }), _jsx(CfSelect, { value: selectedTarget?.value ?? "", items: targetOptions.map((option) => ({
48
+ _jsx("span", { className: "gd-cf-dialog__label", children: intl.formatMessage(conditionalFormattingMessages.dialogTarget) }), _jsx(CfSelect, { value: selectedTarget?.value, items: targetOptions.map((option) => ({
150
49
  value: option.value,
151
50
  title: option.title,
152
51
  icon: targetIcon(option.target.kind),
153
- })), onSelect: changeTarget, placeholder: intl.formatMessage(conditionalFormattingMessages.dialogSelectTarget) }), rule.conditions.map((condition) => (_jsx(ConditionEditor, { condition: condition, kind: rule.target.kind, removable: rule.conditions.length > 1, intl: intl, onChange: (next) => updateCondition(condition.id, next), onRemove: () => removeCondition(condition.id) }, condition.id))), _jsx(Button, { className: "gd-button-secondary gd-cf-dialog__add-condition", iconLeft: "gd-icon-plus", value: intl.formatMessage(conditionalFormattingMessages.dialogAddCondition), onClick: addCondition })
52
+ })), onSelect: changeTarget, placeholder: intl.formatMessage(conditionalFormattingMessages.dialogSelectTarget) }), _jsx(ReorderList, { items: rule.conditions, getKey: (condition) => condition.id, onReorder: reorderConditions, renderItem: (condition, slot) => (_jsx(ConditionEditor, { condition: condition, kind: rule.target.kind, isPercent: isPercent, separators: separators, suggestions: suggestions, removable: rule.conditions.length > 1, slot: slot, intl: intl, 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 })
154
53
  ] }), _jsxs("div", { className: "gd-cf-dialog__footer", children: [
155
54
  _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: !complete, onClick: () => onSave(rule) })
156
55
  ] })
@@ -1,12 +1,14 @@
1
- import { type IInsightDefinition } from "@gooddata/sdk-model";
1
+ import { type IInsightDefinition, type ISeparators } from "@gooddata/sdk-model";
2
2
  import { type IVisualizationProperties } from "../../../interfaces/Visualization.js";
3
+ import { type ICfTargetData } from "./conditionalFormattingModel.js";
3
4
  export interface IConditionalFormattingSectionProps {
4
5
  properties?: IVisualizationProperties;
5
6
  propertiesMeta?: Record<string, unknown>;
6
7
  insight?: IInsightDefinition;
7
- titlesByLocalId?: Record<string, string>;
8
+ targetData?: ICfTargetData;
9
+ separators?: ISeparators;
8
10
  isLoading?: boolean;
9
11
  pushData?: (data: unknown) => void;
10
12
  }
11
- export declare function ConditionalFormattingSection({ properties, propertiesMeta, insight, titlesByLocalId, isLoading, pushData }: IConditionalFormattingSectionProps): import("react/jsx-runtime").JSX.Element;
13
+ export declare function ConditionalFormattingSection({ properties, propertiesMeta, insight, targetData, separators, isLoading, pushData }: IConditionalFormattingSectionProps): import("react/jsx-runtime").JSX.Element;
12
14
  //# 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":"AAOA,OAAO,EAAE,KAAK,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAK9D,OAAO,EAAE,KAAK,wBAAwB,EAAE,MAAM,sCAAsC,CAAC;AAoBrF,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,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACzC,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;CACtC;AAmCD,wBAAgB,4BAA4B,CAAC,EACzC,UAAU,EACV,cAAc,EACd,OAAO,EACP,eAAe,EACf,SAAS,EACT,QAAQ,EACX,EAAE,kCAAkC,2CAsGpC"}
1
+ {"version":3,"file":"ConditionalFormattingSection.d.ts","sourceRoot":"","sources":["../../../../../src/internal/components/configurationControls/conditionalFormatting/ConditionalFormattingSection.tsx"],"names":[],"mappings":"AAQA,OAAO,EAAE,KAAK,kBAAkB,EAAE,KAAK,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAKhF,OAAO,EAAE,KAAK,wBAAwB,EAAE,MAAM,sCAAsC,CAAC;AAIrF,OAAO,EACH,KAAK,aAAa,EAOrB,MAAM,iCAAiC,CAAC;AAUzC,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,UAAU,CAAC,EAAE,WAAW,CAAC;IACzB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;CACtC;AAyCD,wBAAgB,4BAA4B,CAAC,EACzC,UAAU,EACV,cAAc,EACd,OAAO,EACP,UAAU,EACV,UAAU,EACV,SAAS,EACT,QAAQ,EACX,EAAE,kCAAkC,2CA4GpC"}
@@ -1,29 +1,29 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  // (C) 2026 GoodData Corporation
3
3
  import { useState } from "react";
4
+ import cx from "classnames";
4
5
  import { cloneDeep, set } from "lodash-es";
5
6
  import { useIntl } from "react-intl";
6
- import { Button } from "@gooddata/sdk-ui-kit";
7
+ import { Button, UiIconButton } from "@gooddata/sdk-ui-kit";
7
8
  import { conditionalFormattingMessages } from "../../../../locales.js";
8
9
  import { ConfigSection } from "../ConfigSection.js";
9
10
  import { ConditionalFormattingDialog } from "./ConditionalFormattingDialog.js";
10
11
  import { buildTargetOptions, findTargetOption, newRule, targetIcon, targetLocalId, } from "./conditionalFormattingModel.js";
12
+ import { ReorderList } from "./ReorderList.js";
11
13
  const SECTION_ID = "conditionalFormatting_section";
12
- // A saved rule: its target type icon (ABC / metric) + title; the delete (trash) appears on hover.
13
- // Reorder is preserved as rules-array order (first-match-wins) — drag-to-reorder UI is deferred.
14
- function RuleChip({ rule, option, labels, onEdit, onDelete }) {
15
- return (_jsxs("div", { className: "gd-cf-rule", children: [
16
- _jsxs("button", { type: "button", className: "gd-cf-rule__body", onClick: onEdit, title: labels.edit, children: [
17
- _jsx("span", { className: `gd-cf-type-icon ${targetIcon(rule.target.kind)}`, "aria-hidden": "true" }), _jsx("span", { className: "gd-cf-rule__title", children: option ? option.title : targetLocalId(rule.target) }), option ? null : _jsx("span", { className: "gd-cf-rule__invalid", children: labels.invalid })] }), _jsx(Button, { className: "gd-button-link gd-button-icon-only gd-icon-trash gd-cf-rule__delete", accessibilityConfig: { ariaLabel: labels.delete }, onClick: onDelete })
14
+ // Array order = evaluation order (first-match-wins).
15
+ function RuleChip({ rule, option, labels, slot, onEdit, onDelete }) {
16
+ return (_jsxs("div", { className: cx("gd-cf-rule", slot.className), ...slot.rootProps, children: [slot.handle, _jsxs("button", { type: "button", className: "gd-cf-rule__body", onClick: onEdit, title: labels.edit, children: [
17
+ _jsx("span", { className: `gd-cf-type-icon ${targetIcon(rule.target.kind)}`, "aria-hidden": "true" }), _jsx("span", { className: "gd-cf-rule__title", children: option ? option.title : targetLocalId(rule.target) }), option ? null : _jsx("span", { className: "gd-cf-rule__invalid", children: labels.invalid })] }), _jsx("span", { className: "gd-cf-rule__delete", children: _jsx(UiIconButton, { icon: "trash", size: "small", variant: "tertiary", isDesctructive: true, label: labels.delete, onClick: onDelete }) })
18
18
  ] }));
19
19
  }
20
- export function ConditionalFormattingSection({ properties, propertiesMeta, insight, titlesByLocalId, isLoading, pushData, }) {
20
+ export function ConditionalFormattingSection({ properties, propertiesMeta, insight, targetData, separators, isLoading, pushData, }) {
21
21
  const intl = useIntl();
22
22
  const [dialog, setDialog] = useState(null);
23
23
  const config = properties?.controls?.["conditionalFormatting"];
24
24
  const rules = config?.rules ?? [];
25
25
  const enabled = config?.enabled ?? false;
26
- const targetOptions = insight ? buildTargetOptions(insight, titlesByLocalId) : [];
26
+ const targetOptions = insight ? buildTargetOptions(insight, targetData) : [];
27
27
  const canAddRule = !isLoading && targetOptions.length > 0;
28
28
  const commit = (rulesNext, enabledNext) => {
29
29
  // Spread the existing config so version (and any future cross-stack fields) survive an edit.
@@ -50,5 +50,5 @@ export function ConditionalFormattingSection({ properties, propertiesMeta, insig
50
50
  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: [
51
51
  _jsx("span", { className: "s-cf-popover-anchor gd-cf-section__anchor" }), _jsxs("div", { className: "gd-cf-section__subcategory", children: [
52
52
  _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({ rule: newRule(targetOptions[0].target), isNew: true }) })
53
- ] }), rules.length === 0 ? (_jsx("div", { className: "gd-cf-section__empty", children: intl.formatMessage(conditionalFormattingMessages.empty) })) : (_jsx("div", { className: "gd-cf-section__rules", children: rules.map((rule) => (_jsx(RuleChip, { rule: rule, option: findTargetOption(targetOptions, rule.target), labels: chipLabels, onEdit: () => setDialog({ rule, isNew: false }), onDelete: () => deleteRule(rule.id) }, rule.id))) })), dialog ? (_jsx(ConditionalFormattingDialog, { rule: dialog.rule, isNew: dialog.isNew, targetOptions: targetOptions, alignTo: ".s-cf-popover-anchor", onSave: (rule) => saveRule(rule, dialog.isNew), onClose: () => setDialog(null) }, dialog.rule.id)) : null] }));
53
+ ] }), rules.length === 0 ? (_jsx("div", { className: "gd-cf-section__empty", children: intl.formatMessage(conditionalFormattingMessages.empty) })) : (_jsx("div", { className: "gd-cf-section__rules", children: _jsx(ReorderList, { items: rules, getKey: (rule) => rule.id, onReorder: (next) => commit(next, enabled), renderItem: (rule, slot) => (_jsx(RuleChip, { rule: rule, option: findTargetOption(targetOptions, rule.target), labels: chipLabels, slot: slot, onEdit: () => setDialog({ rule, isNew: false }), onDelete: () => deleteRule(rule.id) })) }) })), dialog ? (_jsx(ConditionalFormattingDialog, { rule: dialog.rule, isNew: dialog.isNew, targetOptions: targetOptions, separators: separators, alignTo: ".s-cf-popover-anchor", onSave: (rule) => saveRule(rule, dialog.isNew), onClose: () => setDialog(null) }, dialog.rule.id)) : null] }));
54
54
  }
@@ -0,0 +1,25 @@
1
+ import { type HTMLAttributes, type ReactNode } from "react";
2
+ type IReorderRootProps = Pick<HTMLAttributes<HTMLElement>, "draggable" | "onPointerDown" | "onDragStart" | "onDragEnter" | "onDragOver" | "onDragLeave" | "onDrop" | "onDragEnd">;
3
+ export interface IReorderSlot {
4
+ /** Grip element to render inside the row; null when the list is not reorderable. */
5
+ handle: ReactNode;
6
+ /** Spread onto the row root element. */
7
+ rootProps: IReorderRootProps;
8
+ /** Reorder classes to merge into the row's own className. */
9
+ className: string;
10
+ }
11
+ export interface IReorderListProps<T> {
12
+ items: readonly T[];
13
+ getKey: (item: T) => string;
14
+ /** Receives the full reordered array. */
15
+ onReorder: (items: T[]) => void;
16
+ renderItem: (item: T, slot: IReorderSlot, index: number) => ReactNode;
17
+ }
18
+ /**
19
+ * Drag-to-reorder for a small vertical list, on native HTML5 DnD (no dependency, no provider).
20
+ * The render prop keeps the consumer's own row markup; the whole row drags (the grip is a hover
21
+ * affordance). Pointer-only — no keyboard reorder yet.
22
+ */
23
+ export declare function ReorderList<T>({ items, getKey, onReorder, renderItem }: IReorderListProps<T>): import("react/jsx-runtime").JSX.Element;
24
+ export {};
25
+ //# sourceMappingURL=ReorderList.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ReorderList.d.ts","sourceRoot":"","sources":["../../../../../src/internal/components/configurationControls/conditionalFormatting/ReorderList.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAY,KAAK,cAAc,EAAE,KAAK,SAAS,EAAoB,MAAM,OAAO,CAAC;AAMxF,KAAK,iBAAiB,GAAG,IAAI,CACzB,cAAc,CAAC,WAAW,CAAC,EACzB,WAAW,GACX,eAAe,GACf,aAAa,GACb,aAAa,GACb,YAAY,GACZ,aAAa,GACb,QAAQ,GACR,WAAW,CAChB,CAAC;AAEF,MAAM,WAAW,YAAY;IACzB,oFAAoF;IACpF,MAAM,EAAE,SAAS,CAAC;IAClB,wCAAwC;IACxC,SAAS,EAAE,iBAAiB,CAAC;IAC7B,6DAA6D;IAC7D,SAAS,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,iBAAiB,CAAC,CAAC;IAChC,KAAK,EAAE,SAAS,CAAC,EAAE,CAAC;IACpB,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,MAAM,CAAC;IAC5B,yCAAyC;IACzC,SAAS,EAAE,CAAC,KAAK,EAAE,CAAC,EAAE,KAAK,IAAI,CAAC;IAChC,UAAU,EAAE,CAAC,IAAI,EAAE,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,MAAM,KAAK,SAAS,CAAC;CACzE;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,UAAU,EAAE,EAAE,iBAAiB,CAAC,CAAC,CAAC,2CA6F5F"}