@trackunit/filters-filter-bar 1.3.200 → 1.3.201

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.cjs.js CHANGED
@@ -645,6 +645,25 @@ const DefaultRadioFilter = ({ filterDefinition, filterBarActions, options, loadi
645
645
  }, value: selectedRadioId?.key || "", children: jsxRuntime.jsx(DynamicFilterList, { checked: index => filterBarActions.objectIncludesValue(filterDefinition.filterKey, res[index]?.key || ""), className: "m-1 mt-0", count: index => res[index]?.count, keyMapper: index => res[index]?.key || "", labelMapper: index => res[index]?.label || "", rowCount: res.length, showRequestMoreUseSearch: showRequestMoreUseSearch, type: "Radio" }) })) })] }));
646
646
  };
647
647
 
648
+ /**
649
+ * Tooltip label for the filter button
650
+ */
651
+ const FilterButtonTooltipLabel = ({ filterBarConfig, }) => {
652
+ const [t] = useTranslation();
653
+ switch (filterBarConfig.appliedFilterKeys().length) {
654
+ case 0:
655
+ return t("filtersBar.appliedFiltersTooltip.none");
656
+ case 1:
657
+ return filterBarConfig.appliedFilterKeys()[0]
658
+ ? t("filtersBar.appliedFiltersTooltip.singular", {
659
+ filterName: filterBarConfig.getFilterTitle(filterBarConfig.appliedFilterKeys()[0]),
660
+ })
661
+ : null; // should never happen though
662
+ default:
663
+ return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [t("filtersBar.appliedFiltersTooltip.plural", { count: filterBarConfig.appliedFilterKeys().length }), jsxRuntime.jsx("ul", { className: "list-inside", children: filterBarConfig.appliedFilterKeys().map((appliedFilterKey, index) => (jsxRuntime.jsx("li", { className: "list-disc", children: filterBarConfig.getFilterTitle(appliedFilterKey) }, index))) })] }));
664
+ }
665
+ };
666
+
648
667
  /**
649
668
  * Returns the two first values, appends counter if more.
650
669
  *
@@ -760,6 +779,64 @@ const useGroupFilters = (filterDefinitions, hiddenFilters) => {
760
779
  };
761
780
  const uniqueKeysFromGroups = (filters) => [...new Set(filters.map(filter => filter.group))];
762
781
 
782
+ /**
783
+ * This hook is used to manage the filters menu.
784
+ * It returns the filters that should be shown in the menu, the filters that should be shown directly in the filter bar,
785
+ * and the filters that should be shown in the search results.
786
+ */
787
+ const useFiltersMenu = ({ filterBarDefinition, filterBarConfig, hiddenFilters = [], allowShowFiltersDirectly = true, }) => {
788
+ const hideInMenu = react.useMemo(() => {
789
+ return sharedUtils.objectValues(filterBarDefinition)
790
+ .map(filter => {
791
+ const showInFilterBar = filter.showInFilterBar ? filter.showInFilterBar() : true;
792
+ const showInStarredMenu = filter.showInStarredMenu ? filter.showInStarredMenu() : true;
793
+ const showMenuAnywayBecauseFilterHasChanged = filterBarConfig.appliedFilterKeys().includes(filter.filterKey);
794
+ return (!showInFilterBar || !showInStarredMenu) && !showMenuAnywayBecauseFilterHasChanged
795
+ ? filter.filterKey
796
+ : null;
797
+ })
798
+ .filter(sharedUtils.truthy);
799
+ }, [filterBarConfig, filterBarDefinition]);
800
+ const removeCustomFieldsGroup = react.useCallback((groupOfFilters) => groupOfFilters.filter(group => group.key !== "CUSTOM_FIELDS"), []);
801
+ const { filtersGrouped } = useGroupFilters(sharedUtils.objectValues(filterBarDefinition), [...hideInMenu, ...hiddenFilters]);
802
+ const { appliedFilters, filtersToShow, showDirectlyFilters, hasCustomFields } = react.useMemo(() => {
803
+ const allFilters = filtersGrouped.map(group => group.filters).flat();
804
+ return {
805
+ appliedFilters: allFilters.filter(filter => filterBarConfig.appliedFilterKeys().includes(filter.filterKey)),
806
+ filtersToShow: allFilters.filter(filter => !filter.showDirectly),
807
+ showDirectlyFilters: allFilters.filter(filter => filter.showDirectly && allowShowFiltersDirectly),
808
+ hasCustomFields: allFilters.some(filter => filter.group === "CUSTOM_FIELDS"),
809
+ };
810
+ }, [filterBarConfig, filtersGrouped, allowShowFiltersDirectly]);
811
+ const [searchResults, searchText, setSearchText] = reactCoreHooks.useTextSearch(filtersToShow, item => [item.title]);
812
+ const { filtersGrouped: searchResultsGrouped } = useGroupFilters(searchResults, []);
813
+ const { filtersGrouped: filtersToShowGrouped } = useGroupFilters(filtersToShow, []);
814
+ const appliedCustomFields = react.useMemo(() => appliedFilters.filter(filter => filter.group === "CUSTOM_FIELDS"), [appliedFilters]);
815
+ return react.useMemo(() => {
816
+ return {
817
+ appliedFilters,
818
+ hasCustomFields,
819
+ showDirectlyFilters,
820
+ appliedCustomFields,
821
+ searchText,
822
+ setSearchText,
823
+ filtersToShowGrouped,
824
+ searchResultsGrouped,
825
+ removeCustomFieldsGroup,
826
+ };
827
+ }, [
828
+ appliedFilters,
829
+ hasCustomFields,
830
+ showDirectlyFilters,
831
+ appliedCustomFields,
832
+ searchText,
833
+ setSearchText,
834
+ filtersToShowGrouped,
835
+ searchResultsGrouped,
836
+ removeCustomFieldsGroup,
837
+ ]);
838
+ };
839
+
763
840
  /**
764
841
  * FiltersRenderer renders an array of Filter components from filter definitions
765
842
  * It ignores hidden filters.
@@ -780,6 +857,60 @@ const FiltersRenderer = ({ filters, filterBarConfig, visualStyle, }) => {
780
857
  .map(filter => (jsxRuntime.jsx(FilterComponent, { filter: filter, filterBarActions: filterBarConfig, filterState: { values: filterBarConfig.values, setters: filterBarConfig.setters }, visualStyle: visualStyle }, `filter-${filter.filterKey}`)));
781
858
  };
782
859
 
860
+ /**
861
+ * FiltersList is a React component that displays a list of filters within a filter bar.
862
+ *
863
+ * @returns {ReactElement} - Returns the FiltersList component.
864
+ */
865
+ const GroupedFiltersList = ({ filterBarConfig, filtersGrouped, className, dataTestId = "grouped-filters-list", }) => {
866
+ return (jsxRuntime.jsx("div", { className: className, "data-testid": dataTestId, role: "menu", children: filtersGrouped.map((group, idx) => {
867
+ const isLastGroup = idx === filtersGrouped.length - 1;
868
+ return (jsxRuntime.jsxs("div", { className: "flex flex-col gap-1", children: [jsxRuntime.jsxs("div", { children: [jsxRuntime.jsx(reactComponents.Text, { className: "text-secondary-400 h-7 px-3 py-2", dataTestId: `${group.key}-group-title`, size: "small", uppercase: true, weight: "bold", children: group.title }), jsxRuntime.jsx("ul", { "aria-labelledby": `${group.key}-group-title`, className: "grid", "data-testid": `${group.key}-group-list`, children: jsxRuntime.jsx(FiltersRenderer, { filterBarConfig: filterBarConfig, filters: group.filters, visualStyle: "list-item" }) })] }), isLastGroup ? null : jsxRuntime.jsx("div", { className: "bg-secondary-200 h-[1px] w-full", role: "separator" })] }, group.key));
869
+ }) }));
870
+ };
871
+
872
+ /**
873
+ * ResetFiltersButton is a React component that provides a button for resetting filters.
874
+ *
875
+ * @returns {ReactElement | null} The rendered ResetFiltersButton component, or null if no filters have been applied.
876
+ */
877
+ const ResetFiltersButton = ({ resetFiltersToInitialState, dataTestId, className, }) => {
878
+ const [t] = useTranslation();
879
+ return (jsxRuntime.jsxs(reactComponents.Button, { className: className, dataTestId: dataTestId ?? "reset-filters-button", onClick: () => {
880
+ resetFiltersToInitialState();
881
+ }, size: "small", variant: "ghost", children: [t("filtersBar.resetFilters"), jsxRuntime.jsx("span", { className: "sr-only", children: "Resets all applied filters" })] }));
882
+ };
883
+
884
+ /**
885
+ *
886
+ */
887
+ const FiltersMenuContent = ({ filterBarConfig, setShowCustomFilters, setSearchText, searchText, searchResultsGrouped, filtersToShowGrouped, removeCustomFieldsGroup, hasCustomFields, appliedCustomFields, showCustomFilters, }) => {
888
+ const [t] = useTranslation();
889
+ return (jsxRuntime.jsxs(reactComponents.Card, { className: "max-h-[min(600px,_calc(100dvh-32px))] w-[300px] overflow-y-hidden", dataTestId: "starred-filters-menu-popover", children: [jsxRuntime.jsxs("div", { className: " flex-col gap-1 p-1", children: [jsxRuntime.jsx(reactFormComponents.Search, { autoFocus: true, dataTestId: "starred-filters-menu-search", fieldSize: "small", id: "search-filters-list", onChange: e => setSearchText(e.currentTarget.value), onClear: () => setSearchText(""), placeholder: t("filtersBar.searchFiltersPlaceholder"), value: searchText }), jsxRuntime.jsxs("div", { className: "flex h-7 items-center justify-between gap-1 px-3", children: [jsxRuntime.jsx(reactComponents.Text, { className: "text-secondary-400", size: "small", children: jsxRuntime.jsx(FiltersAppliedCountLabel, { filterBarConfig: filterBarConfig }) }), filterBarConfig.appliedFilterKeys().length > 0 ? (jsxRuntime.jsx(ResetFiltersButton, { resetFiltersToInitialState: filterBarConfig.resetFiltersToInitialState })) : null] })] }), jsxRuntime.jsx(Separator, {}), jsxRuntime.jsxs(reactComponents.CardBody, { className: "gap-1 p-1", density: "none", disableGap: true, children: [jsxRuntime.jsx(GroupedFiltersList, { className: "flex flex-col gap-1", filterBarConfig: filterBarConfig, filtersGrouped: searchText
890
+ ? searchResultsGrouped
891
+ : showCustomFilters
892
+ ? filtersToShowGrouped
893
+ : removeCustomFieldsGroup(filtersToShowGrouped) }), hasCustomFields && !showCustomFilters && !searchText ? (jsxRuntime.jsx(CustomFieldsHiddenGroup, { appliedCustomFields: appliedCustomFields, filterBarConfig: filterBarConfig, onShow: () => {
894
+ setShowCustomFilters(true);
895
+ } })) : null] })] }));
896
+ };
897
+ const Separator = () => jsxRuntime.jsx("hr", { className: "border-secondary-200", role: "separator" });
898
+ const FiltersAppliedCountLabel = ({ filterBarConfig, }) => {
899
+ const [t] = useTranslation();
900
+ switch (filterBarConfig.appliedFilterKeys().length) {
901
+ case 0:
902
+ return t("filtersBar.appliedFiltersTooltip.none");
903
+ case 1:
904
+ return filterBarConfig.appliedFilterKeys()[0] ? t("filtersMenu.appliedFiltersLabel.singular") : null;
905
+ default:
906
+ return jsxRuntime.jsx(jsxRuntime.Fragment, { children: t("filtersMenu.appliedFiltersLabel.plural", { count: filterBarConfig.appliedFilterKeys().length }) });
907
+ }
908
+ };
909
+ const CustomFieldsHiddenGroup = ({ appliedCustomFields, filterBarConfig, onShow, }) => {
910
+ const [t] = useTranslation();
911
+ return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(Separator, {}), jsxRuntime.jsxs("div", { children: [jsxRuntime.jsx(reactComponents.Button, { "aria-controls": "filters-list", className: "text-primary-600 w-full justify-between px-3", onClick: onShow, prefix: jsxRuntime.jsx(reactComponents.Text, { className: "text-secondary-400", size: "small", uppercase: true, weight: "bold", children: t("filtersBar.groups.CUSTOM_FIELDS") }), size: "small", variant: "ghost-neutral", children: t("filtersBar.showAll") }), appliedCustomFields.length > 0 ? (jsxRuntime.jsx("ul", { "aria-label": "Visible custom fields", "data-testid": "applied-custom-fields-list", children: jsxRuntime.jsx(FiltersRenderer, { filterBarConfig: filterBarConfig, filters: appliedCustomFields, visualStyle: "list-item" }) })) : null] })] }));
912
+ };
913
+
783
914
  /**
784
915
  * TooltipValues component that displays formatted tooltip values based on the provided input.
785
916
  *
@@ -873,100 +1004,31 @@ const MultipleFilterTooltipLabel = ({ filterBarConfig, filterKeys, }) => {
873
1004
  }
874
1005
  };
875
1006
 
876
- /**
877
- * FiltersList is a React component that displays a list of filters within a filter bar.
878
- *
879
- * @returns {ReactElement} - Returns the FiltersList component.
880
- */
881
- const GroupedFiltersList = ({ filterBarConfig, filtersGrouped, className, dataTestId = "grouped-filters-list", }) => {
882
- return (jsxRuntime.jsx("div", { className: className, "data-testid": dataTestId, role: "menu", children: filtersGrouped.map((group, idx) => {
883
- const isLastGroup = idx === filtersGrouped.length - 1;
884
- return (jsxRuntime.jsxs("div", { className: "flex flex-col gap-1", children: [jsxRuntime.jsxs("div", { children: [jsxRuntime.jsx(reactComponents.Text, { className: "text-secondary-400 h-7 px-3 py-2", dataTestId: `${group.key}-group-title`, size: "small", uppercase: true, weight: "bold", children: group.title }), jsxRuntime.jsx("ul", { "aria-labelledby": `${group.key}-group-title`, className: "grid", "data-testid": `${group.key}-group-list`, children: jsxRuntime.jsx(FiltersRenderer, { filterBarConfig: filterBarConfig, filters: group.filters, visualStyle: "list-item" }) })] }), isLastGroup ? null : jsxRuntime.jsx("div", { className: "bg-secondary-200 h-[1px] w-full", role: "separator" })] }, group.key));
885
- }) }));
886
- };
887
-
888
- /**
889
- * ResetFiltersButton is a React component that provides a button for resetting filters.
890
- *
891
- * @returns {ReactElement | null} The rendered ResetFiltersButton component, or null if no filters have been applied.
892
- */
893
- const ResetFiltersButton = ({ resetFiltersToInitialState, dataTestId, className, }) => {
894
- const [t] = useTranslation();
895
- return (jsxRuntime.jsxs(reactComponents.Button, { className: className, dataTestId: dataTestId ?? "reset-filters-button", onClick: () => {
896
- resetFiltersToInitialState();
897
- }, size: "small", variant: "ghost", children: [t("filtersBar.resetFilters"), jsxRuntime.jsx("span", { className: "sr-only", children: "Resets all applied filters" })] }));
898
- };
899
-
900
1007
  /**
901
1008
  * FilterMenu is a React component that displays a list of filters in a popover menu based on the provided filter bar configuration.
902
1009
  *
903
1010
  * @template TFilterBarDefinition - The type representing the filter bar definition.
904
1011
  * @returns {ReactElement} - Returns the FilterMenu component.
905
1012
  */
906
- const FiltersMenu = ({ filterBarDefinition, filterBarConfig, hiddenFilters = [], compact, title, dataTestId = "filters-menu", className, }) => {
1013
+ const FiltersMenu = ({ filterBarDefinition, filterBarConfig, hiddenFilters = [], compact, title, dataTestId = "filters-menu", className, showAppliedFiltersCount = true, buttonProps, allowShowFiltersDirectly = true, }) => {
907
1014
  const [t] = useTranslation();
908
1015
  const { isSm } = reactComponents.useViewportBreakpoints();
909
1016
  const [showCustomFilters, setShowCustomFilters] = react.useState(false);
910
- // TODO: Add analytics if event requirements are defined
911
- // const { logEvent } = useAnalytics(FilterEvents);
912
- const hideInMenu = react.useMemo(() => {
913
- return sharedUtils.objectValues(filterBarDefinition)
914
- .map(filter => {
915
- const showInFilterBar = filter.showInFilterBar ? filter.showInFilterBar() : true;
916
- const showInStarredMenu = filter.showInStarredMenu ? filter.showInStarredMenu() : true; // TODO: Starred menu concept should be completely removed everywhere
917
- const showMenuAnywayBecauseFilterHasChanged = filterBarConfig.appliedFilterKeys().includes(filter.filterKey);
918
- return (!showInFilterBar || !showInStarredMenu) && !showMenuAnywayBecauseFilterHasChanged
919
- ? filter.filterKey
920
- : null;
921
- })
922
- .filter(sharedUtils.truthy);
923
- }, [filterBarConfig, filterBarDefinition]);
924
- const { filtersGrouped } = useGroupFilters(sharedUtils.objectValues(filterBarDefinition), [...hideInMenu, ...hiddenFilters]);
925
- const { appliedFilters, filtersToShow, showDirectlyFilters, hasCustomFields } = react.useMemo(() => {
926
- const allFilters = filtersGrouped.map(group => group.filters).flat();
927
- return {
928
- appliedFilters: allFilters.filter(filter => filterBarConfig.appliedFilterKeys().includes(filter.filterKey)),
929
- filtersToShow: allFilters.filter(filter => !filter.showDirectly),
930
- showDirectlyFilters: allFilters.filter(filter => filter.showDirectly),
931
- hasCustomFields: allFilters.some(filter => filter.group === "CUSTOM_FIELDS"),
932
- };
933
- }, [filterBarConfig, filtersGrouped]);
934
- const [searchResults, searchText, setSearchText] = reactCoreHooks.useTextSearch(filtersToShow, item => [item.title]);
935
- const { filtersGrouped: searchResultsGrouped } = useGroupFilters(searchResults, []);
936
- const { filtersGrouped: filtersToShowGrouped } = useGroupFilters(filtersToShow, []);
937
- const appliedCustomFields = react.useMemo(() => appliedFilters.filter(filter => filter.group === "CUSTOM_FIELDS"), [appliedFilters]);
1017
+ const { appliedFilters, showDirectlyFilters, hasCustomFields, filtersToShowGrouped, searchResultsGrouped, searchText, appliedCustomFields, removeCustomFieldsGroup, setSearchText, } = useFiltersMenu({
1018
+ filterBarDefinition,
1019
+ filterBarConfig,
1020
+ hiddenFilters,
1021
+ allowShowFiltersDirectly,
1022
+ });
938
1023
  return (jsxRuntime.jsxs("div", { className: tailwindMerge.twMerge("flex items-center gap-2", className), "data-testid": dataTestId, children: [jsxRuntime.jsx(reactComponents.Popover, { onOpenStateChange: open => {
939
1024
  if (!open) {
940
1025
  setShowCustomFilters(false);
941
1026
  setSearchText("");
942
1027
  }
943
1028
  }, placement: "bottom-start", children: modalState => {
944
- return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(reactComponents.PopoverTrigger, { children: jsxRuntime.jsx("div", { "data-testid": "starred-filters-menu-trigger", id: "starred-filters-menu-trigger", children: jsxRuntime.jsx(reactComponents.Tooltip, { disabled: !compact || modalState.isOpen, label: jsxRuntime.jsx(MultipleFilterTooltipLabel, { filterBarConfig: filterBarConfig }), children: jsxRuntime.jsx(reactComponents.Button, { prefix: jsxRuntime.jsx(reactComponents.Icon, { ariaHidden: true, color: filterBarConfig.appliedFilterKeys().length > 0 ? "primary" : undefined, name: "Filter", size: "small" }), size: "small", suffix: compact && filterBarConfig.appliedFilterKeys().length > 0 && isSm ? (jsxRuntime.jsxs("div", { children: [jsxRuntime.jsxs("span", { "aria-hidden": true, children: ["(", filterBarConfig.appliedFilterKeys().length, ")"] }), jsxRuntime.jsxs("span", { className: "sr-only", children: [filterBarConfig.appliedFilterKeys().length, " filters applied"] })] })) : undefined, variant: "secondary", children: jsxRuntime.jsx("span", { className: "hidden sm:block", children: title ?? t("filtersBar.filtersHeading") }) }) }) }) }), jsxRuntime.jsx(reactComponents.PopoverContent, { cellPadding: 100, children: jsxRuntime.jsxs(reactComponents.Card, { className: "max-h-[min(600px,_calc(100dvh-32px))] w-[300px] overflow-y-hidden", dataTestId: "starred-filters-menu-popover", children: [jsxRuntime.jsxs("div", { className: "flex flex-col gap-1 p-1", children: [jsxRuntime.jsx(reactFormComponents.Search, { autoFocus: true, dataTestId: "starred-filters-menu-search", fieldSize: "small", id: "search-filters-list", onChange: e => setSearchText(e.currentTarget.value), onClear: () => setSearchText(""), placeholder: t("filtersBar.searchFiltersPlaceholder"), value: searchText }), jsxRuntime.jsxs("div", { className: "flex h-7 items-center justify-between gap-1 px-3", children: [jsxRuntime.jsx(reactComponents.Text, { className: "text-secondary-400", size: "small", children: jsxRuntime.jsx(FiltersAppliedCountLabel, { filterBarConfig: filterBarConfig }) }), filterBarConfig.appliedFilterKeys().length > 0 ? (jsxRuntime.jsx(ResetFiltersButton, { resetFiltersToInitialState: filterBarConfig.resetFiltersToInitialState })) : null] })] }), jsxRuntime.jsx(Separator, {}), jsxRuntime.jsxs(reactComponents.CardBody, { className: "gap-1 p-1", density: "none", disableGap: true, children: [jsxRuntime.jsx(GroupedFiltersList, { className: "flex flex-col gap-1", filterBarConfig: filterBarConfig, filtersGrouped: searchText
945
- ? searchResultsGrouped
946
- : showCustomFilters
947
- ? filtersToShowGrouped
948
- : removeCustomFields(filtersToShowGrouped) }), hasCustomFields && !showCustomFilters && !searchText ? (jsxRuntime.jsx(CustomFieldsHiddenGroup, { appliedCustomFields: appliedCustomFields, filterBarConfig: filterBarConfig, onShow: () => {
949
- setShowCustomFilters(true);
950
- } })) : null] })] }) })] }));
1029
+ return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(reactComponents.PopoverTrigger, { children: jsxRuntime.jsx("div", { "data-testid": "starred-filters-menu-trigger", id: "starred-filters-menu-trigger", children: jsxRuntime.jsx(reactComponents.Tooltip, { disabled: !compact || modalState.isOpen, label: jsxRuntime.jsx(MultipleFilterTooltipLabel, { filterBarConfig: filterBarConfig }), children: jsxRuntime.jsx(reactComponents.Button, { prefix: jsxRuntime.jsx(reactComponents.Icon, { ariaHidden: true, color: filterBarConfig.appliedFilterKeys().length > 0 ? "primary" : undefined, name: "Filter", size: "small" }), size: "small", suffix: compact && showAppliedFiltersCount && filterBarConfig.appliedFilterKeys().length > 0 && isSm ? (jsxRuntime.jsxs("div", { children: [jsxRuntime.jsxs("span", { "aria-hidden": true, children: ["(", filterBarConfig.appliedFilterKeys().length, ")"] }), jsxRuntime.jsxs("span", { className: "sr-only", children: [filterBarConfig.appliedFilterKeys().length, " filters applied"] })] })) : undefined, variant: "secondary", ...buttonProps, children: title !== "" ? (jsxRuntime.jsx("span", { className: "hidden sm:block", children: title ?? t("filtersBar.filtersHeading") })) : null }) }) }) }), jsxRuntime.jsx(reactComponents.PopoverContent, { cellPadding: 100, children: jsxRuntime.jsx(FiltersMenuContent, { appliedCustomFields: appliedCustomFields, filterBarConfig: filterBarConfig, filtersToShowGrouped: filtersToShowGrouped, hasCustomFields: hasCustomFields, removeCustomFieldsGroup: removeCustomFieldsGroup, searchResultsGrouped: searchResultsGrouped, searchText: searchText, setSearchText: setSearchText, setShowCustomFilters: setShowCustomFilters, showCustomFilters: showCustomFilters }) })] }));
951
1030
  } }), showDirectlyFilters.length > 0 ? (jsxRuntime.jsx(FiltersRenderer, { filterBarConfig: filterBarConfig, filters: showDirectlyFilters })) : null, !compact ? (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [appliedFilters.filter(filter => !filter.showDirectly).length > 0 ? (jsxRuntime.jsx("div", { className: "h-4 w-[1px] bg-slate-300", "data-testid": "applied-filters-buttons" })) : null, jsxRuntime.jsx(FiltersRenderer, { filterBarConfig: filterBarConfig, filters: appliedFilters }), filterBarConfig.appliedFilterKeys().length > 0 ? (jsxRuntime.jsx(ResetFiltersButton, { resetFiltersToInitialState: filterBarConfig.resetFiltersToInitialState })) : null] })) : null] }));
952
1031
  };
953
- const Separator = () => jsxRuntime.jsx("hr", { className: "border-secondary-200", role: "separator" });
954
- const FiltersAppliedCountLabel = ({ filterBarConfig, }) => {
955
- const [t] = useTranslation();
956
- switch (filterBarConfig.appliedFilterKeys().length) {
957
- case 0:
958
- return t("filtersBar.appliedFiltersTooltip.none");
959
- case 1:
960
- return filterBarConfig.appliedFilterKeys()[0] ? t("filtersMenu.appliedFiltersLabel.singular") : null;
961
- default:
962
- return jsxRuntime.jsx(jsxRuntime.Fragment, { children: t("filtersMenu.appliedFiltersLabel.plural", { count: filterBarConfig.appliedFilterKeys().length }) });
963
- }
964
- };
965
- const CustomFieldsHiddenGroup = ({ appliedCustomFields, filterBarConfig, onShow, }) => {
966
- const [t] = useTranslation();
967
- return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(Separator, {}), jsxRuntime.jsxs("div", { children: [jsxRuntime.jsx(reactComponents.Button, { "aria-controls": "filters-list", className: "text-primary-600 w-full justify-between px-3", onClick: onShow, prefix: jsxRuntime.jsx(reactComponents.Text, { className: "text-secondary-400", size: "small", uppercase: true, weight: "bold", children: t("filtersBar.groups.CUSTOM_FIELDS") }), size: "small", variant: "ghost-neutral", children: t("filtersBar.showAll") }), appliedCustomFields.length > 0 ? (jsxRuntime.jsx("ul", { "aria-label": "Visible custom fields", "data-testid": "applied-custom-fields-list", children: jsxRuntime.jsx(FiltersRenderer, { filterBarConfig: filterBarConfig, filters: appliedCustomFields, visualStyle: "list-item" }) })) : null] })] }));
968
- };
969
- const removeCustomFields = (filtersGrouped) => filtersGrouped.filter(group => group.key !== "CUSTOM_FIELDS");
970
1032
 
971
1033
  /**
972
1034
  * Filter is a React component that renders a filter element based on the provided filter definition and state.
@@ -1225,8 +1287,8 @@ const HierarchicalCheckboxFilter = ({ filterDefinition, filterBarActions, option
1225
1287
  /**
1226
1288
  * The FilterBar component serves as a wrapper for managing filters.
1227
1289
  */
1228
- const FilterBar = ({ hiddenFilters, className, filterBarDefinition, filterBarConfig, compact = true, title, }) => {
1229
- return (jsxRuntime.jsx(FiltersMenu, { className: className, compact: compact, dataTestId: `${filterBarConfig.name}-filterbar`, filterBarConfig: filterBarConfig, filterBarDefinition: filterBarDefinition, hiddenFilters: hiddenFilters, title: title }));
1290
+ const FilterBar = ({ hiddenFilters, className, filterBarDefinition, filterBarConfig, compact = true, title, allowShowFiltersDirectly = true, }) => {
1291
+ return (jsxRuntime.jsx(FiltersMenu, { allowShowFiltersDirectly: allowShowFiltersDirectly, className: className, compact: compact, dataTestId: `${filterBarConfig.name}-filterbar`, filterBarConfig: filterBarConfig, filterBarDefinition: filterBarDefinition, hiddenFilters: hiddenFilters, title: title }));
1230
1292
  };
1231
1293
 
1232
1294
  // Can't import jest.fn so must define a function that does nothing but mimics the jest.fn
@@ -1888,8 +1950,8 @@ const useFilterBar = ({ name, onValuesChange, filterBarDefinition, }) => {
1888
1950
  setValue: (key, callback) => setValue(setFilterBarConfig, key, callback),
1889
1951
  });
1890
1952
  react.useEffect(() => {
1891
- onValuesChange?.(filterBarConfig.values);
1892
1953
  saveData(filterBarConfig, filterBarDefinition);
1954
+ onValuesChange?.(filterBarConfig.values);
1893
1955
  }, [filterBarConfig, filterBarDefinition, onValuesChange, saveData]);
1894
1956
  return react.useMemo(() => {
1895
1957
  return {
@@ -2048,12 +2110,14 @@ exports.DefaultMinMaxFilter = DefaultMinMaxFilter;
2048
2110
  exports.DefaultRadioFilter = DefaultRadioFilter;
2049
2111
  exports.DynamicFilterList = DynamicFilterList;
2050
2112
  exports.FilterBar = FilterBar;
2113
+ exports.FilterButtonTooltipLabel = FilterButtonTooltipLabel;
2051
2114
  exports.FilterComponent = FilterComponent;
2052
2115
  exports.FilterEvents = FilterEvents;
2053
2116
  exports.FilterHeader = FilterHeader;
2054
2117
  exports.FilterResults = FilterResults;
2055
2118
  exports.FilterTableComponent = FilterTableComponent;
2056
2119
  exports.FiltersMenu = FiltersMenu;
2120
+ exports.FiltersMenuContent = FiltersMenuContent;
2057
2121
  exports.FiltersRenderer = FiltersRenderer;
2058
2122
  exports.GroupedFiltersList = GroupedFiltersList;
2059
2123
  exports.HierarchicalCheckboxFilter = HierarchicalCheckboxFilter;
@@ -2072,5 +2136,6 @@ exports.mockFilterBar = mockFilterBar;
2072
2136
  exports.toggleFilterValue = toggleFilterValue;
2073
2137
  exports.useFilterBar = useFilterBar;
2074
2138
  exports.useFilterBarAsync = useFilterBarAsync;
2139
+ exports.useFiltersMenu = useFiltersMenu;
2075
2140
  exports.useSearchParamAsFilter = useSearchParamAsFilter;
2076
2141
  exports.validateFilter = validateFilter;
package/index.esm.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
2
2
  import { registerTranslations, useNamespaceTranslation } from '@trackunit/i18n-library-translation';
3
- import { VirtualizedList, Text, Button, useViewportBreakpoints, Popover, PopoverTrigger, Tooltip, Icon, PopoverContent, Card, CardBody, IconButton, MenuList } from '@trackunit/react-components';
3
+ import { VirtualizedList, Text, Button, Card, CardBody, useViewportBreakpoints, Popover, PopoverTrigger, Tooltip, Icon, PopoverContent, IconButton, MenuList } from '@trackunit/react-components';
4
4
  import { useAnalytics, useTextSearch, useCurrentUser } from '@trackunit/react-core-hooks';
5
5
  import { FilterBody, RadioFilterItem, CheckBoxFilterItem, FilterHeader as FilterHeader$1, FilterFooter, Filter } from '@trackunit/react-filter-components';
6
6
  import { useRef, useMemo, useState, useEffect, useCallback, Fragment as Fragment$1 } from 'react';
@@ -643,6 +643,25 @@ const DefaultRadioFilter = ({ filterDefinition, filterBarActions, options, loadi
643
643
  }, value: selectedRadioId?.key || "", children: jsx(DynamicFilterList, { checked: index => filterBarActions.objectIncludesValue(filterDefinition.filterKey, res[index]?.key || ""), className: "m-1 mt-0", count: index => res[index]?.count, keyMapper: index => res[index]?.key || "", labelMapper: index => res[index]?.label || "", rowCount: res.length, showRequestMoreUseSearch: showRequestMoreUseSearch, type: "Radio" }) })) })] }));
644
644
  };
645
645
 
646
+ /**
647
+ * Tooltip label for the filter button
648
+ */
649
+ const FilterButtonTooltipLabel = ({ filterBarConfig, }) => {
650
+ const [t] = useTranslation();
651
+ switch (filterBarConfig.appliedFilterKeys().length) {
652
+ case 0:
653
+ return t("filtersBar.appliedFiltersTooltip.none");
654
+ case 1:
655
+ return filterBarConfig.appliedFilterKeys()[0]
656
+ ? t("filtersBar.appliedFiltersTooltip.singular", {
657
+ filterName: filterBarConfig.getFilterTitle(filterBarConfig.appliedFilterKeys()[0]),
658
+ })
659
+ : null; // should never happen though
660
+ default:
661
+ return (jsxs(Fragment, { children: [t("filtersBar.appliedFiltersTooltip.plural", { count: filterBarConfig.appliedFilterKeys().length }), jsx("ul", { className: "list-inside", children: filterBarConfig.appliedFilterKeys().map((appliedFilterKey, index) => (jsx("li", { className: "list-disc", children: filterBarConfig.getFilterTitle(appliedFilterKey) }, index))) })] }));
662
+ }
663
+ };
664
+
646
665
  /**
647
666
  * Returns the two first values, appends counter if more.
648
667
  *
@@ -758,6 +777,64 @@ const useGroupFilters = (filterDefinitions, hiddenFilters) => {
758
777
  };
759
778
  const uniqueKeysFromGroups = (filters) => [...new Set(filters.map(filter => filter.group))];
760
779
 
780
+ /**
781
+ * This hook is used to manage the filters menu.
782
+ * It returns the filters that should be shown in the menu, the filters that should be shown directly in the filter bar,
783
+ * and the filters that should be shown in the search results.
784
+ */
785
+ const useFiltersMenu = ({ filterBarDefinition, filterBarConfig, hiddenFilters = [], allowShowFiltersDirectly = true, }) => {
786
+ const hideInMenu = useMemo(() => {
787
+ return objectValues(filterBarDefinition)
788
+ .map(filter => {
789
+ const showInFilterBar = filter.showInFilterBar ? filter.showInFilterBar() : true;
790
+ const showInStarredMenu = filter.showInStarredMenu ? filter.showInStarredMenu() : true;
791
+ const showMenuAnywayBecauseFilterHasChanged = filterBarConfig.appliedFilterKeys().includes(filter.filterKey);
792
+ return (!showInFilterBar || !showInStarredMenu) && !showMenuAnywayBecauseFilterHasChanged
793
+ ? filter.filterKey
794
+ : null;
795
+ })
796
+ .filter(truthy);
797
+ }, [filterBarConfig, filterBarDefinition]);
798
+ const removeCustomFieldsGroup = useCallback((groupOfFilters) => groupOfFilters.filter(group => group.key !== "CUSTOM_FIELDS"), []);
799
+ const { filtersGrouped } = useGroupFilters(objectValues(filterBarDefinition), [...hideInMenu, ...hiddenFilters]);
800
+ const { appliedFilters, filtersToShow, showDirectlyFilters, hasCustomFields } = useMemo(() => {
801
+ const allFilters = filtersGrouped.map(group => group.filters).flat();
802
+ return {
803
+ appliedFilters: allFilters.filter(filter => filterBarConfig.appliedFilterKeys().includes(filter.filterKey)),
804
+ filtersToShow: allFilters.filter(filter => !filter.showDirectly),
805
+ showDirectlyFilters: allFilters.filter(filter => filter.showDirectly && allowShowFiltersDirectly),
806
+ hasCustomFields: allFilters.some(filter => filter.group === "CUSTOM_FIELDS"),
807
+ };
808
+ }, [filterBarConfig, filtersGrouped, allowShowFiltersDirectly]);
809
+ const [searchResults, searchText, setSearchText] = useTextSearch(filtersToShow, item => [item.title]);
810
+ const { filtersGrouped: searchResultsGrouped } = useGroupFilters(searchResults, []);
811
+ const { filtersGrouped: filtersToShowGrouped } = useGroupFilters(filtersToShow, []);
812
+ const appliedCustomFields = useMemo(() => appliedFilters.filter(filter => filter.group === "CUSTOM_FIELDS"), [appliedFilters]);
813
+ return useMemo(() => {
814
+ return {
815
+ appliedFilters,
816
+ hasCustomFields,
817
+ showDirectlyFilters,
818
+ appliedCustomFields,
819
+ searchText,
820
+ setSearchText,
821
+ filtersToShowGrouped,
822
+ searchResultsGrouped,
823
+ removeCustomFieldsGroup,
824
+ };
825
+ }, [
826
+ appliedFilters,
827
+ hasCustomFields,
828
+ showDirectlyFilters,
829
+ appliedCustomFields,
830
+ searchText,
831
+ setSearchText,
832
+ filtersToShowGrouped,
833
+ searchResultsGrouped,
834
+ removeCustomFieldsGroup,
835
+ ]);
836
+ };
837
+
761
838
  /**
762
839
  * FiltersRenderer renders an array of Filter components from filter definitions
763
840
  * It ignores hidden filters.
@@ -778,6 +855,60 @@ const FiltersRenderer = ({ filters, filterBarConfig, visualStyle, }) => {
778
855
  .map(filter => (jsx(FilterComponent, { filter: filter, filterBarActions: filterBarConfig, filterState: { values: filterBarConfig.values, setters: filterBarConfig.setters }, visualStyle: visualStyle }, `filter-${filter.filterKey}`)));
779
856
  };
780
857
 
858
+ /**
859
+ * FiltersList is a React component that displays a list of filters within a filter bar.
860
+ *
861
+ * @returns {ReactElement} - Returns the FiltersList component.
862
+ */
863
+ const GroupedFiltersList = ({ filterBarConfig, filtersGrouped, className, dataTestId = "grouped-filters-list", }) => {
864
+ return (jsx("div", { className: className, "data-testid": dataTestId, role: "menu", children: filtersGrouped.map((group, idx) => {
865
+ const isLastGroup = idx === filtersGrouped.length - 1;
866
+ return (jsxs("div", { className: "flex flex-col gap-1", children: [jsxs("div", { children: [jsx(Text, { className: "text-secondary-400 h-7 px-3 py-2", dataTestId: `${group.key}-group-title`, size: "small", uppercase: true, weight: "bold", children: group.title }), jsx("ul", { "aria-labelledby": `${group.key}-group-title`, className: "grid", "data-testid": `${group.key}-group-list`, children: jsx(FiltersRenderer, { filterBarConfig: filterBarConfig, filters: group.filters, visualStyle: "list-item" }) })] }), isLastGroup ? null : jsx("div", { className: "bg-secondary-200 h-[1px] w-full", role: "separator" })] }, group.key));
867
+ }) }));
868
+ };
869
+
870
+ /**
871
+ * ResetFiltersButton is a React component that provides a button for resetting filters.
872
+ *
873
+ * @returns {ReactElement | null} The rendered ResetFiltersButton component, or null if no filters have been applied.
874
+ */
875
+ const ResetFiltersButton = ({ resetFiltersToInitialState, dataTestId, className, }) => {
876
+ const [t] = useTranslation();
877
+ return (jsxs(Button, { className: className, dataTestId: dataTestId ?? "reset-filters-button", onClick: () => {
878
+ resetFiltersToInitialState();
879
+ }, size: "small", variant: "ghost", children: [t("filtersBar.resetFilters"), jsx("span", { className: "sr-only", children: "Resets all applied filters" })] }));
880
+ };
881
+
882
+ /**
883
+ *
884
+ */
885
+ const FiltersMenuContent = ({ filterBarConfig, setShowCustomFilters, setSearchText, searchText, searchResultsGrouped, filtersToShowGrouped, removeCustomFieldsGroup, hasCustomFields, appliedCustomFields, showCustomFilters, }) => {
886
+ const [t] = useTranslation();
887
+ return (jsxs(Card, { className: "max-h-[min(600px,_calc(100dvh-32px))] w-[300px] overflow-y-hidden", dataTestId: "starred-filters-menu-popover", children: [jsxs("div", { className: " flex-col gap-1 p-1", children: [jsx(Search, { autoFocus: true, dataTestId: "starred-filters-menu-search", fieldSize: "small", id: "search-filters-list", onChange: e => setSearchText(e.currentTarget.value), onClear: () => setSearchText(""), placeholder: t("filtersBar.searchFiltersPlaceholder"), value: searchText }), jsxs("div", { className: "flex h-7 items-center justify-between gap-1 px-3", children: [jsx(Text, { className: "text-secondary-400", size: "small", children: jsx(FiltersAppliedCountLabel, { filterBarConfig: filterBarConfig }) }), filterBarConfig.appliedFilterKeys().length > 0 ? (jsx(ResetFiltersButton, { resetFiltersToInitialState: filterBarConfig.resetFiltersToInitialState })) : null] })] }), jsx(Separator, {}), jsxs(CardBody, { className: "gap-1 p-1", density: "none", disableGap: true, children: [jsx(GroupedFiltersList, { className: "flex flex-col gap-1", filterBarConfig: filterBarConfig, filtersGrouped: searchText
888
+ ? searchResultsGrouped
889
+ : showCustomFilters
890
+ ? filtersToShowGrouped
891
+ : removeCustomFieldsGroup(filtersToShowGrouped) }), hasCustomFields && !showCustomFilters && !searchText ? (jsx(CustomFieldsHiddenGroup, { appliedCustomFields: appliedCustomFields, filterBarConfig: filterBarConfig, onShow: () => {
892
+ setShowCustomFilters(true);
893
+ } })) : null] })] }));
894
+ };
895
+ const Separator = () => jsx("hr", { className: "border-secondary-200", role: "separator" });
896
+ const FiltersAppliedCountLabel = ({ filterBarConfig, }) => {
897
+ const [t] = useTranslation();
898
+ switch (filterBarConfig.appliedFilterKeys().length) {
899
+ case 0:
900
+ return t("filtersBar.appliedFiltersTooltip.none");
901
+ case 1:
902
+ return filterBarConfig.appliedFilterKeys()[0] ? t("filtersMenu.appliedFiltersLabel.singular") : null;
903
+ default:
904
+ return jsx(Fragment, { children: t("filtersMenu.appliedFiltersLabel.plural", { count: filterBarConfig.appliedFilterKeys().length }) });
905
+ }
906
+ };
907
+ const CustomFieldsHiddenGroup = ({ appliedCustomFields, filterBarConfig, onShow, }) => {
908
+ const [t] = useTranslation();
909
+ return (jsxs(Fragment, { children: [jsx(Separator, {}), jsxs("div", { children: [jsx(Button, { "aria-controls": "filters-list", className: "text-primary-600 w-full justify-between px-3", onClick: onShow, prefix: jsx(Text, { className: "text-secondary-400", size: "small", uppercase: true, weight: "bold", children: t("filtersBar.groups.CUSTOM_FIELDS") }), size: "small", variant: "ghost-neutral", children: t("filtersBar.showAll") }), appliedCustomFields.length > 0 ? (jsx("ul", { "aria-label": "Visible custom fields", "data-testid": "applied-custom-fields-list", children: jsx(FiltersRenderer, { filterBarConfig: filterBarConfig, filters: appliedCustomFields, visualStyle: "list-item" }) })) : null] })] }));
910
+ };
911
+
781
912
  /**
782
913
  * TooltipValues component that displays formatted tooltip values based on the provided input.
783
914
  *
@@ -871,100 +1002,31 @@ const MultipleFilterTooltipLabel = ({ filterBarConfig, filterKeys, }) => {
871
1002
  }
872
1003
  };
873
1004
 
874
- /**
875
- * FiltersList is a React component that displays a list of filters within a filter bar.
876
- *
877
- * @returns {ReactElement} - Returns the FiltersList component.
878
- */
879
- const GroupedFiltersList = ({ filterBarConfig, filtersGrouped, className, dataTestId = "grouped-filters-list", }) => {
880
- return (jsx("div", { className: className, "data-testid": dataTestId, role: "menu", children: filtersGrouped.map((group, idx) => {
881
- const isLastGroup = idx === filtersGrouped.length - 1;
882
- return (jsxs("div", { className: "flex flex-col gap-1", children: [jsxs("div", { children: [jsx(Text, { className: "text-secondary-400 h-7 px-3 py-2", dataTestId: `${group.key}-group-title`, size: "small", uppercase: true, weight: "bold", children: group.title }), jsx("ul", { "aria-labelledby": `${group.key}-group-title`, className: "grid", "data-testid": `${group.key}-group-list`, children: jsx(FiltersRenderer, { filterBarConfig: filterBarConfig, filters: group.filters, visualStyle: "list-item" }) })] }), isLastGroup ? null : jsx("div", { className: "bg-secondary-200 h-[1px] w-full", role: "separator" })] }, group.key));
883
- }) }));
884
- };
885
-
886
- /**
887
- * ResetFiltersButton is a React component that provides a button for resetting filters.
888
- *
889
- * @returns {ReactElement | null} The rendered ResetFiltersButton component, or null if no filters have been applied.
890
- */
891
- const ResetFiltersButton = ({ resetFiltersToInitialState, dataTestId, className, }) => {
892
- const [t] = useTranslation();
893
- return (jsxs(Button, { className: className, dataTestId: dataTestId ?? "reset-filters-button", onClick: () => {
894
- resetFiltersToInitialState();
895
- }, size: "small", variant: "ghost", children: [t("filtersBar.resetFilters"), jsx("span", { className: "sr-only", children: "Resets all applied filters" })] }));
896
- };
897
-
898
1005
  /**
899
1006
  * FilterMenu is a React component that displays a list of filters in a popover menu based on the provided filter bar configuration.
900
1007
  *
901
1008
  * @template TFilterBarDefinition - The type representing the filter bar definition.
902
1009
  * @returns {ReactElement} - Returns the FilterMenu component.
903
1010
  */
904
- const FiltersMenu = ({ filterBarDefinition, filterBarConfig, hiddenFilters = [], compact, title, dataTestId = "filters-menu", className, }) => {
1011
+ const FiltersMenu = ({ filterBarDefinition, filterBarConfig, hiddenFilters = [], compact, title, dataTestId = "filters-menu", className, showAppliedFiltersCount = true, buttonProps, allowShowFiltersDirectly = true, }) => {
905
1012
  const [t] = useTranslation();
906
1013
  const { isSm } = useViewportBreakpoints();
907
1014
  const [showCustomFilters, setShowCustomFilters] = useState(false);
908
- // TODO: Add analytics if event requirements are defined
909
- // const { logEvent } = useAnalytics(FilterEvents);
910
- const hideInMenu = useMemo(() => {
911
- return objectValues(filterBarDefinition)
912
- .map(filter => {
913
- const showInFilterBar = filter.showInFilterBar ? filter.showInFilterBar() : true;
914
- const showInStarredMenu = filter.showInStarredMenu ? filter.showInStarredMenu() : true; // TODO: Starred menu concept should be completely removed everywhere
915
- const showMenuAnywayBecauseFilterHasChanged = filterBarConfig.appliedFilterKeys().includes(filter.filterKey);
916
- return (!showInFilterBar || !showInStarredMenu) && !showMenuAnywayBecauseFilterHasChanged
917
- ? filter.filterKey
918
- : null;
919
- })
920
- .filter(truthy);
921
- }, [filterBarConfig, filterBarDefinition]);
922
- const { filtersGrouped } = useGroupFilters(objectValues(filterBarDefinition), [...hideInMenu, ...hiddenFilters]);
923
- const { appliedFilters, filtersToShow, showDirectlyFilters, hasCustomFields } = useMemo(() => {
924
- const allFilters = filtersGrouped.map(group => group.filters).flat();
925
- return {
926
- appliedFilters: allFilters.filter(filter => filterBarConfig.appliedFilterKeys().includes(filter.filterKey)),
927
- filtersToShow: allFilters.filter(filter => !filter.showDirectly),
928
- showDirectlyFilters: allFilters.filter(filter => filter.showDirectly),
929
- hasCustomFields: allFilters.some(filter => filter.group === "CUSTOM_FIELDS"),
930
- };
931
- }, [filterBarConfig, filtersGrouped]);
932
- const [searchResults, searchText, setSearchText] = useTextSearch(filtersToShow, item => [item.title]);
933
- const { filtersGrouped: searchResultsGrouped } = useGroupFilters(searchResults, []);
934
- const { filtersGrouped: filtersToShowGrouped } = useGroupFilters(filtersToShow, []);
935
- const appliedCustomFields = useMemo(() => appliedFilters.filter(filter => filter.group === "CUSTOM_FIELDS"), [appliedFilters]);
1015
+ const { appliedFilters, showDirectlyFilters, hasCustomFields, filtersToShowGrouped, searchResultsGrouped, searchText, appliedCustomFields, removeCustomFieldsGroup, setSearchText, } = useFiltersMenu({
1016
+ filterBarDefinition,
1017
+ filterBarConfig,
1018
+ hiddenFilters,
1019
+ allowShowFiltersDirectly,
1020
+ });
936
1021
  return (jsxs("div", { className: twMerge("flex items-center gap-2", className), "data-testid": dataTestId, children: [jsx(Popover, { onOpenStateChange: open => {
937
1022
  if (!open) {
938
1023
  setShowCustomFilters(false);
939
1024
  setSearchText("");
940
1025
  }
941
1026
  }, placement: "bottom-start", children: modalState => {
942
- return (jsxs(Fragment, { children: [jsx(PopoverTrigger, { children: jsx("div", { "data-testid": "starred-filters-menu-trigger", id: "starred-filters-menu-trigger", children: jsx(Tooltip, { disabled: !compact || modalState.isOpen, label: jsx(MultipleFilterTooltipLabel, { filterBarConfig: filterBarConfig }), children: jsx(Button, { prefix: jsx(Icon, { ariaHidden: true, color: filterBarConfig.appliedFilterKeys().length > 0 ? "primary" : undefined, name: "Filter", size: "small" }), size: "small", suffix: compact && filterBarConfig.appliedFilterKeys().length > 0 && isSm ? (jsxs("div", { children: [jsxs("span", { "aria-hidden": true, children: ["(", filterBarConfig.appliedFilterKeys().length, ")"] }), jsxs("span", { className: "sr-only", children: [filterBarConfig.appliedFilterKeys().length, " filters applied"] })] })) : undefined, variant: "secondary", children: jsx("span", { className: "hidden sm:block", children: title ?? t("filtersBar.filtersHeading") }) }) }) }) }), jsx(PopoverContent, { cellPadding: 100, children: jsxs(Card, { className: "max-h-[min(600px,_calc(100dvh-32px))] w-[300px] overflow-y-hidden", dataTestId: "starred-filters-menu-popover", children: [jsxs("div", { className: "flex flex-col gap-1 p-1", children: [jsx(Search, { autoFocus: true, dataTestId: "starred-filters-menu-search", fieldSize: "small", id: "search-filters-list", onChange: e => setSearchText(e.currentTarget.value), onClear: () => setSearchText(""), placeholder: t("filtersBar.searchFiltersPlaceholder"), value: searchText }), jsxs("div", { className: "flex h-7 items-center justify-between gap-1 px-3", children: [jsx(Text, { className: "text-secondary-400", size: "small", children: jsx(FiltersAppliedCountLabel, { filterBarConfig: filterBarConfig }) }), filterBarConfig.appliedFilterKeys().length > 0 ? (jsx(ResetFiltersButton, { resetFiltersToInitialState: filterBarConfig.resetFiltersToInitialState })) : null] })] }), jsx(Separator, {}), jsxs(CardBody, { className: "gap-1 p-1", density: "none", disableGap: true, children: [jsx(GroupedFiltersList, { className: "flex flex-col gap-1", filterBarConfig: filterBarConfig, filtersGrouped: searchText
943
- ? searchResultsGrouped
944
- : showCustomFilters
945
- ? filtersToShowGrouped
946
- : removeCustomFields(filtersToShowGrouped) }), hasCustomFields && !showCustomFilters && !searchText ? (jsx(CustomFieldsHiddenGroup, { appliedCustomFields: appliedCustomFields, filterBarConfig: filterBarConfig, onShow: () => {
947
- setShowCustomFilters(true);
948
- } })) : null] })] }) })] }));
1027
+ return (jsxs(Fragment, { children: [jsx(PopoverTrigger, { children: jsx("div", { "data-testid": "starred-filters-menu-trigger", id: "starred-filters-menu-trigger", children: jsx(Tooltip, { disabled: !compact || modalState.isOpen, label: jsx(MultipleFilterTooltipLabel, { filterBarConfig: filterBarConfig }), children: jsx(Button, { prefix: jsx(Icon, { ariaHidden: true, color: filterBarConfig.appliedFilterKeys().length > 0 ? "primary" : undefined, name: "Filter", size: "small" }), size: "small", suffix: compact && showAppliedFiltersCount && filterBarConfig.appliedFilterKeys().length > 0 && isSm ? (jsxs("div", { children: [jsxs("span", { "aria-hidden": true, children: ["(", filterBarConfig.appliedFilterKeys().length, ")"] }), jsxs("span", { className: "sr-only", children: [filterBarConfig.appliedFilterKeys().length, " filters applied"] })] })) : undefined, variant: "secondary", ...buttonProps, children: title !== "" ? (jsx("span", { className: "hidden sm:block", children: title ?? t("filtersBar.filtersHeading") })) : null }) }) }) }), jsx(PopoverContent, { cellPadding: 100, children: jsx(FiltersMenuContent, { appliedCustomFields: appliedCustomFields, filterBarConfig: filterBarConfig, filtersToShowGrouped: filtersToShowGrouped, hasCustomFields: hasCustomFields, removeCustomFieldsGroup: removeCustomFieldsGroup, searchResultsGrouped: searchResultsGrouped, searchText: searchText, setSearchText: setSearchText, setShowCustomFilters: setShowCustomFilters, showCustomFilters: showCustomFilters }) })] }));
949
1028
  } }), showDirectlyFilters.length > 0 ? (jsx(FiltersRenderer, { filterBarConfig: filterBarConfig, filters: showDirectlyFilters })) : null, !compact ? (jsxs(Fragment, { children: [appliedFilters.filter(filter => !filter.showDirectly).length > 0 ? (jsx("div", { className: "h-4 w-[1px] bg-slate-300", "data-testid": "applied-filters-buttons" })) : null, jsx(FiltersRenderer, { filterBarConfig: filterBarConfig, filters: appliedFilters }), filterBarConfig.appliedFilterKeys().length > 0 ? (jsx(ResetFiltersButton, { resetFiltersToInitialState: filterBarConfig.resetFiltersToInitialState })) : null] })) : null] }));
950
1029
  };
951
- const Separator = () => jsx("hr", { className: "border-secondary-200", role: "separator" });
952
- const FiltersAppliedCountLabel = ({ filterBarConfig, }) => {
953
- const [t] = useTranslation();
954
- switch (filterBarConfig.appliedFilterKeys().length) {
955
- case 0:
956
- return t("filtersBar.appliedFiltersTooltip.none");
957
- case 1:
958
- return filterBarConfig.appliedFilterKeys()[0] ? t("filtersMenu.appliedFiltersLabel.singular") : null;
959
- default:
960
- return jsx(Fragment, { children: t("filtersMenu.appliedFiltersLabel.plural", { count: filterBarConfig.appliedFilterKeys().length }) });
961
- }
962
- };
963
- const CustomFieldsHiddenGroup = ({ appliedCustomFields, filterBarConfig, onShow, }) => {
964
- const [t] = useTranslation();
965
- return (jsxs(Fragment, { children: [jsx(Separator, {}), jsxs("div", { children: [jsx(Button, { "aria-controls": "filters-list", className: "text-primary-600 w-full justify-between px-3", onClick: onShow, prefix: jsx(Text, { className: "text-secondary-400", size: "small", uppercase: true, weight: "bold", children: t("filtersBar.groups.CUSTOM_FIELDS") }), size: "small", variant: "ghost-neutral", children: t("filtersBar.showAll") }), appliedCustomFields.length > 0 ? (jsx("ul", { "aria-label": "Visible custom fields", "data-testid": "applied-custom-fields-list", children: jsx(FiltersRenderer, { filterBarConfig: filterBarConfig, filters: appliedCustomFields, visualStyle: "list-item" }) })) : null] })] }));
966
- };
967
- const removeCustomFields = (filtersGrouped) => filtersGrouped.filter(group => group.key !== "CUSTOM_FIELDS");
968
1030
 
969
1031
  /**
970
1032
  * Filter is a React component that renders a filter element based on the provided filter definition and state.
@@ -1223,8 +1285,8 @@ const HierarchicalCheckboxFilter = ({ filterDefinition, filterBarActions, option
1223
1285
  /**
1224
1286
  * The FilterBar component serves as a wrapper for managing filters.
1225
1287
  */
1226
- const FilterBar = ({ hiddenFilters, className, filterBarDefinition, filterBarConfig, compact = true, title, }) => {
1227
- return (jsx(FiltersMenu, { className: className, compact: compact, dataTestId: `${filterBarConfig.name}-filterbar`, filterBarConfig: filterBarConfig, filterBarDefinition: filterBarDefinition, hiddenFilters: hiddenFilters, title: title }));
1288
+ const FilterBar = ({ hiddenFilters, className, filterBarDefinition, filterBarConfig, compact = true, title, allowShowFiltersDirectly = true, }) => {
1289
+ return (jsx(FiltersMenu, { allowShowFiltersDirectly: allowShowFiltersDirectly, className: className, compact: compact, dataTestId: `${filterBarConfig.name}-filterbar`, filterBarConfig: filterBarConfig, filterBarDefinition: filterBarDefinition, hiddenFilters: hiddenFilters, title: title }));
1228
1290
  };
1229
1291
 
1230
1292
  // Can't import jest.fn so must define a function that does nothing but mimics the jest.fn
@@ -1886,8 +1948,8 @@ const useFilterBar = ({ name, onValuesChange, filterBarDefinition, }) => {
1886
1948
  setValue: (key, callback) => setValue(setFilterBarConfig, key, callback),
1887
1949
  });
1888
1950
  useEffect(() => {
1889
- onValuesChange?.(filterBarConfig.values);
1890
1951
  saveData(filterBarConfig, filterBarDefinition);
1952
+ onValuesChange?.(filterBarConfig.values);
1891
1953
  }, [filterBarConfig, filterBarDefinition, onValuesChange, saveData]);
1892
1954
  return useMemo(() => {
1893
1955
  return {
@@ -2040,4 +2102,4 @@ const mergeFilters = (filterBarDefinition, extraFilters) => {
2040
2102
  */
2041
2103
  setupLibraryTranslations();
2042
2104
 
2043
- export { DefaultCheckboxFilter, DefaultDateRangeFilter, DefaultMinMaxFilter, DefaultRadioFilter, DynamicFilterList, FilterBar, FilterComponent, FilterEvents, FilterHeader, FilterResults, FilterTableComponent, FiltersMenu, FiltersRenderer, GroupedFiltersList, HierarchicalCheckboxFilter, ResetFiltersButton, areaFilterGeoJsonGeometrySchema, isAreaFilterValue, isArrayFilterValue, isBooleanValue, isDateRangeValue, isMinMaxFilterValue, isStringArrayFilterValue, isValueName, isValueNameArray, mergeFilters, mockFilterBar, toggleFilterValue, useFilterBar, useFilterBarAsync, useSearchParamAsFilter, validateFilter };
2105
+ export { DefaultCheckboxFilter, DefaultDateRangeFilter, DefaultMinMaxFilter, DefaultRadioFilter, DynamicFilterList, FilterBar, FilterButtonTooltipLabel, FilterComponent, FilterEvents, FilterHeader, FilterResults, FilterTableComponent, FiltersMenu, FiltersMenuContent, FiltersRenderer, GroupedFiltersList, HierarchicalCheckboxFilter, ResetFiltersButton, areaFilterGeoJsonGeometrySchema, isAreaFilterValue, isArrayFilterValue, isBooleanValue, isDateRangeValue, isMinMaxFilterValue, isStringArrayFilterValue, isValueName, isValueNameArray, mergeFilters, mockFilterBar, toggleFilterValue, useFilterBar, useFilterBarAsync, useFiltersMenu, useSearchParamAsFilter, validateFilter };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trackunit/filters-filter-bar",
3
- "version": "1.3.200",
3
+ "version": "1.3.201",
4
4
  "repository": "https://github.com/Trackunit/manager",
5
5
  "license": "SEE LICENSE IN LICENSE.txt",
6
6
  "engines": {
@@ -14,17 +14,17 @@
14
14
  "tailwind-merge": "^2.0.0",
15
15
  "string-ts": "^2.0.0",
16
16
  "zod": "3.23.4",
17
- "@trackunit/iris-app-api": "1.3.127",
17
+ "@trackunit/iris-app-api": "1.3.128",
18
18
  "@trackunit/react-core-hooks": "1.3.131",
19
- "@trackunit/react-filter-components": "1.3.165",
20
- "@trackunit/react-date-and-time-components": "1.3.167",
19
+ "@trackunit/react-filter-components": "1.3.166",
20
+ "@trackunit/react-date-and-time-components": "1.3.168",
21
21
  "@trackunit/shared-utils": "1.5.121",
22
- "@trackunit/react-form-components": "1.3.165",
22
+ "@trackunit/react-form-components": "1.3.166",
23
23
  "@trackunit/react-core-contexts-api": "1.4.127",
24
24
  "@trackunit/geo-json-utils": "1.3.121",
25
- "@trackunit/i18n-library-translation": "1.3.136",
25
+ "@trackunit/i18n-library-translation": "1.3.137",
26
26
  "@trackunit/css-class-variance-utilities": "1.3.121",
27
- "@trackunit/react-components": "1.4.145",
27
+ "@trackunit/react-components": "1.4.146",
28
28
  "@trackunit/react-test-setup": "1.0.11"
29
29
  },
30
30
  "module": "./index.esm.js",
@@ -26,9 +26,13 @@ interface FilterBarProps<TFilterBarDefinition extends FilterBarDefinition> {
26
26
  * The title of the filter bar default is "Filters" (translated)
27
27
  */
28
28
  title?: string;
29
+ /**
30
+ * If true, the filters marked as showDirectly will not be shown directly in the filter bar
31
+ */
32
+ allowShowFiltersDirectly?: boolean;
29
33
  }
30
34
  /**
31
35
  * The FilterBar component serves as a wrapper for managing filters.
32
36
  */
33
- export declare const FilterBar: <TFilterBarDefinition extends FilterBarDefinition>({ hiddenFilters, className, filterBarDefinition, filterBarConfig, compact, title, }: FilterBarProps<TFilterBarDefinition>) => import("react/jsx-runtime").JSX.Element;
37
+ export declare const FilterBar: <TFilterBarDefinition extends FilterBarDefinition>({ hiddenFilters, className, filterBarDefinition, filterBarConfig, compact, title, allowShowFiltersDirectly, }: FilterBarProps<TFilterBarDefinition>) => import("react/jsx-runtime").JSX.Element;
34
38
  export {};
@@ -0,0 +1,7 @@
1
+ import { FilterBarConfig, FilterBarDefinition, FilterMapActions, FilterMapGetter } from "../types/FilterTypes";
2
+ /**
3
+ * Tooltip label for the filter button
4
+ */
5
+ export declare const FilterButtonTooltipLabel: <TFilterBarDefinition extends FilterBarDefinition>({ filterBarConfig, }: {
6
+ filterBarConfig: FilterBarConfig<TFilterBarDefinition> & FilterMapActions & FilterMapGetter;
7
+ }) => string | import("react/jsx-runtime").JSX.Element | null;
@@ -1,7 +1,11 @@
1
- import { CommonProps } from "@trackunit/react-components";
1
+ import { ButtonProps, CommonProps } from "@trackunit/react-components";
2
2
  import { ReactElement } from "react";
3
3
  import { FilterBarConfig, FilterBarDefinition, FilterMapActions, FilterMapGetter } from "../types/FilterTypes";
4
4
  interface FiltersMenuProps<TFilterBarDefinition extends FilterBarDefinition> extends CommonProps {
5
+ /**
6
+ * If true, the filters marked as showDirectly will not be shown directly in the filter bar
7
+ */
8
+ allowShowFiltersDirectly?: boolean;
5
9
  /**
6
10
  * Configuration for the filter bar.
7
11
  */
@@ -9,7 +13,7 @@ interface FiltersMenuProps<TFilterBarDefinition extends FilterBarDefinition> ext
9
13
  /**
10
14
  * The definition of the filter bar, specifying its structure and filters.
11
15
  */
12
- filterBarDefinition: FilterBarDefinition;
16
+ filterBarDefinition: TFilterBarDefinition;
13
17
  /**
14
18
  * If you want some of the filters to be hidden, but still programmatically enabled
15
19
  */
@@ -18,10 +22,18 @@ interface FiltersMenuProps<TFilterBarDefinition extends FilterBarDefinition> ext
18
22
  * If true, the starred filters will be displayed in a compact mode
19
23
  */
20
24
  compact?: boolean;
25
+ /**
26
+ * If true, the applied filters count will be shown in the filter button, only visible when compact is true
27
+ */
28
+ showAppliedFiltersCount?: boolean;
21
29
  /**
22
30
  * The title of the filter bar default is "Filters" (translated)
23
31
  */
24
32
  title?: string;
33
+ /**
34
+ * The icon props for the filter button to override the icon look and feel.
35
+ */
36
+ buttonProps?: ButtonProps;
25
37
  }
26
38
  /**
27
39
  * FilterMenu is a React component that displays a list of filters in a popover menu based on the provided filter bar configuration.
@@ -29,5 +41,5 @@ interface FiltersMenuProps<TFilterBarDefinition extends FilterBarDefinition> ext
29
41
  * @template TFilterBarDefinition - The type representing the filter bar definition.
30
42
  * @returns {ReactElement} - Returns the FilterMenu component.
31
43
  */
32
- export declare const FiltersMenu: <TFilterBarDefinition extends FilterBarDefinition>({ filterBarDefinition, filterBarConfig, hiddenFilters, compact, title, dataTestId, className, }: FiltersMenuProps<TFilterBarDefinition>) => ReactElement;
44
+ export declare const FiltersMenu: <TFilterBarDefinition extends FilterBarDefinition>({ filterBarDefinition, filterBarConfig, hiddenFilters, compact, title, dataTestId, className, showAppliedFiltersCount, buttonProps, allowShowFiltersDirectly, }: FiltersMenuProps<TFilterBarDefinition>) => ReactElement;
33
45
  export {};
@@ -0,0 +1,17 @@
1
+ import { GroupOfFilters } from "../hooks/useGroupFilters";
2
+ import { FilterBarConfig, FilterBarDefinition, FilterDefinition, FilterMapActions, FilterMapGetter } from "../types/FilterTypes";
3
+ /**
4
+ *
5
+ */
6
+ export declare const FiltersMenuContent: ({ filterBarConfig, setShowCustomFilters, setSearchText, searchText, searchResultsGrouped, filtersToShowGrouped, removeCustomFieldsGroup, hasCustomFields, appliedCustomFields, showCustomFilters, }: {
7
+ filterBarConfig: FilterBarConfig<FilterBarDefinition> & FilterMapActions & FilterMapGetter;
8
+ setShowCustomFilters: (show: boolean) => void;
9
+ setSearchText: (text: string) => void;
10
+ searchText: string;
11
+ searchResultsGrouped: GroupOfFilters[];
12
+ filtersToShowGrouped: GroupOfFilters[];
13
+ removeCustomFieldsGroup: (groupOfFilters: GroupOfFilters[]) => GroupOfFilters[];
14
+ hasCustomFields: boolean;
15
+ appliedCustomFields: FilterDefinition[];
16
+ showCustomFilters: boolean;
17
+ }) => import("react/jsx-runtime").JSX.Element;
@@ -4,10 +4,12 @@ export * from "./DefaultFilterTypes";
4
4
  export * from "./DefaultMinMaxFilter";
5
5
  export * from "./DefaultRadioFilter";
6
6
  export * from "./DynamicFilterList";
7
+ export * from "./FilterButtonTooltipLabel";
7
8
  export * from "./FilterComponent";
8
9
  export * from "./FilterHeader";
9
10
  export * from "./FilterResults";
10
11
  export * from "./FiltersMenu";
12
+ export * from "./FiltersMenuContent";
11
13
  export * from "./FiltersRenderer";
12
14
  export * from "./FilterTableComponent";
13
15
  export * from "./GroupedFiltersList";
@@ -0,0 +1,25 @@
1
+ import { FilterBarConfig, FilterBarDefinition, FilterMapActions, FilterMapGetter } from "../types/FilterTypes";
2
+ import { GroupOfFilters } from "./useGroupFilters";
3
+ type UseFiltersMenuProps<TFilterBarDefinition extends FilterBarDefinition> = {
4
+ filterBarDefinition: TFilterBarDefinition;
5
+ filterBarConfig: FilterBarConfig<TFilterBarDefinition> & FilterMapActions & FilterMapGetter;
6
+ hiddenFilters?: string[];
7
+ allowShowFiltersDirectly?: boolean;
8
+ };
9
+ /**
10
+ * This hook is used to manage the filters menu.
11
+ * It returns the filters that should be shown in the menu, the filters that should be shown directly in the filter bar,
12
+ * and the filters that should be shown in the search results.
13
+ */
14
+ export declare const useFiltersMenu: <TFilterBarDefinition extends FilterBarDefinition>({ filterBarDefinition, filterBarConfig, hiddenFilters, allowShowFiltersDirectly, }: UseFiltersMenuProps<TFilterBarDefinition>) => {
15
+ appliedFilters: import("../types/FilterTypes").FilterDefinition[];
16
+ hasCustomFields: boolean;
17
+ showDirectlyFilters: import("../types/FilterTypes").FilterDefinition[];
18
+ appliedCustomFields: import("../types/FilterTypes").FilterDefinition[];
19
+ searchText: string;
20
+ setSearchText: import("react").Dispatch<string>;
21
+ filtersToShowGrouped: GroupOfFilters[];
22
+ searchResultsGrouped: GroupOfFilters[];
23
+ removeCustomFieldsGroup: (groupOfFilters: GroupOfFilters[]) => GroupOfFilters[];
24
+ };
25
+ export {};
@@ -3,6 +3,7 @@ export * from "./FilterBar";
3
3
  export * from "./hooks/mockFilterBar";
4
4
  export * from "./hooks/useFilterBar";
5
5
  export * from "./hooks/useFilterBarAsync";
6
+ export * from "./hooks/useFiltersMenu";
6
7
  export * from "./hooks/useSearchParamAsFilter";
7
8
  export * from "./types/FilterTypes";
8
9
  export * from "./utils/FilterEvents";