@gooddata/sdk-ui-filters 11.45.0-alpha.1 → 11.45.0-alpha.2
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/esm/MeasureValueFilter/DimensionalitySection.d.ts +0 -5
- package/esm/MeasureValueFilter/DimensionalitySection.d.ts.map +1 -1
- package/esm/MeasureValueFilter/DimensionalitySection.js +12 -125
- package/esm/MeasureValueFilter/DropdownBody.d.ts.map +1 -1
- package/esm/MeasureValueFilter/DropdownBody.js +4 -5
- package/esm/MeasureValueFilter/useDimensionalityEditor.d.ts +72 -0
- package/esm/MeasureValueFilter/useDimensionalityEditor.d.ts.map +1 -0
- package/esm/MeasureValueFilter/useDimensionalityEditor.js +135 -0
- package/esm/RankingFilter/RankingAttributesSection.d.ts +44 -0
- package/esm/RankingFilter/RankingAttributesSection.d.ts.map +1 -0
- package/esm/RankingFilter/RankingAttributesSection.js +38 -0
- package/esm/RankingFilter/RankingFilterDropdown.d.ts +11 -1
- package/esm/RankingFilter/RankingFilterDropdown.d.ts.map +1 -1
- package/esm/RankingFilter/RankingFilterDropdown.js +7 -6
- package/esm/RankingFilter/RankingFilterDropdownBody.d.ts +15 -1
- package/esm/RankingFilter/RankingFilterDropdownBody.d.ts.map +1 -1
- package/esm/RankingFilter/RankingFilterDropdownBody.js +74 -11
- package/esm/sdk-ui-filters.d.ts +9 -0
- package/package.json +12 -12
|
@@ -1,10 +1,5 @@
|
|
|
1
1
|
import { type ObjRefInScope } from "@gooddata/sdk-model";
|
|
2
2
|
import type { IDimensionalityItem } from "./typings.js";
|
|
3
|
-
/**
|
|
4
|
-
* Compare two-dimensionality arrays for equality (order-insensitive).
|
|
5
|
-
* Returns true if they contain the same identifiers regardless of order.
|
|
6
|
-
*/
|
|
7
|
-
export declare const areDimensionalitySetsEqual: (a: IDimensionalityItem[] | undefined, b: IDimensionalityItem[] | undefined) => boolean;
|
|
8
3
|
interface IDimensionalitySectionProps {
|
|
9
4
|
/**
|
|
10
5
|
* Current dimensionality items in the filter.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DimensionalitySection.d.ts","sourceRoot":"","sources":["../../src/MeasureValueFilter/DimensionalitySection.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"DimensionalitySection.d.ts","sourceRoot":"","sources":["../../src/MeasureValueFilter/DimensionalitySection.tsx"],"names":[],"mappings":"AAMA,OAAO,EAAE,KAAK,aAAa,EAAkB,MAAM,qBAAqB,CAAC;AAIzE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAGxD,UAAU,2BAA2B;IACjC;;OAEG;IACH,cAAc,EAAE,mBAAmB,EAAE,CAAC;IACtC;;OAEG;IACH,qBAAqB,CAAC,EAAE,mBAAmB,EAAE,CAAC;IAC9C;;OAEG;IACH,qBAAqB,CAAC,EAAE,mBAAmB,EAAE,CAAC;IAC9C;;OAEG;IACH,yBAAyB,CAAC,EAAE,CAAC,cAAc,EAAE,aAAa,EAAE,KAAK,OAAO,CAAC,mBAAmB,EAAE,CAAC,CAAC;IAChG;;OAEG;IACH,8BAA8B,CAAC,EAAE,OAAO,CAAC;IACzC;;OAEG;IACH,sBAAsB,EAAE,CAAC,cAAc,EAAE,mBAAmB,EAAE,KAAK,IAAI,CAAC;IACxE;;;OAGG;IACH,gBAAgB,EAAE,OAAO,CAAC;CAC7B;AAED;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,mEAgJhC,CAAC"}
|
|
@@ -1,45 +1,11 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
// (C) 2025-2026 GoodData Corporation
|
|
3
|
-
import { memo
|
|
3
|
+
import { memo } from "react";
|
|
4
4
|
import { useIntl } from "react-intl";
|
|
5
|
-
import {
|
|
5
|
+
import { objRefToString } from "@gooddata/sdk-model";
|
|
6
6
|
import { Bubble, BubbleHoverTrigger, UiButton, UiIconButton, UiTag } from "@gooddata/sdk-ui-kit";
|
|
7
7
|
import { AttributePicker } from "./AttributePicker.js";
|
|
8
|
-
import {
|
|
9
|
-
function WithAddButton({ children, appendAddButton, isDisabled, tooltip, buttonRef, onClick, }) {
|
|
10
|
-
if (!appendAddButton) {
|
|
11
|
-
return children;
|
|
12
|
-
}
|
|
13
|
-
return (_jsxs("div", { className: "gd-mvf-dimensionality-add-wrapper", children: [children, _jsxs(BubbleHoverTrigger, { children: [
|
|
14
|
-
_jsx(UiIconButton, { ref: buttonRef, icon: "plus", size: "small", variant: "tertiary", isDisabled: isDisabled, onClick: onClick, dataTestId: "mvf-dimensionality-plus" }), _jsx(Bubble, { alignPoints: [{ align: "bc tc" }], children: tooltip })
|
|
15
|
-
] })
|
|
16
|
-
] }));
|
|
17
|
-
}
|
|
18
|
-
/**
|
|
19
|
-
* Compare two-dimensionality arrays for equality (order-insensitive).
|
|
20
|
-
* Returns true if they contain the same identifiers regardless of order.
|
|
21
|
-
*/
|
|
22
|
-
export const areDimensionalitySetsEqual = (a, b) => {
|
|
23
|
-
const aItems = a ?? [];
|
|
24
|
-
const bItems = b ?? [];
|
|
25
|
-
if (aItems.length !== bItems.length) {
|
|
26
|
-
return false;
|
|
27
|
-
}
|
|
28
|
-
// Check that every item in 'a' exists in 'b'
|
|
29
|
-
return aItems.every((aItem) => bItems.some((bItem) => areObjRefsEqual(aItem.identifier, bItem.identifier)));
|
|
30
|
-
};
|
|
31
|
-
const areDimensionalityItemsDeepEqual = (availableItem, filterItem) => {
|
|
32
|
-
if (availableItem.ref && filterItem.ref) {
|
|
33
|
-
return areObjRefsEqual(availableItem.ref, filterItem.ref);
|
|
34
|
-
}
|
|
35
|
-
if (availableItem.ref) {
|
|
36
|
-
return areObjRefsEqual(availableItem.ref, filterItem.identifier);
|
|
37
|
-
}
|
|
38
|
-
if (filterItem.ref) {
|
|
39
|
-
return areObjRefsEqual(availableItem.identifier, filterItem.ref);
|
|
40
|
-
}
|
|
41
|
-
return areObjRefsEqual(availableItem.identifier, filterItem.identifier);
|
|
42
|
-
};
|
|
8
|
+
import { WithAddButton, useDimensionalityEditor } from "./useDimensionalityEditor.js";
|
|
43
9
|
/**
|
|
44
10
|
* @internal
|
|
45
11
|
* Dimensionality section for the measure value filter dropdown.
|
|
@@ -47,108 +13,29 @@ const areDimensionalityItemsDeepEqual = (availableItem, filterItem) => {
|
|
|
47
13
|
*/
|
|
48
14
|
export const DimensionalitySection = memo(function DimensionalitySection({ dimensionality, insightDimensionality, catalogDimensionality, loadCatalogDimensionality, isLoadingCatalogDimensionality, onDimensionalityChange, isMigratedFilter, }) {
|
|
49
15
|
const intl = useIntl();
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const { lazyCatalogDimensionality, isLoadingLazyCatalogDimensionality } = useLazyCatalogDimensionality({
|
|
58
|
-
isOpen: isAttributePickerOpen,
|
|
59
|
-
dimensionality,
|
|
60
|
-
loadCatalogDimensionality,
|
|
16
|
+
const { inlineAddButtonRef, isAttributePickerOpen, actualAnchor, shouldShowResetButton, availableInsightItems, availableCatalogItems, isLoadingCatalogForPicker, handleRemove: handleRemoveDimensionality, handleReset: handleResetDimensionality, handleOpenInlinePicker: handleOpenInlineAttributePicker, handleOpenStandalonePicker: handleOpenStandaloneAttributePicker, handleClosePicker: handleCloseAttributePicker, handleAddItems: handleAddDimensionalityItems, } = useDimensionalityEditor({
|
|
17
|
+
items: dimensionality,
|
|
18
|
+
insightItems: insightDimensionality,
|
|
19
|
+
catalogItems: catalogDimensionality,
|
|
20
|
+
loadCatalog: loadCatalogDimensionality,
|
|
21
|
+
isLoadingCatalog: isLoadingCatalogDimensionality,
|
|
22
|
+
onChange: onDimensionalityChange,
|
|
61
23
|
});
|
|
62
|
-
/**
|
|
63
|
-
* Check if the current dimensionality differs from the insight defaults (order-insensitive).
|
|
64
|
-
* Reset button should be visible when they differ.
|
|
65
|
-
*/
|
|
66
|
-
const shouldShowResetButton = useMemo(() => {
|
|
67
|
-
return !areDimensionalitySetsEqual(dimensionality, insightDimensionality);
|
|
68
|
-
}, [dimensionality, insightDimensionality]);
|
|
69
|
-
/**
|
|
70
|
-
* Compute available items for the AttributePicker.
|
|
71
|
-
* Keep insight items and catalog items separate so the picker does not need to guess origin.
|
|
72
|
-
*/
|
|
73
|
-
const availableInsightItems = useMemo(() => {
|
|
74
|
-
return (insightDimensionality ?? []).filter((availableItem) => !dimensionality.some((filterItem) => areDimensionalityItemsDeepEqual(availableItem, filterItem)));
|
|
75
|
-
}, [insightDimensionality, dimensionality]);
|
|
76
|
-
const availableCatalogItems = useMemo(() => {
|
|
77
|
-
const effectiveCatalog = loadCatalogDimensionality
|
|
78
|
-
? (lazyCatalogDimensionality ?? [])
|
|
79
|
-
: (catalogDimensionality ?? []);
|
|
80
|
-
const selectedFilteredOut = effectiveCatalog.filter((availableItem) => !dimensionality.some((filterItem) => areDimensionalityItemsDeepEqual(availableItem, filterItem)));
|
|
81
|
-
// Prevent duplicates between "From visualization" and catalog lists.
|
|
82
|
-
// Insight items may be represented as LocalIdRefs; when available, use `ref` (display-form ObjRef)
|
|
83
|
-
// for stable deduplication against catalog candidates (which use ObjRefs).
|
|
84
|
-
const insightRefKeys = new Set(availableInsightItems
|
|
85
|
-
.map((i) => (i.ref ? objRefToString(i.ref) : undefined))
|
|
86
|
-
.filter((x) => !!x));
|
|
87
|
-
const insightTitlesWithoutRef = new Set(availableInsightItems.filter((i) => !i.ref).map((i) => i.title));
|
|
88
|
-
return selectedFilteredOut.filter((item) => {
|
|
89
|
-
const itemKey = objRefToString(item.identifier);
|
|
90
|
-
if (insightRefKeys.has(itemKey)) {
|
|
91
|
-
return false;
|
|
92
|
-
}
|
|
93
|
-
// Fallback for environments where insight items do not provide refs.
|
|
94
|
-
return !insightTitlesWithoutRef.has(item.title);
|
|
95
|
-
});
|
|
96
|
-
}, [
|
|
97
|
-
catalogDimensionality,
|
|
98
|
-
dimensionality,
|
|
99
|
-
availableInsightItems,
|
|
100
|
-
lazyCatalogDimensionality,
|
|
101
|
-
loadCatalogDimensionality,
|
|
102
|
-
]);
|
|
103
|
-
const handleRemoveDimensionality = useCallback((index) => {
|
|
104
|
-
const newDimensionality = dimensionality.filter((_, i) => i !== index);
|
|
105
|
-
onDimensionalityChange(newDimensionality);
|
|
106
|
-
}, [dimensionality, onDimensionalityChange]);
|
|
107
|
-
/**
|
|
108
|
-
* Reset dimensionality to insight defaults (bucket attributes).
|
|
109
|
-
*/
|
|
110
|
-
const handleResetDimensionality = useCallback(() => {
|
|
111
|
-
const newDimensionality = insightDimensionality ?? [];
|
|
112
|
-
onDimensionalityChange(newDimensionality);
|
|
113
|
-
}, [insightDimensionality, onDimensionalityChange]);
|
|
114
|
-
const handleOpenInlineAttributePicker = useCallback(() => {
|
|
115
|
-
setAnchorType("inline");
|
|
116
|
-
setIsAttributePickerOpen(true);
|
|
117
|
-
}, []);
|
|
118
|
-
const handleOpenStandaloneAttributePicker = useCallback((event) => {
|
|
119
|
-
setAnchorType("standalone");
|
|
120
|
-
setStandaloneAnchor(event.currentTarget);
|
|
121
|
-
setIsAttributePickerOpen(true);
|
|
122
|
-
}, []);
|
|
123
|
-
const handleCloseAttributePicker = useCallback(() => {
|
|
124
|
-
setIsAttributePickerOpen(false);
|
|
125
|
-
setStandaloneAnchor(null);
|
|
126
|
-
}, []);
|
|
127
|
-
const handleAddDimensionalityItems = useCallback((items) => {
|
|
128
|
-
const newDimensionality = [...dimensionality, ...items];
|
|
129
|
-
onDimensionalityChange(newDimensionality);
|
|
130
|
-
setIsAttributePickerOpen(false);
|
|
131
|
-
setStandaloneAnchor(null);
|
|
132
|
-
}, [dimensionality, onDimensionalityChange]);
|
|
133
24
|
const addButtonTooltip = intl.formatMessage({ id: "mvf.dimensionality.addButton.tooltip" });
|
|
134
25
|
const resetButtonTooltip = intl.formatMessage({
|
|
135
26
|
id: "mvf.dimensionality.reset.tooltip",
|
|
136
27
|
});
|
|
137
|
-
// Determine the actual anchor element to use
|
|
138
|
-
const actualAnchor = anchorType === "inline" ? inlineAddButtonRef.current : standaloneAnchor;
|
|
139
28
|
return (_jsxs("div", { className: "gd-mvf-dropdown-section gd-mvf-dimensionality s-mvf-dimensionality", "data-testid": "mvf-dimensionality", children: [
|
|
140
29
|
_jsx("div", { className: "gd-mvf-dimensionality-header", children: _jsx("label", { children: intl.formatMessage({ id: "mvf.dimensionality.forEach" }) }) }), dimensionality.length > 0 ? (_jsxs("div", { className: "gd-mvf-dimensionality-items", children: [dimensionality.map((item, index) => {
|
|
141
30
|
// Select icon and color based on item type: date items get date icon (blue)
|
|
142
31
|
const isDateItem = item.type === "chronologicalDate" || item.type === "genericDate";
|
|
143
32
|
const icon = isDateItem ? "date" : "ldmAttribute";
|
|
144
33
|
const iconColor = isDateItem ? "primary" : "warning";
|
|
145
|
-
return (_jsx(WithAddButton, { appendAddButton: index === dimensionality.length - 1, isDisabled: false, tooltip: addButtonTooltip, buttonRef: inlineAddButtonRef, onClick: handleOpenInlineAttributePicker, children: _jsx("div", { className: "gd-mvf-dimensionality-tag-wrapper", children: _jsx(UiTag, { variant: "outlined", size: "large", iconBefore: icon, iconBeforeColor: iconColor, label: item.title, isDeletable: true, onDelete: () => handleRemoveDimensionality(index), dataTestId: `dimensionality-tag-${index}` }) }) }, objRefToString(item.identifier)));
|
|
34
|
+
return (_jsx(WithAddButton, { appendAddButton: index === dimensionality.length - 1, isDisabled: false, tooltip: addButtonTooltip, buttonRef: inlineAddButtonRef, onClick: handleOpenInlineAttributePicker, dataTestId: "mvf-dimensionality-plus", children: _jsx("div", { className: "gd-mvf-dimensionality-tag-wrapper", children: _jsx(UiTag, { variant: "outlined", size: "large", iconBefore: icon, iconBeforeColor: iconColor, label: item.title, isDeletable: true, onDelete: () => handleRemoveDimensionality(index), dataTestId: `dimensionality-tag-${index}` }) }) }, objRefToString(item.identifier)));
|
|
146
35
|
}), shouldShowResetButton ? (_jsx("div", { className: "gd-mvf-dimensionality-reset-button", children: _jsxs(BubbleHoverTrigger, { children: [
|
|
147
36
|
_jsx(UiIconButton, { icon: "history", size: "small", variant: "tertiary", onClick: handleResetDimensionality, label: resetButtonTooltip, dataTestId: "mvf-dimensionality-reset" }), _jsx(Bubble, { alignPoints: [{ align: "cr cl" }], children: resetButtonTooltip })
|
|
148
37
|
] }) })) : null] })) : (_jsxs("div", { className: "gd-mvf-dimensionality-empty-actions", children: [
|
|
149
38
|
_jsx(UiButton, { label: intl.formatMessage({ id: "mvf.dimensionality.addAttributes" }), variant: "tertiary", size: "small", iconBefore: "plus", onClick: handleOpenStandaloneAttributePicker }), shouldShowResetButton ? (_jsxs(BubbleHoverTrigger, { children: [
|
|
150
39
|
_jsx(UiButton, { size: "small", variant: "tertiary", iconBefore: "history", label: intl.formatMessage({ id: "mvf.dimensionality.reset" }), onClick: handleResetDimensionality, dataTestId: "mvf-dimensionality-reset" }), _jsx(Bubble, { alignPoints: [{ align: "cr cl" }], children: resetButtonTooltip })
|
|
151
|
-
] })) : null] })), isMigratedFilter ? null : (_jsx("div", { role: "status", "aria-live": "polite", className: "gd-message information gd-mvf-backward-compatibility-message", "data-testid": "mvf-backward-compatibility-note", children: _jsx("div", { className: "gd-message-text", children: intl.formatMessage({ id: "mvf.dimensionality.backwardCompatibilityNote" }) }) })), isAttributePickerOpen && actualAnchor ? (_jsx(AttributePicker, { anchorElement: actualAnchor, onAdd: handleAddDimensionalityItems, onCancel: handleCloseAttributePicker, availableInsightItems: availableInsightItems, availableCatalogItems: availableCatalogItems, isLoadingCatalogDimensionality:
|
|
152
|
-
? isLoadingLazyCatalogDimensionality
|
|
153
|
-
: isLoadingCatalogDimensionality })) : null] }));
|
|
40
|
+
] })) : null] })), isMigratedFilter ? null : (_jsx("div", { role: "status", "aria-live": "polite", className: "gd-message information gd-mvf-backward-compatibility-message", "data-testid": "mvf-backward-compatibility-note", children: _jsx("div", { className: "gd-message-text", children: intl.formatMessage({ id: "mvf.dimensionality.backwardCompatibilityNote" }) }) })), isAttributePickerOpen && actualAnchor ? (_jsx(AttributePicker, { anchorElement: actualAnchor, onAdd: handleAddDimensionalityItems, onCancel: handleCloseAttributePicker, availableInsightItems: availableInsightItems, availableCatalogItems: availableCatalogItems, isLoadingCatalogDimensionality: isLoadingCatalogForPicker })) : null] }));
|
|
154
41
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DropdownBody.d.ts","sourceRoot":"","sources":["../../src/MeasureValueFilter/DropdownBody.tsx"],"names":[],"mappings":"AAOA,OAAO,EACH,KAAK,sBAAsB,EAC3B,KAAK,WAAW,EAChB,KAAK,2BAA2B,EAChC,KAAK,aAAa,EAKrB,MAAM,qBAAqB,CAAC;AAa7B,OAAO,EAAiC,KAAK,0BAA0B,EAAE,MAAM,YAAY,CAAC;AAC5F,OAAO,EACH,KAAK,mBAAmB,EACxB,KAAK,uCAAuC,EAC5C,KAAK,mCAAmC,EACxC,KAAK,cAAc,EACtB,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"DropdownBody.d.ts","sourceRoot":"","sources":["../../src/MeasureValueFilter/DropdownBody.tsx"],"names":[],"mappings":"AAOA,OAAO,EACH,KAAK,sBAAsB,EAC3B,KAAK,WAAW,EAChB,KAAK,2BAA2B,EAChC,KAAK,aAAa,EAKrB,MAAM,qBAAqB,CAAC;AAa7B,OAAO,EAAiC,KAAK,0BAA0B,EAAE,MAAM,YAAY,CAAC;AAC5F,OAAO,EACH,KAAK,mBAAmB,EACxB,KAAK,uCAAuC,EAC5C,KAAK,mCAAmC,EACxC,KAAK,cAAc,EACtB,MAAM,cAAc,CAAC;AAItB,UAAU,kBAAmB,SAAQ,uCAAuC;IACxE,QAAQ,EAAE,0BAA0B,CAAC;IACrC,UAAU,EAAE,2BAA2B,EAAE,CAAC;IAC1C,wBAAwB,CAAC,EAAE,OAAO,CAAC;IACnC,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,mCAAmC,CAAC;IAC7C,QAAQ,CAAC,EAAE,mCAAmC,CAAC;IAC/C,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,UAAU,CAAC,EAAE,WAAW,CAAC;IACzB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,4BAA4B,CAAC,EAAE,OAAO,CAAC;IACvC,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAClC,cAAc,CAAC,EAAE,mBAAmB,EAAE,CAAC;IACvC,qBAAqB,CAAC,EAAE,mBAAmB,EAAE,CAAC;IAC9C,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAClC,sBAAsB,CAAC,EAAE,OAAO,CAAC;IACjC,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC,qBAAqB,CAAC,EAAE,mBAAmB,EAAE,CAAC;IAC9C,yBAAyB,CAAC,EAAE,CAAC,cAAc,EAAE,aAAa,EAAE,KAAK,OAAO,CAAC,mBAAmB,EAAE,CAAC,CAAC;IAChG,sBAAsB,CAAC,EAAE,CAAC,cAAc,EAAE,aAAa,EAAE,KAAK,IAAI,CAAC;IACnE,8BAA8B,CAAC,EAAE,OAAO,CAAC;IACzC,iBAAiB,CAAC,EAAE,MAAM,OAAO,CAAC,sBAAsB,GAAG,SAAS,CAAC,CAAC;IACtE,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,iFAAiF;IACjF,QAAQ,CAAC,EAAE,MAAM,CAAC;CACrB;AAoHD,eAAO,MAAM,oBAAoB,0DA04B/B,CAAC;AAEH,eAAO,MAAM,YAAY,0DAMvB,CAAC"}
|
|
@@ -8,13 +8,14 @@ import { IntlWrapper } from "@gooddata/sdk-ui";
|
|
|
8
8
|
import { UiIconButton, UiTooltip } from "@gooddata/sdk-ui-kit";
|
|
9
9
|
import { ConditionInputSection } from "./ConditionInputSection.js";
|
|
10
10
|
import { MEASURE_VALUE_FILTER_DROPDOWN_BODY_CLASS } from "./constants.js";
|
|
11
|
-
import { DimensionalitySection
|
|
11
|
+
import { DimensionalitySection } from "./DimensionalitySection.js";
|
|
12
12
|
import { intervalIncludesZero } from "./helpers/intervalIncludesZero.js";
|
|
13
13
|
import { MeasureValueFilterDropdownActions } from "./MeasureValueFilterDropdownActions.js";
|
|
14
14
|
import { MeasureValueFilterDropdownHeader } from "./MeasureValueFilterDropdownHeader.js";
|
|
15
15
|
import { OperatorDropdown } from "./OperatorDropdown.js";
|
|
16
16
|
import { PreviewSection } from "./PreviewSection.js";
|
|
17
17
|
import { TreatNullValuesAsZeroCheckbox } from "./TreatNullValuesAsZeroCheckbox.js";
|
|
18
|
+
import { areDimensionalitySetsEqual, isEmptyDimensionalityInvalid } from "./useDimensionalityEditor.js";
|
|
18
19
|
import { WarningMessageComponent } from "./WarningMessage.js";
|
|
19
20
|
const DefaultValuePrecision = 6;
|
|
20
21
|
/**
|
|
@@ -248,8 +249,7 @@ export const DropdownBodyWithIntl = memo(function DropdownBodyWithIntl(props) {
|
|
|
248
249
|
// disable the Apply button when the filter has no currently set dimensionality but insight has it
|
|
249
250
|
// (e.g., non-headline insight that can have the dimensionality) but only when supported via ff
|
|
250
251
|
if (isDimensionalityEnabled &&
|
|
251
|
-
state.dimensionality
|
|
252
|
-
(insightDimensionality?.length ?? 0) > 0) {
|
|
252
|
+
isEmptyDimensionalityInvalid(state.dimensionality, insightDimensionality)) {
|
|
253
253
|
return true;
|
|
254
254
|
}
|
|
255
255
|
const effectiveConditions = enableMultipleConditions
|
|
@@ -276,8 +276,7 @@ export const DropdownBodyWithIntl = memo(function DropdownBodyWithIntl(props) {
|
|
|
276
276
|
}
|
|
277
277
|
// Check for empty dimensionality first
|
|
278
278
|
if (isDimensionalityEnabled &&
|
|
279
|
-
state.dimensionality
|
|
280
|
-
(insightDimensionality?.length ?? 0) > 0) {
|
|
279
|
+
isEmptyDimensionalityInvalid(state.dimensionality, insightDimensionality)) {
|
|
281
280
|
return intl.formatMessage({ id: "mvf.applyButton.tooltip.emptyDimensionality" });
|
|
282
281
|
}
|
|
283
282
|
// Check for invalid conditions
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { type MouseEvent, type ReactNode, type RefObject } from "react";
|
|
2
|
+
import { type ObjRefInScope } from "@gooddata/sdk-model";
|
|
3
|
+
import type { IDimensionalityItem } from "./typings.js";
|
|
4
|
+
/**
|
|
5
|
+
* Compare two dimensionality arrays for equality (order-insensitive).
|
|
6
|
+
* Returns true if they contain the same identifiers regardless of order.
|
|
7
|
+
*/
|
|
8
|
+
export declare const areDimensionalitySetsEqual: (a: IDimensionalityItem[] | undefined, b: IDimensionalityItem[] | undefined) => boolean;
|
|
9
|
+
/**
|
|
10
|
+
* An empty dimensionality selection is only valid when the insight itself has no attributes. When the
|
|
11
|
+
* insight has attributes they are always the fallback, so an explicitly empty selection must be blocked
|
|
12
|
+
* (the Apply button is disabled). Shared by the measure value filter and the ranking filter "out of" section.
|
|
13
|
+
*/
|
|
14
|
+
export declare const isEmptyDimensionalityInvalid: (selection: IDimensionalityItem[] | undefined, insightDimensionality: IDimensionalityItem[] | undefined) => boolean;
|
|
15
|
+
interface IWithAddButtonProps {
|
|
16
|
+
children: ReactNode;
|
|
17
|
+
appendAddButton: boolean;
|
|
18
|
+
isDisabled: boolean;
|
|
19
|
+
tooltip: string;
|
|
20
|
+
buttonRef: RefObject<HTMLButtonElement | null>;
|
|
21
|
+
onClick: (event: MouseEvent<HTMLButtonElement>) => void;
|
|
22
|
+
/** Test id of the inline "+" button - differs between the measure value filter and the ranking filter. */
|
|
23
|
+
dataTestId: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Renders the children and, when {@link IWithAddButtonProps.appendAddButton} is set, an inline "+" button
|
|
27
|
+
* that opens the attribute picker. Shared by the measure value filter dimensionality and the ranking filter
|
|
28
|
+
* "out of" sections.
|
|
29
|
+
*/
|
|
30
|
+
export declare function WithAddButton({ children, appendAddButton, isDisabled, tooltip, buttonRef, onClick, dataTestId }: IWithAddButtonProps): ReactNode;
|
|
31
|
+
/**
|
|
32
|
+
* Inputs for {@link useDimensionalityEditor}. Neutral names so both the measure value filter (dimensionality)
|
|
33
|
+
* and the ranking filter ("out of" attributes) can use it.
|
|
34
|
+
*/
|
|
35
|
+
export interface IUseDimensionalityEditorParams {
|
|
36
|
+
/** The currently selected dimensionality items. */
|
|
37
|
+
items: IDimensionalityItem[];
|
|
38
|
+
/** Insight default items (from buckets), used for the reset action and the "From visualization" list. */
|
|
39
|
+
insightItems?: IDimensionalityItem[];
|
|
40
|
+
/** Catalog items, used when not lazily loaded via {@link IUseDimensionalityEditorParams.loadCatalog}. */
|
|
41
|
+
catalogItems?: IDimensionalityItem[];
|
|
42
|
+
/** Lazily loads catalog items valid for the current selection (on picker open). */
|
|
43
|
+
loadCatalog?: (items: ObjRefInScope[]) => Promise<IDimensionalityItem[]>;
|
|
44
|
+
/** Whether eagerly-provided catalog items are loading (only used when {@link loadCatalog} is absent). */
|
|
45
|
+
isLoadingCatalog?: boolean;
|
|
46
|
+
/** Called whenever the selection changes (add / remove / reset). */
|
|
47
|
+
onChange: (items: IDimensionalityItem[]) => void;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Shared state and logic for the dimensionality-items editor used by both the measure value filter
|
|
51
|
+
* dimensionality section and the ranking filter "out of" attributes section: the attribute-picker open/anchor
|
|
52
|
+
* state, the reset visibility, the available insight/catalog items (deduplicated against the current
|
|
53
|
+
* selection and against each other), and the add/remove/reset/open/close handlers. The two filter sections
|
|
54
|
+
* supply this with neutrally-named inputs and render their own (test-id / class / header) markup around it.
|
|
55
|
+
*/
|
|
56
|
+
export declare function useDimensionalityEditor({ items, insightItems, catalogItems, loadCatalog, isLoadingCatalog, onChange }: IUseDimensionalityEditorParams): {
|
|
57
|
+
inlineAddButtonRef: RefObject<HTMLButtonElement | null>;
|
|
58
|
+
isAttributePickerOpen: boolean;
|
|
59
|
+
actualAnchor: HTMLElement | null;
|
|
60
|
+
shouldShowResetButton: boolean;
|
|
61
|
+
availableInsightItems: IDimensionalityItem[];
|
|
62
|
+
availableCatalogItems: IDimensionalityItem[];
|
|
63
|
+
isLoadingCatalogForPicker: boolean | undefined;
|
|
64
|
+
handleRemove: (index: number) => void;
|
|
65
|
+
handleReset: () => void;
|
|
66
|
+
handleOpenInlinePicker: () => void;
|
|
67
|
+
handleOpenStandalonePicker: (event: MouseEvent<HTMLButtonElement, globalThis.MouseEvent>) => void;
|
|
68
|
+
handleClosePicker: () => void;
|
|
69
|
+
handleAddItems: (added: IDimensionalityItem[]) => void;
|
|
70
|
+
};
|
|
71
|
+
export {};
|
|
72
|
+
//# sourceMappingURL=useDimensionalityEditor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useDimensionalityEditor.d.ts","sourceRoot":"","sources":["../../src/MeasureValueFilter/useDimensionalityEditor.tsx"],"names":[],"mappings":"AAEA,OAAO,EACH,KAAK,UAAU,EACf,KAAK,SAAS,EACd,KAAK,SAAS,EAKjB,MAAM,OAAO,CAAC;AAEf,OAAO,EAAE,KAAK,aAAa,EAAmC,MAAM,qBAAqB,CAAC;AAG1F,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAGxD;;;GAGG;AACH,eAAO,MAAM,0BAA0B,yFAetC,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,4BAA4B,qHAG+C,CAAC;AAkBzF,UAAU,mBAAmB;IACzB,QAAQ,EAAE,SAAS,CAAC;IACpB,eAAe,EAAE,OAAO,CAAC;IACzB,UAAU,EAAE,OAAO,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,SAAS,CAAC,iBAAiB,GAAG,IAAI,CAAC,CAAC;IAC/C,OAAO,EAAE,CAAC,KAAK,EAAE,UAAU,CAAC,iBAAiB,CAAC,KAAK,IAAI,CAAC;IACxD,0GAA0G;IAC1G,UAAU,EAAE,MAAM,CAAC;CACtB;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,EAC1B,QAAQ,EACR,eAAe,EACf,UAAU,EACV,OAAO,EACP,SAAS,EACT,OAAO,EACP,UAAU,EACb,EAAE,mBAAmB,GAAG,SAAS,CAsBjC;AAED;;;GAGG;AACH,MAAM,WAAW,8BAA8B;IAC3C,mDAAmD;IACnD,KAAK,EAAE,mBAAmB,EAAE,CAAC;IAC7B,yGAAyG;IACzG,YAAY,CAAC,EAAE,mBAAmB,EAAE,CAAC;IACrC,yGAAyG;IACzG,YAAY,CAAC,EAAE,mBAAmB,EAAE,CAAC;IACrC,mFAAmF;IACnF,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,aAAa,EAAE,KAAK,OAAO,CAAC,mBAAmB,EAAE,CAAC,CAAC;IACzE,yGAAyG;IACzG,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,oEAAoE;IACpE,QAAQ,EAAE,CAAC,KAAK,EAAE,mBAAmB,EAAE,KAAK,IAAI,CAAC;CACpD;AAED;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,EACpC,KAAK,EACL,YAAY,EACZ,YAAY,EACZ,WAAW,EACX,gBAAgB,EAChB,QAAQ,EACX,EAAE,8BAA8B;;;;;;;;;;;;;;EAmHhC"}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// (C) 2025-2026 GoodData Corporation
|
|
3
|
+
import { useCallback, useMemo, useRef, useState, } from "react";
|
|
4
|
+
import { areObjRefsEqual, objRefToString } from "@gooddata/sdk-model";
|
|
5
|
+
import { Bubble, BubbleHoverTrigger, UiIconButton } from "@gooddata/sdk-ui-kit";
|
|
6
|
+
import { useLazyCatalogDimensionality } from "./useLazyCatalogDimensionality.js";
|
|
7
|
+
/**
|
|
8
|
+
* Compare two dimensionality arrays for equality (order-insensitive).
|
|
9
|
+
* Returns true if they contain the same identifiers regardless of order.
|
|
10
|
+
*/
|
|
11
|
+
export const areDimensionalitySetsEqual = (a, b) => {
|
|
12
|
+
const aItems = a ?? [];
|
|
13
|
+
const bItems = b ?? [];
|
|
14
|
+
if (aItems.length !== bItems.length) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
// Check that every item in 'a' exists in 'b'
|
|
18
|
+
return aItems.every((aItem) => bItems.some((bItem) => areObjRefsEqual(aItem.identifier, bItem.identifier)));
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* An empty dimensionality selection is only valid when the insight itself has no attributes. When the
|
|
22
|
+
* insight has attributes they are always the fallback, so an explicitly empty selection must be blocked
|
|
23
|
+
* (the Apply button is disabled). Shared by the measure value filter and the ranking filter "out of" section.
|
|
24
|
+
*/
|
|
25
|
+
export const isEmptyDimensionalityInvalid = (selection, insightDimensionality) => (selection?.length ?? 0) === 0 && (insightDimensionality?.length ?? 0) > 0;
|
|
26
|
+
const areDimensionalityItemsDeepEqual = (availableItem, filterItem) => {
|
|
27
|
+
if (availableItem.ref && filterItem.ref) {
|
|
28
|
+
return areObjRefsEqual(availableItem.ref, filterItem.ref);
|
|
29
|
+
}
|
|
30
|
+
if (availableItem.ref) {
|
|
31
|
+
return areObjRefsEqual(availableItem.ref, filterItem.identifier);
|
|
32
|
+
}
|
|
33
|
+
if (filterItem.ref) {
|
|
34
|
+
return areObjRefsEqual(availableItem.identifier, filterItem.ref);
|
|
35
|
+
}
|
|
36
|
+
return areObjRefsEqual(availableItem.identifier, filterItem.identifier);
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Renders the children and, when {@link IWithAddButtonProps.appendAddButton} is set, an inline "+" button
|
|
40
|
+
* that opens the attribute picker. Shared by the measure value filter dimensionality and the ranking filter
|
|
41
|
+
* "out of" sections.
|
|
42
|
+
*/
|
|
43
|
+
export function WithAddButton({ children, appendAddButton, isDisabled, tooltip, buttonRef, onClick, dataTestId, }) {
|
|
44
|
+
if (!appendAddButton) {
|
|
45
|
+
return children;
|
|
46
|
+
}
|
|
47
|
+
return (_jsxs("div", { className: "gd-mvf-dimensionality-add-wrapper", children: [children, _jsxs(BubbleHoverTrigger, { children: [
|
|
48
|
+
_jsx(UiIconButton, { ref: buttonRef, icon: "plus", size: "small", variant: "tertiary", isDisabled: isDisabled, onClick: onClick, dataTestId: dataTestId }), _jsx(Bubble, { alignPoints: [{ align: "bc tc" }], children: tooltip })
|
|
49
|
+
] })
|
|
50
|
+
] }));
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Shared state and logic for the dimensionality-items editor used by both the measure value filter
|
|
54
|
+
* dimensionality section and the ranking filter "out of" attributes section: the attribute-picker open/anchor
|
|
55
|
+
* state, the reset visibility, the available insight/catalog items (deduplicated against the current
|
|
56
|
+
* selection and against each other), and the add/remove/reset/open/close handlers. The two filter sections
|
|
57
|
+
* supply this with neutrally-named inputs and render their own (test-id / class / header) markup around it.
|
|
58
|
+
*/
|
|
59
|
+
export function useDimensionalityEditor({ items, insightItems, catalogItems, loadCatalog, isLoadingCatalog, onChange, }) {
|
|
60
|
+
// Ref for the inline add button (the "+" next to the last tag).
|
|
61
|
+
const inlineAddButtonRef = useRef(null);
|
|
62
|
+
// Attribute picker state - whether it is open and which button (inline vs standalone) triggered it.
|
|
63
|
+
const [isAttributePickerOpen, setIsAttributePickerOpen] = useState(false);
|
|
64
|
+
const [anchorType, setAnchorType] = useState("standalone");
|
|
65
|
+
const [standaloneAnchor, setStandaloneAnchor] = useState(null);
|
|
66
|
+
const { lazyCatalogDimensionality, isLoadingLazyCatalogDimensionality } = useLazyCatalogDimensionality({
|
|
67
|
+
isOpen: isAttributePickerOpen,
|
|
68
|
+
dimensionality: items,
|
|
69
|
+
loadCatalogDimensionality: loadCatalog,
|
|
70
|
+
});
|
|
71
|
+
// Reset is offered whenever the current selection differs from the insight defaults (order-insensitive).
|
|
72
|
+
const shouldShowResetButton = useMemo(() => !areDimensionalitySetsEqual(items, insightItems), [items, insightItems]);
|
|
73
|
+
// Keep insight items and catalog items separate so the picker does not need to guess their origin.
|
|
74
|
+
const availableInsightItems = useMemo(() => (insightItems ?? []).filter((availableItem) => !items.some((filterItem) => areDimensionalityItemsDeepEqual(availableItem, filterItem))), [insightItems, items]);
|
|
75
|
+
const availableCatalogItems = useMemo(() => {
|
|
76
|
+
const effectiveCatalog = loadCatalog ? (lazyCatalogDimensionality ?? []) : (catalogItems ?? []);
|
|
77
|
+
const selectedFilteredOut = effectiveCatalog.filter((availableItem) => !items.some((filterItem) => areDimensionalityItemsDeepEqual(availableItem, filterItem)));
|
|
78
|
+
// Prevent duplicates between the "From visualization" and catalog lists. Insight items may be
|
|
79
|
+
// LocalIdRefs; when available, use `ref` (display-form ObjRef) for stable deduplication against
|
|
80
|
+
// catalog candidates (which use ObjRefs).
|
|
81
|
+
const insightRefKeys = new Set(availableInsightItems
|
|
82
|
+
.map((i) => (i.ref ? objRefToString(i.ref) : undefined))
|
|
83
|
+
.filter((x) => !!x));
|
|
84
|
+
const insightTitlesWithoutRef = new Set((insightItems ?? []).filter((i) => !i.ref).map((i) => i.title));
|
|
85
|
+
return selectedFilteredOut.filter((item) => {
|
|
86
|
+
const itemKey = objRefToString(item.identifier);
|
|
87
|
+
if (insightRefKeys.has(itemKey)) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
// Fallback for environments where insight items do not provide refs.
|
|
91
|
+
return !insightTitlesWithoutRef.has(item.title);
|
|
92
|
+
});
|
|
93
|
+
}, [catalogItems, items, insightItems, availableInsightItems, lazyCatalogDimensionality, loadCatalog]);
|
|
94
|
+
const handleRemove = useCallback((index) => {
|
|
95
|
+
onChange(items.filter((_, i) => i !== index));
|
|
96
|
+
}, [items, onChange]);
|
|
97
|
+
const handleReset = useCallback(() => {
|
|
98
|
+
onChange(insightItems ?? []);
|
|
99
|
+
}, [insightItems, onChange]);
|
|
100
|
+
const handleOpenInlinePicker = useCallback(() => {
|
|
101
|
+
setAnchorType("inline");
|
|
102
|
+
setIsAttributePickerOpen(true);
|
|
103
|
+
}, []);
|
|
104
|
+
const handleOpenStandalonePicker = useCallback((event) => {
|
|
105
|
+
setAnchorType("standalone");
|
|
106
|
+
setStandaloneAnchor(event.currentTarget);
|
|
107
|
+
setIsAttributePickerOpen(true);
|
|
108
|
+
}, []);
|
|
109
|
+
const handleClosePicker = useCallback(() => {
|
|
110
|
+
setIsAttributePickerOpen(false);
|
|
111
|
+
setStandaloneAnchor(null);
|
|
112
|
+
}, []);
|
|
113
|
+
const handleAddItems = useCallback((added) => {
|
|
114
|
+
onChange([...items, ...added]);
|
|
115
|
+
setIsAttributePickerOpen(false);
|
|
116
|
+
setStandaloneAnchor(null);
|
|
117
|
+
}, [items, onChange]);
|
|
118
|
+
const actualAnchor = anchorType === "inline" ? inlineAddButtonRef.current : standaloneAnchor;
|
|
119
|
+
const isLoadingCatalogForPicker = loadCatalog ? isLoadingLazyCatalogDimensionality : isLoadingCatalog;
|
|
120
|
+
return {
|
|
121
|
+
inlineAddButtonRef,
|
|
122
|
+
isAttributePickerOpen,
|
|
123
|
+
actualAnchor,
|
|
124
|
+
shouldShowResetButton,
|
|
125
|
+
availableInsightItems,
|
|
126
|
+
availableCatalogItems,
|
|
127
|
+
isLoadingCatalogForPicker,
|
|
128
|
+
handleRemove,
|
|
129
|
+
handleReset,
|
|
130
|
+
handleOpenInlinePicker,
|
|
131
|
+
handleOpenStandalonePicker,
|
|
132
|
+
handleClosePicker,
|
|
133
|
+
handleAddItems,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { type ObjRefInScope } from "@gooddata/sdk-model";
|
|
2
|
+
import type { IDimensionalityItem } from "../MeasureValueFilter/typings.js";
|
|
3
|
+
/**
|
|
4
|
+
* @internal
|
|
5
|
+
*/
|
|
6
|
+
export interface IRankingAttributesSectionProps {
|
|
7
|
+
/**
|
|
8
|
+
* Current "out of" attribute items in the filter.
|
|
9
|
+
*/
|
|
10
|
+
attributes: IDimensionalityItem[];
|
|
11
|
+
/**
|
|
12
|
+
* Insight default attribute items (from buckets).
|
|
13
|
+
*/
|
|
14
|
+
insightAttributes?: IDimensionalityItem[];
|
|
15
|
+
/**
|
|
16
|
+
* Catalog attribute items (from computeValidObjects).
|
|
17
|
+
*/
|
|
18
|
+
catalogAttributes?: IDimensionalityItem[];
|
|
19
|
+
/**
|
|
20
|
+
* Optional callback to load catalog attribute items lazily (e.g. on picker open).
|
|
21
|
+
*/
|
|
22
|
+
loadCatalogAttributes?: (attributes: ObjRefInScope[]) => Promise<IDimensionalityItem[]>;
|
|
23
|
+
/**
|
|
24
|
+
* Whether catalog attributes are currently being loaded.
|
|
25
|
+
*/
|
|
26
|
+
isLoadingCatalogAttributes?: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Callback when the "out of" attributes change.
|
|
29
|
+
*/
|
|
30
|
+
onAttributesChange: (attributes: IDimensionalityItem[]) => void;
|
|
31
|
+
/**
|
|
32
|
+
* Whether the filter already has its own "out of" attributes (or the insight has none). When false, the
|
|
33
|
+
* ranking currently follows the visualization's attributes and an informational note is shown - mirroring
|
|
34
|
+
* the measure value filter dimensionality. Defaults to true (no note).
|
|
35
|
+
*/
|
|
36
|
+
isMigratedFilter?: boolean;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* @internal
|
|
40
|
+
* "Out of" attributes section for the ranking filter dropdown. Allows users to rank "out of" the insight's
|
|
41
|
+
* own attributes (default) or custom attributes picked from the catalog, with a reset to defaults.
|
|
42
|
+
*/
|
|
43
|
+
export declare const RankingAttributesSection: import("react").NamedExoticComponent<IRankingAttributesSectionProps>;
|
|
44
|
+
//# sourceMappingURL=RankingAttributesSection.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RankingAttributesSection.d.ts","sourceRoot":"","sources":["../../src/RankingFilter/RankingAttributesSection.tsx"],"names":[],"mappings":"AAMA,OAAO,EAAE,KAAK,aAAa,EAAkB,MAAM,qBAAqB,CAAC;AAIzE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AAG5E;;GAEG;AACH,MAAM,WAAW,8BAA8B;IAC3C;;OAEG;IACH,UAAU,EAAE,mBAAmB,EAAE,CAAC;IAClC;;OAEG;IACH,iBAAiB,CAAC,EAAE,mBAAmB,EAAE,CAAC;IAC1C;;OAEG;IACH,iBAAiB,CAAC,EAAE,mBAAmB,EAAE,CAAC;IAC1C;;OAEG;IACH,qBAAqB,CAAC,EAAE,CAAC,UAAU,EAAE,aAAa,EAAE,KAAK,OAAO,CAAC,mBAAmB,EAAE,CAAC,CAAC;IACxF;;OAEG;IACH,0BAA0B,CAAC,EAAE,OAAO,CAAC;IACrC;;OAEG;IACH,kBAAkB,EAAE,CAAC,UAAU,EAAE,mBAAmB,EAAE,KAAK,IAAI,CAAC;IAChE;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC9B;AAED;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,sEA0InC,CAAC"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// (C) 2025-2026 GoodData Corporation
|
|
3
|
+
import { memo } from "react";
|
|
4
|
+
import { FormattedMessage, useIntl } from "react-intl";
|
|
5
|
+
import { objRefToString } from "@gooddata/sdk-model";
|
|
6
|
+
import { Bubble, BubbleHoverTrigger, UiButton, UiIconButton, UiTag } from "@gooddata/sdk-ui-kit";
|
|
7
|
+
import { AttributePicker } from "../MeasureValueFilter/AttributePicker.js";
|
|
8
|
+
import { WithAddButton, useDimensionalityEditor } from "../MeasureValueFilter/useDimensionalityEditor.js";
|
|
9
|
+
/**
|
|
10
|
+
* @internal
|
|
11
|
+
* "Out of" attributes section for the ranking filter dropdown. Allows users to rank "out of" the insight's
|
|
12
|
+
* own attributes (default) or custom attributes picked from the catalog, with a reset to defaults.
|
|
13
|
+
*/
|
|
14
|
+
export const RankingAttributesSection = memo(function RankingAttributesSection({ attributes, insightAttributes, catalogAttributes, loadCatalogAttributes, isLoadingCatalogAttributes, onAttributesChange, isMigratedFilter = true, }) {
|
|
15
|
+
const intl = useIntl();
|
|
16
|
+
const { inlineAddButtonRef, isAttributePickerOpen, actualAnchor, shouldShowResetButton, availableInsightItems, availableCatalogItems, isLoadingCatalogForPicker, handleRemove: handleRemoveAttribute, handleReset: handleResetAttributes, handleOpenInlinePicker: handleOpenInlineAttributePicker, handleOpenStandalonePicker: handleOpenStandaloneAttributePicker, handleClosePicker: handleCloseAttributePicker, handleAddItems: handleAddAttributeItems, } = useDimensionalityEditor({
|
|
17
|
+
items: attributes,
|
|
18
|
+
insightItems: insightAttributes,
|
|
19
|
+
catalogItems: catalogAttributes,
|
|
20
|
+
loadCatalog: loadCatalogAttributes,
|
|
21
|
+
isLoadingCatalog: isLoadingCatalogAttributes,
|
|
22
|
+
onChange: onAttributesChange,
|
|
23
|
+
});
|
|
24
|
+
const addButtonTooltip = intl.formatMessage({ id: "mvf.dimensionality.addButton.tooltip" });
|
|
25
|
+
const resetButtonTooltip = intl.formatMessage({ id: "mvf.dimensionality.reset.tooltip" });
|
|
26
|
+
return (_jsxs("div", { className: "gd-mvf-dimensionality gd-rf-attributes s-rf-attributes", "data-testid": "rf-attributes", children: [
|
|
27
|
+
_jsx("div", { className: "gd-rf-dropdown-section-title", children: _jsx(FormattedMessage, { id: "rankingFilter.outOf" }) }), attributes.length > 0 ? (_jsxs("div", { className: "gd-mvf-dimensionality-items", children: [attributes.map((item, index) => {
|
|
28
|
+
const isDateItem = item.type === "chronologicalDate" || item.type === "genericDate";
|
|
29
|
+
const icon = isDateItem ? "date" : "ldmAttribute";
|
|
30
|
+
const iconColor = isDateItem ? "primary" : "warning";
|
|
31
|
+
return (_jsx(WithAddButton, { appendAddButton: index === attributes.length - 1, isDisabled: false, tooltip: addButtonTooltip, buttonRef: inlineAddButtonRef, onClick: handleOpenInlineAttributePicker, dataTestId: "rf-attributes-plus", children: _jsx("div", { className: "gd-mvf-dimensionality-tag-wrapper", children: _jsx(UiTag, { variant: "outlined", size: "large", iconBefore: icon, iconBeforeColor: iconColor, label: item.title, isDeletable: true, onDelete: () => handleRemoveAttribute(index), dataTestId: `rf-attributes-tag-${index}` }) }) }, objRefToString(item.identifier)));
|
|
32
|
+
}), shouldShowResetButton ? (_jsx("div", { className: "gd-mvf-dimensionality-reset-button", children: _jsxs(BubbleHoverTrigger, { children: [
|
|
33
|
+
_jsx(UiIconButton, { icon: "history", size: "small", variant: "tertiary", onClick: handleResetAttributes, label: resetButtonTooltip, dataTestId: "rf-attributes-reset" }), _jsx(Bubble, { alignPoints: [{ align: "cr cl" }], children: resetButtonTooltip })
|
|
34
|
+
] }) })) : null] })) : (_jsxs("div", { className: "gd-mvf-dimensionality-empty-actions", children: [
|
|
35
|
+
_jsx(UiButton, { label: intl.formatMessage({ id: "mvf.dimensionality.addAttributes" }), variant: "tertiary", size: "small", iconBefore: "plus", onClick: handleOpenStandaloneAttributePicker }), shouldShowResetButton ? (_jsxs(BubbleHoverTrigger, { children: [
|
|
36
|
+
_jsx(UiButton, { size: "small", variant: "tertiary", iconBefore: "history", label: intl.formatMessage({ id: "mvf.dimensionality.reset" }), onClick: handleResetAttributes, dataTestId: "rf-attributes-reset" }), _jsx(Bubble, { alignPoints: [{ align: "cr cl" }], children: resetButtonTooltip })
|
|
37
|
+
] })) : null] })), isMigratedFilter ? null : (_jsx("div", { role: "status", "aria-live": "polite", className: "gd-message information gd-rf-attributes-backward-compatibility-message", "data-testid": "rf-backward-compatibility-note", children: _jsx("div", { className: "gd-message-text", children: intl.formatMessage({ id: "rankingFilter.outOf.backwardCompatibilityNote" }) }) })), isAttributePickerOpen && actualAnchor ? (_jsx(AttributePicker, { anchorElement: actualAnchor, onAdd: handleAddAttributeItems, onCancel: handleCloseAttributePicker, availableInsightItems: availableInsightItems, availableCatalogItems: availableCatalogItems, isLoadingCatalogDimensionality: isLoadingCatalogForPicker })) : null] }));
|
|
38
|
+
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type IRankingFilter, type ObjRefInScope } from "@gooddata/sdk-model";
|
|
2
|
+
import { type IDimensionalityItem } from "../MeasureValueFilter/typings.js";
|
|
2
3
|
import { type IAttributeDropdownItem, type ICustomGranularitySelection, type IMeasureDropdownItem, type RenderMeasureDropdownBody } from "./types.js";
|
|
3
|
-
export declare const prepareRankingFilterState: (filter: IRankingFilter) => IRankingFilter;
|
|
4
|
+
export declare const prepareRankingFilterState: (filter: IRankingFilter, keepAllAttributes?: boolean) => IRankingFilter;
|
|
4
5
|
/**
|
|
5
6
|
* @beta
|
|
6
7
|
*/
|
|
@@ -22,6 +23,15 @@ export interface IRankingFilterDropdownProps {
|
|
|
22
23
|
* with a custom body (e.g. a grouped, searchable catalog picker).
|
|
23
24
|
*/
|
|
24
25
|
renderMeasureDropdownBody?: RenderMeasureDropdownBody;
|
|
26
|
+
isAttributesSectionEnabled?: boolean;
|
|
27
|
+
/** Current "out of" attributes (titled), seeded by the host (filter's own, or insight defaults). */
|
|
28
|
+
attributes?: IDimensionalityItem[];
|
|
29
|
+
/** Insight default "out of" attributes (for the reset action). */
|
|
30
|
+
insightAttributes?: IDimensionalityItem[];
|
|
31
|
+
/** Catalog "out of" attributes (used when not lazily loaded via loadCatalogAttributes). */
|
|
32
|
+
catalogAttributes?: IDimensionalityItem[];
|
|
33
|
+
/** Lazily loads catalog "out of" attributes valid for the current selection (on picker open). */
|
|
34
|
+
loadCatalogAttributes?: (attributes: ObjRefInScope[]) => Promise<IDimensionalityItem[]>;
|
|
25
35
|
}
|
|
26
36
|
/**
|
|
27
37
|
* @beta
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RankingFilterDropdown.d.ts","sourceRoot":"","sources":["../../src/RankingFilter/RankingFilterDropdown.tsx"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,cAAc,EAAE,KAAK,aAAa,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"RankingFilterDropdown.d.ts","sourceRoot":"","sources":["../../src/RankingFilter/RankingFilterDropdown.tsx"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,cAAc,EAAE,KAAK,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAI9E,OAAO,EAAE,KAAK,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AAG5E,OAAO,EACH,KAAK,sBAAsB,EAC3B,KAAK,2BAA2B,EAChC,KAAK,oBAAoB,EACzB,KAAK,yBAAyB,EACjC,MAAM,YAAY,CAAC;AAKpB,eAAO,MAAM,yBAAyB,yEAqBrC,CAAC;AAEF;;GAEG;AACH,MAAM,WAAW,2BAA2B;IACxC,YAAY,EAAE,oBAAoB,EAAE,CAAC;IACrC,cAAc,EAAE,sBAAsB,EAAE,CAAC;IACzC,MAAM,EAAE,cAAc,CAAC;IACvB,OAAO,EAAE,CAAC,MAAM,EAAE,cAAc,KAAK,IAAI,CAAC;IAC1C,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,uBAAuB,CAAC,EAAE,CAAC,GAAG,EAAE,aAAa,KAAK,IAAI,CAAC;IACvD,sBAAsB,CAAC,EAAE,MAAM,IAAI,CAAC;IACpC,QAAQ,CAAC,EAAE,WAAW,GAAG,MAAM,CAAC;IAChC,0BAA0B,CAAC,EAAE,2BAA2B,CAAC;IACzD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,wBAAwB,CAAC,EAAE,OAAO,CAAC;IACnC;;;OAGG;IACH,yBAAyB,CAAC,EAAE,yBAAyB,CAAC;IAItD,0BAA0B,CAAC,EAAE,OAAO,CAAC;IACrC,oGAAoG;IACpG,UAAU,CAAC,EAAE,mBAAmB,EAAE,CAAC;IACnC,kEAAkE;IAClE,iBAAiB,CAAC,EAAE,mBAAmB,EAAE,CAAC;IAC1C,2FAA2F;IAC3F,iBAAiB,CAAC,EAAE,mBAAmB,EAAE,CAAC;IAC1C,iGAAiG;IACjG,qBAAqB,CAAC,EAAE,CAAC,UAAU,EAAE,aAAa,EAAE,KAAK,OAAO,CAAC,mBAAmB,EAAE,CAAC,CAAC;CAC3F;AA8DD;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,2BAA2B,2CAMvE"}
|
|
@@ -6,10 +6,11 @@ import { Overlay } from "@gooddata/sdk-ui-kit";
|
|
|
6
6
|
import { RankingFilterDropdownBody } from "./RankingFilterDropdownBody.js";
|
|
7
7
|
const alignPoints = ["bl tl", "tl bl", "br tr", "tr br"];
|
|
8
8
|
const DROPDOWN_ALIGNMENTS = alignPoints.map((align) => ({ align, offset: { x: 1, y: 0 } }));
|
|
9
|
-
export const prepareRankingFilterState = (filter) => {
|
|
9
|
+
export const prepareRankingFilterState = (filter, keepAllAttributes = false) => {
|
|
10
10
|
const { measure, attributes, operator, value, applyOnResult, strictLimitOfRows } = filter.rankingFilter;
|
|
11
|
-
|
|
12
|
-
const
|
|
11
|
+
// The legacy single-attribute UI keeps only the first attribute; the multi-attribute mode keeps them all.
|
|
12
|
+
const keptAttributes = keepAllAttributes ? attributes : attributes?.slice(0, 1);
|
|
13
|
+
const attributesProp = keptAttributes?.length ? { attributes: keptAttributes } : {};
|
|
13
14
|
const applyOnResultProp = applyOnResult === undefined ? {} : { applyOnResult };
|
|
14
15
|
const strictLimitOfRowsProp = strictLimitOfRows === undefined ? {} : { strictLimitOfRows };
|
|
15
16
|
return {
|
|
@@ -23,13 +24,13 @@ export const prepareRankingFilterState = (filter) => {
|
|
|
23
24
|
},
|
|
24
25
|
};
|
|
25
26
|
};
|
|
26
|
-
function RankingFilterDropdownComponent({ measureItems, attributeItems, filter, onApply, onCancel = () => { }, anchorEl, onDropDownItemMouseOver, onDropDownItemMouseOut, customGranularitySelection, enableRankingWithMvf, enableRankingStrictLimit, renderMeasureDropdownBody, }) {
|
|
27
|
-
const [rankingFilter, setRankingFilter] = useState(prepareRankingFilterState(filter));
|
|
27
|
+
function RankingFilterDropdownComponent({ measureItems, attributeItems, filter, onApply, onCancel = () => { }, anchorEl, onDropDownItemMouseOver, onDropDownItemMouseOut, customGranularitySelection, enableRankingWithMvf, enableRankingStrictLimit, renderMeasureDropdownBody, isAttributesSectionEnabled, attributes, insightAttributes, catalogAttributes, loadCatalogAttributes, }) {
|
|
28
|
+
const [rankingFilter, setRankingFilter] = useState(prepareRankingFilterState(filter, !!isAttributesSectionEnabled));
|
|
28
29
|
const handleApply = (rankingFilter) => {
|
|
29
30
|
setRankingFilter(rankingFilter);
|
|
30
31
|
onApply(rankingFilter);
|
|
31
32
|
};
|
|
32
|
-
return (_jsx(Overlay, { closeOnOutsideClick: true, closeOnParentScroll: true, closeOnMouseDrag: true, alignTo: anchorEl, alignPoints: DROPDOWN_ALIGNMENTS, onClose: onCancel, ignoreClicksOnByClass: [".gd-rf-inner-overlay-dropdown"], children: _jsx(RankingFilterDropdownBody, { measureItems: measureItems, attributeItems: attributeItems, filter: rankingFilter, onApply: handleApply, onCancel: onCancel, onDropDownItemMouseOver: onDropDownItemMouseOver, onDropDownItemMouseOut: onDropDownItemMouseOut, customGranularitySelection: customGranularitySelection, enableRankingWithMvf: enableRankingWithMvf, enableRankingStrictLimit: enableRankingStrictLimit, renderMeasureDropdownBody: renderMeasureDropdownBody }) }));
|
|
33
|
+
return (_jsx(Overlay, { closeOnOutsideClick: true, closeOnParentScroll: true, closeOnMouseDrag: true, alignTo: anchorEl, alignPoints: DROPDOWN_ALIGNMENTS, onClose: onCancel, ignoreClicksOnByClass: [".gd-rf-inner-overlay-dropdown"], children: _jsx(RankingFilterDropdownBody, { measureItems: measureItems, attributeItems: attributeItems, filter: rankingFilter, onApply: handleApply, onCancel: onCancel, onDropDownItemMouseOver: onDropDownItemMouseOver, onDropDownItemMouseOut: onDropDownItemMouseOut, customGranularitySelection: customGranularitySelection, enableRankingWithMvf: enableRankingWithMvf, enableRankingStrictLimit: enableRankingStrictLimit, renderMeasureDropdownBody: renderMeasureDropdownBody, isAttributesSectionEnabled: isAttributesSectionEnabled, attributes: attributes, insightAttributes: insightAttributes, catalogAttributes: catalogAttributes, loadCatalogAttributes: loadCatalogAttributes }) }));
|
|
33
34
|
}
|
|
34
35
|
/**
|
|
35
36
|
* @beta
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type IRankingFilter, type ObjRefInScope } from "@gooddata/sdk-model";
|
|
2
|
+
import { type IDimensionalityItem } from "../MeasureValueFilter/typings.js";
|
|
2
3
|
import { type IAttributeDropdownItem, type ICustomGranularitySelection, type IMeasureDropdownItem, type RenderMeasureDropdownBody } from "./types.js";
|
|
3
4
|
interface IRankingFilterDropdownBodyComponentProps {
|
|
4
5
|
measureItems: IMeasureDropdownItem[];
|
|
@@ -12,7 +13,20 @@ interface IRankingFilterDropdownBodyComponentProps {
|
|
|
12
13
|
enableRankingWithMvf?: boolean;
|
|
13
14
|
enableRankingStrictLimit?: boolean;
|
|
14
15
|
renderMeasureDropdownBody?: RenderMeasureDropdownBody;
|
|
16
|
+
isAttributesSectionEnabled?: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Current "out of" attributes (titled), seeded by the host: the filter's own attributes, or the insight
|
|
19
|
+
* defaults when the filter has none ("reuses insight attributes"). Used as the initial selection and the
|
|
20
|
+
* Apply baseline; carrying titles lets the preview render them (including catalog attributes).
|
|
21
|
+
*/
|
|
22
|
+
attributes?: IDimensionalityItem[];
|
|
23
|
+
/** Insight default "out of" attributes (bucket attributes/dates), used for the reset action. */
|
|
24
|
+
insightAttributes?: IDimensionalityItem[];
|
|
25
|
+
/** Catalog "out of" attributes (used when not lazily loaded via loadCatalogAttributes). */
|
|
26
|
+
catalogAttributes?: IDimensionalityItem[];
|
|
27
|
+
/** Lazily loads catalog "out of" attributes valid for the current selection (on picker open). */
|
|
28
|
+
loadCatalogAttributes?: (attributes: ObjRefInScope[]) => Promise<IDimensionalityItem[]>;
|
|
15
29
|
}
|
|
16
|
-
export declare function RankingFilterDropdownBody({ measureItems, attributeItems, filter, onApply, onCancel, onDropDownItemMouseOver, onDropDownItemMouseOut, customGranularitySelection, enableRankingWithMvf, enableRankingStrictLimit, renderMeasureDropdownBody }: IRankingFilterDropdownBodyComponentProps): import("react/jsx-runtime").JSX.Element;
|
|
30
|
+
export declare function RankingFilterDropdownBody({ measureItems, attributeItems, filter, onApply, onCancel, onDropDownItemMouseOver, onDropDownItemMouseOut, customGranularitySelection, enableRankingWithMvf, enableRankingStrictLimit, renderMeasureDropdownBody, isAttributesSectionEnabled, attributes, insightAttributes, catalogAttributes, loadCatalogAttributes }: IRankingFilterDropdownBodyComponentProps): import("react/jsx-runtime").JSX.Element;
|
|
17
31
|
export {};
|
|
18
32
|
//# sourceMappingURL=RankingFilterDropdownBody.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RankingFilterDropdownBody.d.ts","sourceRoot":"","sources":["../../src/RankingFilter/RankingFilterDropdownBody.tsx"],"names":[],"mappings":"AAQA,OAAO,EACH,KAAK,cAAc,EACnB,KAAK,aAAa,EAGrB,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"RankingFilterDropdownBody.d.ts","sourceRoot":"","sources":["../../src/RankingFilter/RankingFilterDropdownBody.tsx"],"names":[],"mappings":"AAQA,OAAO,EACH,KAAK,cAAc,EACnB,KAAK,aAAa,EAGrB,MAAM,qBAAqB,CAAC;AAG7B,OAAO,EAAE,KAAK,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AAW5E,OAAO,EACH,KAAK,sBAAsB,EAC3B,KAAK,2BAA2B,EAChC,KAAK,oBAAoB,EACzB,KAAK,yBAAyB,EACjC,MAAM,YAAY,CAAC;AAwCpB,UAAU,wCAAwC;IAC9C,YAAY,EAAE,oBAAoB,EAAE,CAAC;IACrC,cAAc,EAAE,sBAAsB,EAAE,CAAC;IACzC,MAAM,EAAE,cAAc,CAAC;IACvB,OAAO,EAAE,CAAC,MAAM,EAAE,cAAc,KAAK,IAAI,CAAC;IAC1C,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,uBAAuB,CAAC,EAAE,CAAC,GAAG,EAAE,aAAa,KAAK,IAAI,CAAC;IACvD,sBAAsB,CAAC,EAAE,MAAM,IAAI,CAAC;IACpC,0BAA0B,CAAC,EAAE,2BAA2B,CAAC;IACzD,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,wBAAwB,CAAC,EAAE,OAAO,CAAC;IACnC,yBAAyB,CAAC,EAAE,yBAAyB,CAAC;IAItD,0BAA0B,CAAC,EAAE,OAAO,CAAC;IACrC;;;;OAIG;IACH,UAAU,CAAC,EAAE,mBAAmB,EAAE,CAAC;IACnC,gGAAgG;IAChG,iBAAiB,CAAC,EAAE,mBAAmB,EAAE,CAAC;IAC1C,2FAA2F;IAC3F,iBAAiB,CAAC,EAAE,mBAAmB,EAAE,CAAC;IAC1C,iGAAiG;IACjG,qBAAqB,CAAC,EAAE,CAAC,UAAU,EAAE,aAAa,EAAE,KAAK,OAAO,CAAC,mBAAmB,EAAE,CAAC,CAAC;CAC3F;AAED,wBAAgB,yBAAyB,CAAC,EACtC,YAAY,EACZ,cAAc,EACd,MAAM,EACN,OAAO,EACP,QAAmB,EACnB,uBAAuB,EACvB,sBAAsB,EACtB,0BAA0B,EAC1B,oBAA4B,EAC5B,wBAAgC,EAChC,yBAAyB,EACzB,0BAAkC,EAClC,UAAU,EACV,iBAAiB,EACjB,iBAAiB,EACjB,qBAAqB,EACxB,EAAE,wCAAwC,2CA+P1C"}
|
|
@@ -1,15 +1,17 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
// (C) 2020-2026 GoodData Corporation
|
|
3
|
-
import { useCallback, useState } from "react";
|
|
3
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
4
4
|
import cx from "classnames";
|
|
5
5
|
import { isEmpty, isEqual, xorWith } from "lodash-es";
|
|
6
6
|
import { FormattedMessage, useIntl } from "react-intl";
|
|
7
7
|
import { areObjRefsEqual, newRankingFilter, } from "@gooddata/sdk-model";
|
|
8
8
|
import { Bubble, BubbleHoverTrigger, Button } from "@gooddata/sdk-ui-kit";
|
|
9
|
+
import { areDimensionalitySetsEqual, isEmptyDimensionalityInvalid, } from "../MeasureValueFilter/useDimensionalityEditor.js";
|
|
9
10
|
import { AttributeDropdown } from "./AttributeDropdown/AttributeDropdown.js";
|
|
10
11
|
import { MeasureDropdown } from "./MeasureDropdown/MeasureDropdown.js";
|
|
11
12
|
import { OperatorDropdown } from "./OperatorDropdown/OperatorDropdown.js";
|
|
12
13
|
import { Preview } from "./Preview.js";
|
|
14
|
+
import { RankingAttributesSection } from "./RankingAttributesSection.js";
|
|
13
15
|
import { ValueDropdown } from "./ValueDropdown/ValueDropdown.js";
|
|
14
16
|
const isApplyButtonDisabled = (filter, filterState, enableRankingWithMvf, applyOnResult, enableRankingStrictLimit, strictLimitOfRows) => {
|
|
15
17
|
const rankingFilter = filter.rankingFilter;
|
|
@@ -30,13 +32,38 @@ const isApplyButtonDisabled = (filter, filterState, enableRankingWithMvf, applyO
|
|
|
30
32
|
applyOnResultNotChanged &&
|
|
31
33
|
strictLimitOfRowsNotChanged);
|
|
32
34
|
};
|
|
33
|
-
export function RankingFilterDropdownBody({ measureItems, attributeItems, filter, onApply, onCancel = () => { }, onDropDownItemMouseOver, onDropDownItemMouseOut, customGranularitySelection, enableRankingWithMvf = false, enableRankingStrictLimit = false, renderMeasureDropdownBody, }) {
|
|
35
|
+
export function RankingFilterDropdownBody({ measureItems, attributeItems, filter, onApply, onCancel = () => { }, onDropDownItemMouseOver, onDropDownItemMouseOut, customGranularitySelection, enableRankingWithMvf = false, enableRankingStrictLimit = false, renderMeasureDropdownBody, isAttributesSectionEnabled = false, attributes, insightAttributes, catalogAttributes, loadCatalogAttributes, }) {
|
|
34
36
|
const intl = useIntl();
|
|
35
37
|
const rankingFilter = filter.rankingFilter;
|
|
36
38
|
const [value, setValue] = useState(rankingFilter.value);
|
|
37
39
|
const [operator, setOperator] = useState(rankingFilter.operator);
|
|
38
40
|
const [measure, setMeasureIdentifier] = useState(rankingFilter.measure);
|
|
39
|
-
|
|
41
|
+
// Legacy mode edits a single attribute (by ref); the custom section edits many titled attributes.
|
|
42
|
+
// In multi-attribute mode the host seeds the selection (filter's own attributes or, when it has none,
|
|
43
|
+
// the insight defaults — "reuses the insight attributes"). The titles travel with the items so the
|
|
44
|
+
// preview can render them; nothing is persisted until the user changes the seeded selection.
|
|
45
|
+
const [attribute, setAttribute] = useState(rankingFilter.attributes?.[0]);
|
|
46
|
+
const [attributeItemsState, setAttributeItemsState] = useState(attributes ?? []);
|
|
47
|
+
// The host seeds the "out of" selection from the filter / insight defaults, which can resolve after this
|
|
48
|
+
// dropdown has mounted. Re-seed when that prop changes, but only while the user has not diverged from the
|
|
49
|
+
// previously seeded set, so a late-arriving seed is reflected without clobbering in-progress edits.
|
|
50
|
+
const seededAttributesRef = useRef(attributes);
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
if (seededAttributesRef.current === attributes) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const previousSeed = seededAttributesRef.current ?? [];
|
|
56
|
+
seededAttributesRef.current = attributes;
|
|
57
|
+
setAttributeItemsState((current) => areDimensionalitySetsEqual(current, previousSeed) ? (attributes ?? []) : current);
|
|
58
|
+
}, [attributes]);
|
|
59
|
+
// Mirrors the measure value filter: when the filter has no "out of" attributes of its own but the insight
|
|
60
|
+
// has some, the ranking currently follows the visualization's attributes and an informational note is
|
|
61
|
+
// shown. The note is dismissed once the user changes the selection (the filter then carries its own set).
|
|
62
|
+
const [isMigratedFilter, setIsMigratedFilter] = useState((rankingFilter.attributes?.length ?? 0) > 0 || (insightAttributes?.length ?? 0) === 0);
|
|
63
|
+
const handleAttributesChange = useCallback((items) => {
|
|
64
|
+
setAttributeItemsState(items);
|
|
65
|
+
setIsMigratedFilter(true);
|
|
66
|
+
}, []);
|
|
40
67
|
const [applyOnResult, setApplyOnResult] = useState(rankingFilter.applyOnResult ?? true);
|
|
41
68
|
// A new filter prioritizes the strict ("+ flag") condition; an existing filter keeps its stored value.
|
|
42
69
|
const [strictLimitOfRows, setStrictLimitOfRows] = useState(rankingFilter.strictLimitOfRows ?? false);
|
|
@@ -44,11 +71,33 @@ export function RankingFilterDropdownBody({ measureItems, attributeItems, filter
|
|
|
44
71
|
setOperator(selectedOperator);
|
|
45
72
|
setStrictLimitOfRows(selectedStrictLimitOfRows);
|
|
46
73
|
}, []);
|
|
74
|
+
// Apply baseline for the "out of" attributes: the seeded selection in multi mode, the stored filter
|
|
75
|
+
// otherwise. Keeping the seeded set as the baseline means reusing the insight defaults (unchanged)
|
|
76
|
+
// leaves Apply disabled and persists nothing.
|
|
77
|
+
const baselineAttributes = isAttributesSectionEnabled
|
|
78
|
+
? (attributes ?? []).map((item) => item.identifier)
|
|
79
|
+
: (rankingFilter.attributes ?? []);
|
|
80
|
+
const isAttributesSelectionInvalid = isAttributesSectionEnabled && isEmptyDimensionalityInvalid(attributeItemsState, insightAttributes);
|
|
47
81
|
const selectedMeasure = measureItems.find((item) => areObjRefsEqual(item.ref, measure));
|
|
48
82
|
const selectedAttribute = attributeItems.find((item) => areObjRefsEqual(item.ref, attribute));
|
|
83
|
+
// The preview reflects the live "out of" attributes. Multi-attribute titles are carried by the items
|
|
84
|
+
// (catalog-aware), joined into a single preview label.
|
|
85
|
+
const previewAttribute = isAttributesSectionEnabled
|
|
86
|
+
? attributeItemsState.length > 0
|
|
87
|
+
? {
|
|
88
|
+
title: attributeItemsState.map((item) => item.title).join(", "),
|
|
89
|
+
ref: attributeItemsState[0].identifier,
|
|
90
|
+
}
|
|
91
|
+
: undefined
|
|
92
|
+
: selectedAttribute;
|
|
49
93
|
const getFilterState = useCallback(() => {
|
|
50
|
-
const
|
|
51
|
-
?
|
|
94
|
+
const attributeRefs = isAttributesSectionEnabled
|
|
95
|
+
? attributeItemsState.map((item) => item.identifier)
|
|
96
|
+
: attribute
|
|
97
|
+
? [attribute]
|
|
98
|
+
: [];
|
|
99
|
+
const baseFilter = attributeRefs.length
|
|
100
|
+
? newRankingFilter(measure, attributeRefs, operator, value)
|
|
52
101
|
: newRankingFilter(measure, operator, value);
|
|
53
102
|
// Add applyOnResult / strictLimitOfRows only when their flags are enabled
|
|
54
103
|
return {
|
|
@@ -60,6 +109,8 @@ export function RankingFilterDropdownBody({ measureItems, attributeItems, filter
|
|
|
60
109
|
};
|
|
61
110
|
}, [
|
|
62
111
|
measure,
|
|
112
|
+
isAttributesSectionEnabled,
|
|
113
|
+
attributeItemsState,
|
|
63
114
|
attribute,
|
|
64
115
|
operator,
|
|
65
116
|
value,
|
|
@@ -75,19 +126,31 @@ export function RankingFilterDropdownBody({ measureItems, attributeItems, filter
|
|
|
75
126
|
const handleApplyOnResultChange = useCallback((event) => {
|
|
76
127
|
setApplyOnResult(event.target.checked);
|
|
77
128
|
}, [setApplyOnResult]);
|
|
78
|
-
return (_jsxs("div", { className: "gd-dialog gd-dropdown overlay gd-rf-dropdown-body s-rf-dropdown-body", children: [
|
|
79
|
-
_jsx("div", { className: "gd-rf-dropdown-header", children: _jsx(FormattedMessage, { id: "rankingFilter.topBottom" }) }), _jsxs("div", { className: "gd-rf-dropdown-section", children: [enableRankingStrictLimit ? (_jsx("div", { className: "gd-rf-dropdown-section-title gd-rf-dropdown-section-title-first", children: _jsx(FormattedMessage, { id: "rankingFilter.condition" }) })) : null, _jsxs("div", { className: cx("gd-rf-value-section", {
|
|
129
|
+
return (_jsxs("div", { className: "gd-dialog gd-dropdown overlay gd-rf-dropdown-body s-rf-dropdown-body", children: [isAttributesSectionEnabled ? null : (_jsx("div", { className: "gd-rf-dropdown-header", children: _jsx(FormattedMessage, { id: "rankingFilter.topBottom" }) })), _jsxs("div", { className: "gd-rf-dropdown-section", children: [enableRankingStrictLimit ? (_jsx("div", { className: "gd-rf-dropdown-section-title gd-rf-dropdown-section-title-first", children: _jsx(FormattedMessage, { id: "rankingFilter.condition" }) })) : null, _jsxs("div", { className: cx("gd-rf-value-section", {
|
|
80
130
|
"gd-rf-value-section--strict-limit": enableRankingStrictLimit,
|
|
81
131
|
}), children: [
|
|
82
132
|
_jsx(OperatorDropdown, { selectedValue: operator, selectedStrictLimitOfRows: strictLimitOfRows, enableRankingStrictLimit: enableRankingStrictLimit, limitValue: value, onSelect: handleConditionSelect }), _jsx(ValueDropdown, { selectedValue: value, onSelect: setValue }), enableRankingStrictLimit ? null : (_jsxs(BubbleHoverTrigger, { showDelay: 0, hideDelay: 0, children: [
|
|
83
133
|
_jsx("span", { className: "gd-icon-circle-question gd-rf-value-tooltip-icon s-rf-value-tooltip-icon" }), _jsx(Bubble, { className: `bubble-primary gd-rf-tooltip-bubble s-rf-value-tooltip`, alignPoints: [{ align: "cr cl" }, { align: "cl cr" }], children: _jsx(FormattedMessage, { id: "rankingFilter.valueTooltip" }) })
|
|
84
|
-
] }))] }),
|
|
134
|
+
] }))] }), isAttributesSectionEnabled ? (
|
|
135
|
+
// The section renders its own "Out of" header, chips, catalog picker and reset.
|
|
136
|
+
_jsx(RankingAttributesSection, { attributes: attributeItemsState, insightAttributes: insightAttributes, catalogAttributes: catalogAttributes, loadCatalogAttributes: loadCatalogAttributes, onAttributesChange: handleAttributesChange, isMigratedFilter: isMigratedFilter })) : (_jsxs(_Fragment, { children: [
|
|
137
|
+
_jsx("div", { className: "gd-rf-dropdown-section-title", children: _jsx(FormattedMessage, { id: "rankingFilter.outOf" }) }), _jsx(AttributeDropdown, { items: attributeItems, selectedItemRef: attribute, onSelect: setAttribute, onDropDownItemMouseOver: onDropDownItemMouseOver, onDropDownItemMouseOut: onDropDownItemMouseOut, customGranularitySelection: customGranularitySelection })
|
|
138
|
+
] })), _jsx("div", { className: "gd-rf-dropdown-section-title", children: _jsx(FormattedMessage, { id: "rankingFilter.basedOn" }) }), _jsx(MeasureDropdown, { items: measureItems, selectedItemRef: measure, onSelect: setMeasureIdentifier, onDropDownItemMouseOver: onDropDownItemMouseOver, onDropDownItemMouseOut: onDropDownItemMouseOut, renderMeasureDropdownBody: renderMeasureDropdownBody }), enableRankingWithMvf ? (_jsx("div", { className: "gd-rf-apply-on-result", children: _jsxs("label", { className: "input-checkbox-label gd-rf-apply-on-result-checkbox", "data-testid": "rf-apply-on-result", children: [
|
|
85
139
|
_jsx("input", { type: "checkbox", name: "apply-on-result", className: "input-checkbox", checked: applyOnResult, onChange: handleApplyOnResultChange }), _jsx("span", { className: "input-label-text", children: intl.formatMessage({
|
|
86
140
|
id: "rankingFilter.applyOnResultLabel",
|
|
87
141
|
}) })
|
|
88
|
-
] }) })) : null, _jsx("div", { className: "gd-rf-dropdown-section-title", children: _jsx(FormattedMessage, { id: "rankingFilter.preview" }) }), _jsx(Preview, { measure: selectedMeasure, attribute:
|
|
142
|
+
] }) })) : null, _jsx("div", { className: "gd-rf-dropdown-section-title", children: _jsx(FormattedMessage, { id: "rankingFilter.preview" }) }), _jsx(Preview, { measure: selectedMeasure, attribute: previewAttribute, operator: operator, value: value, enableRankingStrictLimit: enableRankingStrictLimit, strictLimitOfRows: strictLimitOfRows })
|
|
89
143
|
] }), _jsxs("div", { className: "gd-rf-dropdown-footer", children: [
|
|
90
|
-
_jsx(Button, { className: "gd-button-secondary gd-button-small s-rf-dropdown-cancel", onClick: () => onCancel(), value: intl.formatMessage({ id: "cancel" }) }), _jsx(Button, { className: "gd-button-action gd-button-small s-rf-dropdown-apply", onClick: applyHandler, value: intl.formatMessage({ id: "apply" }), disabled:
|
|
144
|
+
_jsx(Button, { className: "gd-button-secondary gd-button-small s-rf-dropdown-cancel", onClick: () => onCancel(), value: intl.formatMessage({ id: "cancel" }) }), _jsx(Button, { className: "gd-button-action gd-button-small s-rf-dropdown-apply", onClick: applyHandler, value: intl.formatMessage({ id: "apply" }), disabled: isAttributesSelectionInvalid ||
|
|
145
|
+
isApplyButtonDisabled(
|
|
146
|
+
// Compare against the seeded baseline so reusing the insight defaults (unchanged)
|
|
147
|
+
// keeps Apply disabled and does not persist the defaults.
|
|
148
|
+
{
|
|
149
|
+
rankingFilter: {
|
|
150
|
+
...rankingFilter,
|
|
151
|
+
attributes: baselineAttributes.length ? baselineAttributes : undefined,
|
|
152
|
+
},
|
|
153
|
+
}, getFilterState(), enableRankingWithMvf, applyOnResult, enableRankingStrictLimit, strictLimitOfRows) })
|
|
91
154
|
] })
|
|
92
155
|
] }));
|
|
93
156
|
}
|
package/esm/sdk-ui-filters.d.ts
CHANGED
|
@@ -4091,6 +4091,15 @@ export declare interface IRankingFilterDropdownProps {
|
|
|
4091
4091
|
* with a custom body (e.g. a grouped, searchable catalog picker).
|
|
4092
4092
|
*/
|
|
4093
4093
|
renderMeasureDropdownBody?: RenderMeasureDropdownBody;
|
|
4094
|
+
isAttributesSectionEnabled?: boolean;
|
|
4095
|
+
/** Current "out of" attributes (titled), seeded by the host (filter's own, or insight defaults). */
|
|
4096
|
+
attributes?: IDimensionalityItem[];
|
|
4097
|
+
/** Insight default "out of" attributes (for the reset action). */
|
|
4098
|
+
insightAttributes?: IDimensionalityItem[];
|
|
4099
|
+
/** Catalog "out of" attributes (used when not lazily loaded via loadCatalogAttributes). */
|
|
4100
|
+
catalogAttributes?: IDimensionalityItem[];
|
|
4101
|
+
/** Lazily loads catalog "out of" attributes valid for the current selection (on picker open). */
|
|
4102
|
+
loadCatalogAttributes?: (attributes: ObjRefInScope[]) => Promise<IDimensionalityItem[]>;
|
|
4094
4103
|
}
|
|
4095
4104
|
|
|
4096
4105
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gooddata/sdk-ui-filters",
|
|
3
|
-
"version": "11.45.0-alpha.
|
|
3
|
+
"version": "11.45.0-alpha.2",
|
|
4
4
|
"description": "GoodData.UI SDK - Filter Components",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "GoodData Corporation",
|
|
@@ -46,11 +46,11 @@
|
|
|
46
46
|
"ts-invariant": "0.10.3",
|
|
47
47
|
"tslib": "2.8.1",
|
|
48
48
|
"uuid": "11.1.1",
|
|
49
|
-
"@gooddata/sdk-backend-spi": "11.45.0-alpha.
|
|
50
|
-
"@gooddata/sdk-model": "11.45.0-alpha.
|
|
51
|
-
"@gooddata/sdk-ui
|
|
52
|
-
"@gooddata/sdk-ui": "11.45.0-alpha.
|
|
53
|
-
"@gooddata/util": "11.45.0-alpha.
|
|
49
|
+
"@gooddata/sdk-backend-spi": "11.45.0-alpha.2",
|
|
50
|
+
"@gooddata/sdk-model": "11.45.0-alpha.2",
|
|
51
|
+
"@gooddata/sdk-ui": "11.45.0-alpha.2",
|
|
52
|
+
"@gooddata/sdk-ui-kit": "11.45.0-alpha.2",
|
|
53
|
+
"@gooddata/util": "11.45.0-alpha.2"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
56
|
"@microsoft/api-documenter": "^7.17.0",
|
|
@@ -92,12 +92,12 @@
|
|
|
92
92
|
"typescript": "5.9.3",
|
|
93
93
|
"vitest": "4.1.8",
|
|
94
94
|
"vitest-dom": "0.1.1",
|
|
95
|
-
"@gooddata/eslint-config": "11.45.0-alpha.
|
|
96
|
-
"@gooddata/oxlint-config": "11.45.0-alpha.
|
|
97
|
-
"@gooddata/reference-workspace": "11.45.0-alpha.
|
|
98
|
-
"@gooddata/sdk-backend-mockingbird": "11.45.0-alpha.
|
|
99
|
-
"@gooddata/
|
|
100
|
-
"@gooddata/
|
|
95
|
+
"@gooddata/eslint-config": "11.45.0-alpha.2",
|
|
96
|
+
"@gooddata/oxlint-config": "11.45.0-alpha.2",
|
|
97
|
+
"@gooddata/reference-workspace": "11.45.0-alpha.2",
|
|
98
|
+
"@gooddata/sdk-backend-mockingbird": "11.45.0-alpha.2",
|
|
99
|
+
"@gooddata/stylelint-config": "11.45.0-alpha.2",
|
|
100
|
+
"@gooddata/sdk-ui-theme-provider": "11.45.0-alpha.2"
|
|
101
101
|
},
|
|
102
102
|
"peerDependencies": {
|
|
103
103
|
"react": "^18.0.0 || ^19.0.0",
|