@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.
- package/NOTICE +3 -3
- package/esm/internal/components/configurationControls/conditionalFormatting/CfSelect.d.ts +14 -0
- package/esm/internal/components/configurationControls/conditionalFormatting/CfSelect.d.ts.map +1 -0
- package/esm/internal/components/configurationControls/conditionalFormatting/CfSelect.js +25 -0
- package/esm/internal/components/configurationControls/conditionalFormatting/ConditionEditor.d.ts +18 -0
- package/esm/internal/components/configurationControls/conditionalFormatting/ConditionEditor.d.ts.map +1 -0
- package/esm/internal/components/configurationControls/conditionalFormatting/ConditionEditor.js +132 -0
- package/esm/internal/components/configurationControls/conditionalFormatting/ConditionalFormattingDialog.d.ts +4 -1
- package/esm/internal/components/configurationControls/conditionalFormatting/ConditionalFormattingDialog.d.ts.map +1 -1
- package/esm/internal/components/configurationControls/conditionalFormatting/ConditionalFormattingDialog.js +15 -116
- package/esm/internal/components/configurationControls/conditionalFormatting/ConditionalFormattingSection.d.ts +5 -3
- package/esm/internal/components/configurationControls/conditionalFormatting/ConditionalFormattingSection.d.ts.map +1 -1
- package/esm/internal/components/configurationControls/conditionalFormatting/ConditionalFormattingSection.js +10 -10
- package/esm/internal/components/configurationControls/conditionalFormatting/ReorderList.d.ts +25 -0
- package/esm/internal/components/configurationControls/conditionalFormatting/ReorderList.d.ts.map +1 -0
- package/esm/internal/components/configurationControls/conditionalFormatting/ReorderList.js +94 -0
- package/esm/internal/components/configurationControls/conditionalFormatting/conditionalFormattingModel.d.ts +66 -7
- package/esm/internal/components/configurationControls/conditionalFormatting/conditionalFormattingModel.d.ts.map +1 -1
- package/esm/internal/components/configurationControls/conditionalFormatting/conditionalFormattingModel.js +165 -21
- package/esm/internal/components/configurationControls/conditionalFormatting/reorderListModel.d.ts +4 -0
- package/esm/internal/components/configurationControls/conditionalFormatting/reorderListModel.d.ts.map +1 -0
- package/esm/internal/components/configurationControls/conditionalFormatting/reorderListModel.js +16 -0
- package/esm/internal/components/configurationPanels/ConfigurationPanelContent.d.ts +0 -1
- package/esm/internal/components/configurationPanels/ConfigurationPanelContent.d.ts.map +1 -1
- package/esm/internal/components/configurationPanels/PivotTableConfigurationPanel.d.ts +9 -2
- package/esm/internal/components/configurationPanels/PivotTableConfigurationPanel.d.ts.map +1 -1
- package/esm/internal/components/configurationPanels/PivotTableConfigurationPanel.js +4 -4
- package/esm/internal/components/pluggableVisualizations/pivotTableNext/PluggablePivotTableNext.d.ts +1 -1
- package/esm/internal/components/pluggableVisualizations/pivotTableNext/PluggablePivotTableNext.d.ts.map +1 -1
- package/esm/internal/components/pluggableVisualizations/pivotTableNext/PluggablePivotTableNext.js +8 -7
- package/esm/internal/translations/en-US.localization-bundle.d.ts +8 -0
- package/esm/internal/translations/en-US.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/en-US.localization-bundle.js +8 -0
- package/esm/locales.d.ts +6 -0
- package/esm/locales.d.ts.map +1 -1
- package/esm/locales.js +2 -0
- package/package.json +21 -21
- package/styles/css/main.css +102 -13
- package/styles/css/main.css.map +1 -1
- package/styles/internal/css/conditional_formatting.css +102 -13
- package/styles/internal/css/conditional_formatting.css.map +1 -1
- 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
|
+
Date generated: 2026-7-14
|
|
11
11
|
|
|
12
|
-
Revision ID:
|
|
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-
|
|
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
|
+
}
|
package/esm/internal/components/configurationControls/conditionalFormatting/ConditionEditor.d.ts
ADDED
|
@@ -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
|
package/esm/internal/components/configurationControls/conditionalFormatting/ConditionEditor.d.ts.map
ADDED
|
@@ -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"}
|
package/esm/internal/components/configurationControls/conditionalFormatting/ConditionEditor.js
ADDED
|
@@ -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":"
|
|
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 {
|
|
4
|
-
import cx from "classnames";
|
|
3
|
+
import { useState } from "react";
|
|
5
4
|
import { useIntl } from "react-intl";
|
|
6
|
-
import { Button,
|
|
7
|
-
import { conditionalFormattingMessages
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
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,
|
|
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":"
|
|
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
|
-
//
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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,
|
|
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,
|
|
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.
|
|
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
|
package/esm/internal/components/configurationControls/conditionalFormatting/ReorderList.d.ts.map
ADDED
|
@@ -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"}
|