@gooddata/sdk-ui-filters 11.45.0-alpha.1 → 11.45.0

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.
@@ -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":"AAeA,OAAO,EAAE,KAAK,aAAa,EAAmC,MAAM,qBAAqB,CAAC;AAI1F,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AA2CxD;;;GAGG;AACH,eAAO,MAAM,0BAA0B,yFAetC,CAAC;AAkBF,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,mEAsPhC,CAAC"}
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, useCallback, useMemo, useRef, useState, } from "react";
3
+ import { memo } from "react";
4
4
  import { useIntl } from "react-intl";
5
- import { areObjRefsEqual, objRefToString } from "@gooddata/sdk-model";
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 { useLazyCatalogDimensionality } from "./useLazyCatalogDimensionality.js";
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
- // Ref for the inline add button (plus icon next to last tag)
51
- const inlineAddButtonRef = useRef(null);
52
- // AttributePicker state - stores whether it's open and which button triggered it
53
- const [isAttributePickerOpen, setIsAttributePickerOpen] = useState(false);
54
- const [anchorType, setAnchorType] = useState("standalone");
55
- // Store standalone button anchor separately since it's not a ref
56
- const [standaloneAnchor, setStandaloneAnchor] = useState(null);
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: loadCatalogDimensionality
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;AAGtB,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,0DA44B/B,CAAC;AAEH,eAAO,MAAM,YAAY,0DAMvB,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, areDimensionalitySetsEqual } from "./DimensionalitySection.js";
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.length === 0 &&
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.length === 0 &&
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;AAK9E,OAAO,EACH,KAAK,sBAAsB,EAC3B,KAAK,2BAA2B,EAChC,KAAK,oBAAoB,EACzB,KAAK,yBAAyB,EACjC,MAAM,YAAY,CAAC;AAKpB,eAAO,MAAM,yBAAyB,4CAiBrC,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;CACzD;AAkDD;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,2BAA2B,2CAMvE"}
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
- const firstAttribute = attributes?.[0];
12
- const attributesProp = firstAttribute ? { attributes: [firstAttribute] } : {};
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;AAO7B,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;CACzD;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,EAC5B,EAAE,wCAAwC,2CA4K1C"}
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
- const [attribute, setAttributeIdentifier] = useState(rankingFilter.attributes?.[0]);
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 baseFilter = attribute
51
- ? newRankingFilter(measure, [attribute], operator, value)
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
- ] }))] }), _jsx("div", { className: "gd-rf-dropdown-section-title", children: _jsx(FormattedMessage, { id: "rankingFilter.outOf" }) }), _jsx(AttributeDropdown, { items: attributeItems, selectedItemRef: attribute, onSelect: setAttributeIdentifier, onDropDownItemMouseOver: onDropDownItemMouseOver, onDropDownItemMouseOut: onDropDownItemMouseOut, customGranularitySelection: customGranularitySelection }), _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: [
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: selectedAttribute, operator: operator, value: value, enableRankingStrictLimit: enableRankingStrictLimit, strictLimitOfRows: strictLimitOfRows })
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: isApplyButtonDisabled(filter, getFilterState(), enableRankingWithMvf, applyOnResult, enableRankingStrictLimit, strictLimitOfRows) })
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
  }
@@ -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.1",
3
+ "version": "11.45.0",
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.1",
50
- "@gooddata/sdk-model": "11.45.0-alpha.1",
51
- "@gooddata/sdk-ui-kit": "11.45.0-alpha.1",
52
- "@gooddata/sdk-ui": "11.45.0-alpha.1",
53
- "@gooddata/util": "11.45.0-alpha.1"
49
+ "@gooddata/sdk-backend-spi": "11.45.0",
50
+ "@gooddata/sdk-model": "11.45.0",
51
+ "@gooddata/sdk-ui": "11.45.0",
52
+ "@gooddata/util": "11.45.0",
53
+ "@gooddata/sdk-ui-kit": "11.45.0"
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.1",
96
- "@gooddata/oxlint-config": "11.45.0-alpha.1",
97
- "@gooddata/reference-workspace": "11.45.0-alpha.1",
98
- "@gooddata/sdk-backend-mockingbird": "11.45.0-alpha.1",
99
- "@gooddata/sdk-ui-theme-provider": "11.45.0-alpha.1",
100
- "@gooddata/stylelint-config": "11.45.0-alpha.1"
95
+ "@gooddata/eslint-config": "11.45.0",
96
+ "@gooddata/reference-workspace": "11.45.0",
97
+ "@gooddata/oxlint-config": "11.45.0",
98
+ "@gooddata/sdk-backend-mockingbird": "11.45.0",
99
+ "@gooddata/sdk-ui-theme-provider": "11.45.0",
100
+ "@gooddata/stylelint-config": "11.45.0"
101
101
  },
102
102
  "peerDependencies": {
103
103
  "react": "^18.0.0 || ^19.0.0",