From 54bda33b68322ee4c4d9cb4a035942efa3517bc5 Mon Sep 17 00:00:00 2001 From: xMort Date: Mon, 22 Jan 2024 16:46:42 +0100 Subject: [PATCH] feat: introduce ability to configure attribute filter elements limits JIRA: LX-23 --- libs/sdk-ui-dashboard/src/locales.ts | 4 + .../limitValues/AddLimitingItem.tsx | 102 ++++++++++++++++++ .../limitValues/LimitValuesConfiguration.tsx | 34 ++++-- .../limitValues/LimitingItem.tsx | 13 ++- .../limitValues/limitingItemsHook.ts | 44 ++++++-- .../limitingItemsHook.test.ts.snap | 32 +++--- .../localization/bundles/en-US.json | 11 +- .../styles/scss/attributeFilterConfig.scss | 12 +++ libs/sdk-ui-kit/src/Dropdown/DropdownList.tsx | 7 +- 9 files changed, 221 insertions(+), 38 deletions(-) create mode 100644 libs/sdk-ui-dashboard/src/presentation/filterBar/attributeFilter/dashboardDropdownBody/configuration/limitValues/AddLimitingItem.tsx diff --git a/libs/sdk-ui-dashboard/src/locales.ts b/libs/sdk-ui-dashboard/src/locales.ts index 22f2bfec78c..bc0b7125709 100644 --- a/libs/sdk-ui-dashboard/src/locales.ts +++ b/libs/sdk-ui-dashboard/src/locales.ts @@ -73,6 +73,10 @@ export const messages: Record = defineMessages({ dateFilterDropdownConfigurationCancelText: { id: "gs.list.cancel" }, filterResetButtonTooltip: { id: "filter.resetButton.tooltip" }, filterAddValuesLimit: { id: "attributesDropdown.valuesLimiting.addLink" }, + filterAddValuesLimitPopupSearchPlaceholder: { id: "attributesDropdown.valuesLimiting.searchPlaceholder" }, + filterAddValuesLimitPopupSearchNoMatch: { id: "attributesDropdown.valuesLimiting.searchNoMatch" }, + filterAddValuesLimitPopupNoData: { id: "attributesDropdown.valuesLimiting.metricsEmpty" }, + filterAddValuesLimitNoData: { id: "attributesDropdown.valuesLimiting.empty" }, drillToDashboardConfig: { id: "configurationPanel.drillConfig.drillIntoDashboard" }, drillIntoInsight: { id: "configurationPanel.drillConfig.drillIntoInsight" }, diff --git a/libs/sdk-ui-dashboard/src/presentation/filterBar/attributeFilter/dashboardDropdownBody/configuration/limitValues/AddLimitingItem.tsx b/libs/sdk-ui-dashboard/src/presentation/filterBar/attributeFilter/dashboardDropdownBody/configuration/limitValues/AddLimitingItem.tsx new file mode 100644 index 00000000000..eb458a06089 --- /dev/null +++ b/libs/sdk-ui-dashboard/src/presentation/filterBar/attributeFilter/dashboardDropdownBody/configuration/limitValues/AddLimitingItem.tsx @@ -0,0 +1,102 @@ +// (C) 2024 GoodData Corporation + +import React, { useState, useEffect } from "react"; +import { useIntl, FormattedMessage } from "react-intl"; +import { ObjRef, serializeObjRef } from "@gooddata/sdk-model"; +import { DropdownList, Button, Typography, NoData, IAlignPoint } from "@gooddata/sdk-ui-kit"; + +import { messages } from "../../../../../../locales.js"; +import { ConfigurationBubble } from "../../../../../widget/common/configuration/ConfigurationBubble.js"; + +import { useSearchableLimitingItems, IValidationItemWithTitle } from "./limitingItemsHook.js"; +import { LimitingItemTitle } from "./LimitingItem.js"; + +const ALIGN_POINTS: IAlignPoint[] = [ + { + align: "tr tl", + offset: { x: 3, y: -63 }, + }, + { + align: "tl tr", + offset: { x: -3, y: -63 }, + }, +]; + +export interface IAddLimitingItemProps { + currentlySelectedItems: ObjRef[]; + onSelect: (item: ObjRef) => void; + onClose: () => void; +} + +export const AddLimitingItem: React.FC = ({ + currentlySelectedItems, + onSelect, + onClose, +}) => { + const intl = useIntl(); + + const [matchingItems, setMatchingItems] = useState([]); + const items = useSearchableLimitingItems(currentlySelectedItems); + + useEffect(() => { + setMatchingItems(items); + }, [items]); + + const onItemSearch = (keyword: string) => + setMatchingItems(items.filter(({ title }) => title.toLowerCase().includes(keyword.toLowerCase()))); + + return ( + +
+
+ + + +
+ ( + + )} + items={matchingItems} + renderItem={({ item: { item, title } }) => { + return ( +
{ + onSelect(item); + onClose(); + }} + > + +
+ ); + }} + /> +
+
+ ); +}; diff --git a/libs/sdk-ui-dashboard/src/presentation/filterBar/attributeFilter/dashboardDropdownBody/configuration/limitValues/LimitValuesConfiguration.tsx b/libs/sdk-ui-dashboard/src/presentation/filterBar/attributeFilter/dashboardDropdownBody/configuration/limitValues/LimitValuesConfiguration.tsx index 9a92f642def..e3134c8eb02 100644 --- a/libs/sdk-ui-dashboard/src/presentation/filterBar/attributeFilter/dashboardDropdownBody/configuration/limitValues/LimitValuesConfiguration.tsx +++ b/libs/sdk-ui-dashboard/src/presentation/filterBar/attributeFilter/dashboardDropdownBody/configuration/limitValues/LimitValuesConfiguration.tsx @@ -2,14 +2,11 @@ import React, { useState } from "react"; import { FormattedMessage, useIntl, IntlShape } from "react-intl"; -import { Typography, Bubble, BubbleHoverTrigger, Button } from "@gooddata/sdk-ui-kit"; +import { Typography, Bubble, BubbleHoverTrigger, Button, NoData } from "@gooddata/sdk-ui-kit"; import { isObjRef, serializeObjRef, ObjRef, areObjRefsEqual } from "@gooddata/sdk-model"; import { messages } from "../../../../../../locales.js"; import { ValuesLimitingItem } from "../../../types.js"; - -import { LimitingItem } from "./LimitingItem.js"; -import { useLimitingItems } from "./limitingItemsHook.js"; import { IDashboardAttributeFilterParentItem, useDashboardSelector, @@ -18,6 +15,10 @@ import { } from "../../../../../../model/index.js"; import { IntlWrapper } from "../../../../../localization/index.js"; +import { LimitingItem } from "./LimitingItem.js"; +import { useLimitingItems } from "./limitingItemsHook.js"; +import { AddLimitingItem } from "./AddLimitingItem.js"; + const TOOLTIP_ALIGN_POINTS = [{ align: "cr cl" }, { align: "cl cr" }]; const WithExplanationTooltip: React.FC<{ children: React.ReactNode }> = ({ children }) => { @@ -47,7 +48,7 @@ const extractKey = (item: ValuesLimitingItem) => interface ILimitValuesConfigurationProps { parentFilters: IDashboardAttributeFilterParentItem[]; - validateElementsBy?: ObjRef[]; + validateElementsBy: ObjRef[]; metricsAndFacts: IMetricsAndFacts; onUpdate: (items: ObjRef[]) => void; } @@ -59,18 +60,32 @@ const LimitValuesConfiguration: React.FC = ({ onUpdate, }) => { const intl = useIntl(); - const [_isDropdownOpened, setIsDropdownOpened] = useState(false); + const [isDropdownOpened, setIsDropdownOpened] = useState(false); const itemsWithTitles = useLimitingItems(parentFilters, validateElementsBy, metricsAndFacts); + const onAdd = (addedItem: ValuesLimitingItem) => { + // parent filters are ignored till UI will get to support them later + if (isObjRef(addedItem)) { + onUpdate([...validateElementsBy, addedItem]); + } + }; + const onDelete = (deletedItem: ValuesLimitingItem) => { // parent filters are ignored till UI will get to support them later if (isObjRef(deletedItem)) { - onUpdate(validateElementsBy!.filter((item) => !areObjRefsEqual(deletedItem, item))); + onUpdate(validateElementsBy.filter((item) => !areObjRefsEqual(deletedItem, item))); } }; return (
+ {isDropdownOpened ? ( + setIsDropdownOpened(false)} + /> + ) : null}
@@ -88,7 +103,10 @@ const LimitValuesConfiguration: React.FC = ({
{itemsWithTitles.length === 0 ? ( - + ) : ( <> diff --git a/libs/sdk-ui-dashboard/src/presentation/filterBar/attributeFilter/dashboardDropdownBody/configuration/limitValues/LimitingItem.tsx b/libs/sdk-ui-dashboard/src/presentation/filterBar/attributeFilter/dashboardDropdownBody/configuration/limitValues/LimitingItem.tsx index 3d4e4c01211..d8871d2011e 100644 --- a/libs/sdk-ui-dashboard/src/presentation/filterBar/attributeFilter/dashboardDropdownBody/configuration/limitValues/LimitingItem.tsx +++ b/libs/sdk-ui-dashboard/src/presentation/filterBar/attributeFilter/dashboardDropdownBody/configuration/limitValues/LimitingItem.tsx @@ -33,14 +33,12 @@ const isFact = (item: ValuesLimitingItem) => isIdentifierRef(item) && item.type const isAttribute = (item: ValuesLimitingItem) => isIdentifierRef(item) && item.type === "attribute"; const isParentFilter = (item: ValuesLimitingItem) => !isObjRef(item); -export interface ILimitingItemProps { +export interface ILimitingItemTitleProps { title: string | React.ReactNode; item: ValuesLimitingItem; - isDisabled?: boolean; - onDelete: () => void; } -const LimitingItemTitle: React.FC = ({ item, title }) => { +export const LimitingItemTitle: React.FC = ({ item, title }) => { if (isParentFilter(item)) { return ; } @@ -77,6 +75,13 @@ const LimitingItemTitle: React.FC = ({ item, title }) => { return ; }; +export interface ILimitingItemProps { + title: string | React.ReactNode; + item: ValuesLimitingItem; + isDisabled?: boolean; + onDelete: () => void; +} + export const LimitingItem: React.FC = (props) => { const { isDisabled, onDelete } = props; const classNames = cx("attribute-filter__limit__item", { diff --git a/libs/sdk-ui-dashboard/src/presentation/filterBar/attributeFilter/dashboardDropdownBody/configuration/limitValues/limitingItemsHook.ts b/libs/sdk-ui-dashboard/src/presentation/filterBar/attributeFilter/dashboardDropdownBody/configuration/limitValues/limitingItemsHook.ts index 25f77b7de72..7910b08c737 100644 --- a/libs/sdk-ui-dashboard/src/presentation/filterBar/attributeFilter/dashboardDropdownBody/configuration/limitValues/limitingItemsHook.ts +++ b/libs/sdk-ui-dashboard/src/presentation/filterBar/attributeFilter/dashboardDropdownBody/configuration/limitValues/limitingItemsHook.ts @@ -10,6 +10,8 @@ import { selectAllCatalogDisplayFormsMap, IDashboardAttributeFilterParentItem, IMetricsAndFacts, + selectCatalogMeasures, + selectCatalogFacts, } from "../../../../../../model/index.js"; export interface IValuesLimitingItemWithTitle { @@ -56,9 +58,9 @@ const getTypeOrder = (item: ValuesLimitingItem): number => { switch (item.type) { case "measure": return 1; - case "fact": - return 2; case "attribute": + return 2; + case "fact": return 3; default: return 0; @@ -75,23 +77,53 @@ export function sortByTypeAndTitle(a: IValuesLimitingItemWithTitle, b: IValuesLi export const useLimitingItems = ( parentFilters: IDashboardAttributeFilterParentItem[], - validateElementsBy: ObjRef[] | undefined, + validateElementsBy: ObjRef[], metricsAndFacts: IMetricsAndFacts, ): IValuesLimitingItemWithTitle[] => { const attributes = useDashboardSelector(selectCatalogAttributes); const labels = useDashboardSelector(selectAllCatalogDisplayFormsMap); return useMemo(() => { - const parentFilterItems = parentFilters.map((item) => ({ + // parent filters are not yet supported by the UI, once they will, we can uncomment this + const parentFilterItems: IValuesLimitingItemWithTitle[] = []; /*parentFilters.map((item) => ({ title: labels.get(item.displayForm)?.title, item, - })); + }));*/ const validationItems = - validateElementsBy?.map((item) => ({ + validateElementsBy.map((item) => ({ title: findTitleForCatalogItem(item, metricsAndFacts, attributes), item, })) ?? []; return [...parentFilterItems, ...validationItems].sort(sortByTypeAndTitle); }, [parentFilters, validateElementsBy, attributes, labels, metricsAndFacts]); }; + +export interface IValidationItemWithTitle { + title: string; + item: ObjRef; +} + +export const useSearchableLimitingItems = (currentlySelectedItems: ObjRef[]): IValidationItemWithTitle[] => { + const attributes = useDashboardSelector(selectCatalogAttributes); + const measures = useDashboardSelector(selectCatalogMeasures); + const facts = useDashboardSelector(selectCatalogFacts); + + return useMemo(() => { + const metricsWithTitles = measures.map((measure) => ({ + title: measure.measure.title, + item: measure.measure.ref, + })); + const factsWithTitles = facts.map((fact) => ({ + title: fact.fact.title, + item: fact.fact.ref, + })); + const attributesWithTitles = attributes.map((attribute) => ({ + title: attribute.attribute.title, + item: attribute.attribute.ref, + })); + return [...metricsWithTitles, ...factsWithTitles, ...attributesWithTitles] + .filter((item) => !currentlySelectedItems.includes(item.item)) + .sort(sortByTypeAndTitle); + }, [currentlySelectedItems, measures, facts, attributes]); +}; diff --git a/libs/sdk-ui-dashboard/src/presentation/filterBar/attributeFilter/dashboardDropdownBody/configuration/limitValues/tests/__snapshots__/limitingItemsHook.test.ts.snap b/libs/sdk-ui-dashboard/src/presentation/filterBar/attributeFilter/dashboardDropdownBody/configuration/limitValues/tests/__snapshots__/limitingItemsHook.test.ts.snap index 26981672ef9..2c38d2ccb9b 100644 --- a/libs/sdk-ui-dashboard/src/presentation/filterBar/attributeFilter/dashboardDropdownBody/configuration/limitValues/tests/__snapshots__/limitingItemsHook.test.ts.snap +++ b/libs/sdk-ui-dashboard/src/presentation/filterBar/attributeFilter/dashboardDropdownBody/configuration/limitValues/tests/__snapshots__/limitingItemsHook.test.ts.snap @@ -58,43 +58,43 @@ exports[`limitingItemsHook > sortByTypeAndTitle > should sort array of items as }, { "item": { - "identifier": "f0", - "type": "fact", + "identifier": "a3", + "type": "attribute", }, - "title": "Fact 1", + "title": "Attribute 1", }, { "item": { - "identifier": "f2", - "type": "fact", + "identifier": "a1", + "type": "attribute", }, - "title": "Fact 2", + "title": "Attribute 2", }, { "item": { - "identifier": "f1", - "type": "fact", + "identifier": "a2", + "type": "attribute", }, "title": undefined, }, { "item": { - "identifier": "a3", - "type": "attribute", + "identifier": "f0", + "type": "fact", }, - "title": "Attribute 1", + "title": "Fact 1", }, { "item": { - "identifier": "a1", - "type": "attribute", + "identifier": "f2", + "type": "fact", }, - "title": "Attribute 2", + "title": "Fact 2", }, { "item": { - "identifier": "a2", - "type": "attribute", + "identifier": "f1", + "type": "fact", }, "title": undefined, }, diff --git a/libs/sdk-ui-dashboard/src/presentation/localization/bundles/en-US.json b/libs/sdk-ui-dashboard/src/presentation/localization/bundles/en-US.json index 70076512e94..1bc17424f5e 100644 --- a/libs/sdk-ui-dashboard/src/presentation/localization/bundles/en-US.json +++ b/libs/sdk-ui-dashboard/src/presentation/localization/bundles/en-US.json @@ -2361,14 +2361,19 @@ "comment": "List item that represents filtering metric which is a sum aggregation of particular data attribute. Do not translate placeholder {attribute}. It will be replaced with the name of the selected attribute in the runtime.", "limit": 0 }, - "attributesDropdown.valuesLimiting.filtersEmpty": { - "value": "No more dashboard filters to add.", - "comment": "Text shown when all dashboard filters were used and there is none offered in the list.", + "attributesDropdown.valuesLimiting.metricsEmpty": { + "value": "No more items to add.", + "comment": "Text shown when all items were used and there is none offered in the list.", "limit": 0 }, "attributesDropdown.valuesLimiting.unknownItem": { "value": "Unknown item", "comment": "The title of item in the list that does not have a title.", "limit": 0 + }, + "attributesDropdown.valuesLimiting.searchNoMatch": { + "value": "No matching item was found.", + "comment": "Text shown when entered search term do not match any item in the list.", + "limit": 0 } } diff --git a/libs/sdk-ui-dashboard/styles/scss/attributeFilterConfig.scss b/libs/sdk-ui-dashboard/styles/scss/attributeFilterConfig.scss index 1a80ad24953..b5d032ffbf6 100644 --- a/libs/sdk-ui-dashboard/styles/scss/attributeFilterConfig.scss +++ b/libs/sdk-ui-dashboard/styles/scss/attributeFilterConfig.scss @@ -249,3 +249,15 @@ .attribute-filter__limit__item--disabled { color: kit-variables.$default-gd-color-disabled; } + +.attribute-filter__limit__no-data, +.attribute-filter__limit__popup__no-data { + font-size: 12px; + text-align: left; +} + +.attribute-filter__limit__popup__item { + span { + display: flex; + } +} diff --git a/libs/sdk-ui-kit/src/Dropdown/DropdownList.tsx b/libs/sdk-ui-kit/src/Dropdown/DropdownList.tsx index 6b52c151b48..4d82b1cc0cf 100644 --- a/libs/sdk-ui-kit/src/Dropdown/DropdownList.tsx +++ b/libs/sdk-ui-kit/src/Dropdown/DropdownList.tsx @@ -1,5 +1,5 @@ // (C) 2007-2024 GoodData Corporation -import React, { useCallback, useState } from "react"; +import React, { useCallback, useState, useEffect } from "react"; import cx from "classnames"; import { injectIntl, WrappedComponentProps } from "react-intl"; import { Input } from "../Form/index.js"; @@ -147,6 +147,11 @@ export function DropdownList(props: IDropdownListProps): JSX.Element { [onSearch], ); + useEffect(() => { + // update string if dropdown is not getting unmounted on close to not have previous search on re-open + setCurrentSearchString(searchString); + }, [searchString]); + return ( {title ?
{title}
: null}