Skip to content

Commit

Permalink
feat: introduce ability to configure attribute filter elements limits
Browse files Browse the repository at this point in the history
JIRA: LX-23
  • Loading branch information
xMort committed Jan 23, 2024
1 parent 440bf43 commit 54bda33
Show file tree
Hide file tree
Showing 9 changed files with 221 additions and 38 deletions.
4 changes: 4 additions & 0 deletions libs/sdk-ui-dashboard/src/locales.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ export const messages: Record<string, MessageDescriptor> = 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" },
Expand Down
Original file line number Diff line number Diff line change
@@ -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<IAddLimitingItemProps> = ({
currentlySelectedItems,
onSelect,
onClose,
}) => {
const intl = useIntl();

const [matchingItems, setMatchingItems] = useState<IValidationItemWithTitle[]>([]);
const items = useSearchableLimitingItems(currentlySelectedItems);

useEffect(() => {
setMatchingItems(items);
}, [items]);

const onItemSearch = (keyword: string) =>
setMatchingItems(items.filter(({ title }) => title.toLowerCase().includes(keyword.toLowerCase())));

return (
<ConfigurationBubble
alignTo=".attribute-filter__limit__add-button"
alignPoints={ALIGN_POINTS}
onClose={onClose}
>
<div className="">
<div className="configuration-panel-header">
<Typography tagName="h3" className="configuration-panel-header-title">
<FormattedMessage id="attributesDropdown.valuesLimiting.popupTitle" />
</Typography>
<Button
className="gd-button-link gd-button-icon-only gd-icon-cross configuration-panel-header-close-button s-configuration-panel-header-close-button"
onClick={onClose}
/>
</div>
<DropdownList
width={250}
isMobile={false}
showSearch={true}
onSearch={onItemSearch}
searchPlaceholder={intl.formatMessage(
messages.filterAddValuesLimitPopupSearchPlaceholder,
)}
searchFieldSize="small"
renderNoData={({ hasNoMatchingData }) => (
<NoData
className="attribute-filter__limit__popup__no-data"
hasNoMatchingData={hasNoMatchingData}
notFoundLabel={intl.formatMessage(
messages.filterAddValuesLimitPopupSearchNoMatch,
)}
noDataLabel={intl.formatMessage(messages.filterAddValuesLimitPopupNoData)}
/>
)}
items={matchingItems}
renderItem={({ item: { item, title } }) => {
return (
<div
key={serializeObjRef(item)}
className="gd-list-item attribute-filter__limit__popup__item"
onClick={() => {
onSelect(item);
onClose();
}}
>
<LimitingItemTitle item={item} title={title} />
</div>
);
}}
/>
</div>
</ConfigurationBubble>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 }) => {
Expand Down Expand Up @@ -47,7 +48,7 @@ const extractKey = (item: ValuesLimitingItem) =>

interface ILimitValuesConfigurationProps {
parentFilters: IDashboardAttributeFilterParentItem[];
validateElementsBy?: ObjRef[];
validateElementsBy: ObjRef[];
metricsAndFacts: IMetricsAndFacts;
onUpdate: (items: ObjRef[]) => void;
}
Expand All @@ -59,18 +60,32 @@ const LimitValuesConfiguration: React.FC<ILimitValuesConfigurationProps> = ({
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 (
<div>
{isDropdownOpened ? (
<AddLimitingItem
currentlySelectedItems={validateElementsBy}
onSelect={onAdd}
onClose={() => setIsDropdownOpened(false)}
/>
) : null}
<div className="configuration-category attribute-filter__limit__title">
<Typography tagName="h3">
<FormattedMessage id="attributesDropdown.valuesLimiting.title" />
Expand All @@ -88,7 +103,10 @@ const LimitValuesConfiguration: React.FC<ILimitValuesConfigurationProps> = ({
<div>
{itemsWithTitles.length === 0 ? (
<WithExplanationTooltip>
<FormattedMessage id="attributesDropdown.valuesLimiting.empty" />
<NoData
className="attribute-filter__limit__no-data"
noDataLabel={intl.formatMessage(messages.filterAddValuesLimitNoData)}
/>
</WithExplanationTooltip>
) : (
<>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<ILimitingItemProps> = ({ item, title }) => {
export const LimitingItemTitle: React.FC<ILimitingItemTitleProps> = ({ item, title }) => {
if (isParentFilter(item)) {
return <ItemTitleWithIcon title={title} IconComponent={Icon.AttributeFilter} />;
}
Expand Down Expand Up @@ -77,6 +75,13 @@ const LimitingItemTitle: React.FC<ILimitingItemProps> = ({ item, title }) => {
return <ItemTitleWithIcon title={title} />;
};

export interface ILimitingItemProps {
title: string | React.ReactNode;
item: ValuesLimitingItem;
isDisabled?: boolean;
onDelete: () => void;
}

export const LimitingItem: React.FC<ILimitingItemProps> = (props) => {
const { isDisabled, onDelete } = props;
const classNames = cx("attribute-filter__limit__item", {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import {
selectAllCatalogDisplayFormsMap,
IDashboardAttributeFilterParentItem,
IMetricsAndFacts,
selectCatalogMeasures,
selectCatalogFacts,
} from "../../../../../../model/index.js";

export interface IValuesLimitingItemWithTitle {
Expand Down Expand Up @@ -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;
Expand All @@ -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]);
};
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Loading

0 comments on commit 54bda33

Please sign in to comment.