@databiosphere/findable-ui 2.1.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/lib/components/Filter/components/Filters/filters.d.ts +1 -1
  2. package/lib/components/Filter/components/Filters/filters.styles.js +1 -1
  3. package/lib/components/Filter/components/VariableSizeListItem/variableSizeListItem.js +2 -1
  4. package/lib/config/entities.d.ts +7 -1
  5. package/lib/providers/exploreState/constants.d.ts +3 -3
  6. package/lib/providers/exploreState/constants.js +3 -26
  7. package/lib/providers/exploreState/entities.d.ts +10 -3
  8. package/lib/providers/exploreState/initializer/constants.js +2 -0
  9. package/lib/providers/exploreState/initializer/utils.js +63 -4
  10. package/lib/providers/exploreState/payloads/entities.d.ts +8 -0
  11. package/lib/providers/exploreState/utils.d.ts +42 -10
  12. package/lib/providers/exploreState/utils.js +75 -13
  13. package/lib/providers/exploreState.d.ts +12 -4
  14. package/lib/providers/exploreState.js +30 -4
  15. package/lib/views/ExploreView/exploreView.js +14 -8
  16. package/package.json +1 -1
  17. package/src/components/Filter/components/Filters/filters.styles.ts +1 -1
  18. package/src/components/Filter/components/Filters/filters.tsx +1 -1
  19. package/src/components/Filter/components/VariableSizeListItem/variableSizeListItem.tsx +6 -3
  20. package/src/config/entities.ts +11 -1
  21. package/src/providers/exploreState/constants.ts +3 -0
  22. package/src/providers/exploreState/entities.ts +20 -2
  23. package/src/providers/exploreState/initializer/constants.ts +2 -0
  24. package/src/providers/exploreState/initializer/utils.ts +73 -2
  25. package/src/providers/exploreState/payloads/entities.ts +9 -0
  26. package/src/providers/exploreState/utils.ts +104 -20
  27. package/src/providers/exploreState.tsx +67 -10
  28. package/src/views/ExploreView/exploreView.tsx +16 -8
@@ -1,6 +1,6 @@
1
1
  /// <reference types="react" />
2
- import { TrackFilterOpenedFunction } from "config/entities";
3
2
  import { SelectCategoryView } from "../../../../common/entities";
3
+ import { TrackFilterOpenedFunction } from "../../../../config/entities";
4
4
  import { OnFilterFn } from "../../../../hooks/useCategoryFilter";
5
5
  export interface CategoryFilter {
6
6
  categoryViews: SelectCategoryView[];
@@ -10,7 +10,7 @@ const breakpoints_1 = require("../../../../styles/common/mixins/breakpoints");
10
10
  const colors_1 = require("../../../../styles/common/mixins/colors");
11
11
  const fonts_1 = require("../../../../styles/common/mixins/fonts");
12
12
  exports.Filters = (0, styled_1.default)("div", {
13
- shouldForwardProp: (prop) => prop !== "height",
13
+ shouldForwardProp: (prop) => prop !== "height" && prop !== "isBaseStyle",
14
14
  }) `
15
15
  ${(props) => (props.isBaseStyle ? (0, fonts_1.textBody500)(props) : (0, fonts_1.textBody400)(props))};
16
16
  color: ${(props) => (props.isBaseStyle ? (0, colors_1.inkMain)(props) : (0, colors_1.inkLight)(props))};
@@ -25,6 +25,7 @@ var __importStar = (this && this.__importStar) || function (mod) {
25
25
  Object.defineProperty(exports, "__esModule", { value: true });
26
26
  const material_1 = require("@mui/material");
27
27
  const react_1 = __importStar(require("react"));
28
+ const constants_1 = require("../../../../providers/exploreState/constants");
28
29
  const typography_1 = require("../../../../theme/common/typography");
29
30
  const checkedIcon_1 = require("../../../common/CustomIcon/components/CheckedIcon/checkedIcon");
30
31
  const uncheckedIcon_1 = require("../../../common/CustomIcon/components/UncheckedIcon/uncheckedIcon");
@@ -43,6 +44,6 @@ function VariableSizeListItem({ categoryKey, categorySection, matchedItem, onFil
43
44
  };
44
45
  return (react_1.default.createElement(material_1.ListItemButton, { ref: listItemRef, onClick: handleItemClicked, selected: selected, style: style },
45
46
  react_1.default.createElement(material_1.Checkbox, { checked: selected, checkedIcon: react_1.default.createElement(checkedIcon_1.CheckedIcon, null), icon: react_1.default.createElement(uncheckedIcon_1.UncheckedIcon, null) }),
46
- react_1.default.createElement(material_1.ListItemText, { disableTypography: true, primary: react_1.default.createElement(highlightedLabel_1.HighlightedLabel, { label: label, ranges: labelRanges }), secondary: react_1.default.createElement(material_1.Typography, { color: "ink.light", variant: typography_1.TEXT_BODY_SMALL_400 }, count) })));
47
+ react_1.default.createElement(material_1.ListItemText, { disableTypography: true, primary: react_1.default.createElement(highlightedLabel_1.HighlightedLabel, { label: label, ranges: labelRanges }), secondary: categoryKey !== constants_1.SELECT_CATEGORY_KEY.SAVED_FILTERS && (react_1.default.createElement(material_1.Typography, { color: "ink.light", variant: typography_1.TEXT_BODY_SMALL_400 }, count)) })));
47
48
  }
48
49
  exports.default = VariableSizeListItem;
@@ -1,7 +1,7 @@
1
1
  import { TabProps as MTabProps, Theme, ThemeOptions } from "@mui/material";
2
2
  import { ColumnSort } from "@tanstack/react-table";
3
3
  import { JSXElementConstructor, ReactNode } from "react";
4
- import { CategoryKey, SelectedFilterValue } from "../common/entities";
4
+ import { CategoryKey, SelectedFilter, SelectedFilterValue } from "../common/entities";
5
5
  import { HeroTitle } from "../components/common/Title/title";
6
6
  import { FooterProps } from "../components/Layout/components/Footer/footer";
7
7
  import { HeaderProps } from "../components/Layout/components/Header/header";
@@ -51,6 +51,7 @@ export interface BackPageTabConfig extends TabConfig {
51
51
  export interface CategoryGroupConfig {
52
52
  categoryGroups: CategoryGroup[];
53
53
  key: string;
54
+ savedFilters?: SavedFilter[];
54
55
  }
55
56
  /**
56
57
  * Model of grouped configured categories in site config.
@@ -244,6 +245,11 @@ export interface Override {
244
245
  * Related search function.
245
246
  */
246
247
  declare type RelatedSearchFunction = (searchKey: CategoryKey | undefined, resultKey: CategoryKey | undefined, selectedCategoryValues: SelectedFilterValue | undefined) => Promise<RelatedSearchResult | undefined>;
248
+ export interface SavedFilter {
249
+ filters: SelectedFilter[];
250
+ sort?: ColumnSort;
251
+ title: string;
252
+ }
247
253
  /**
248
254
  * Filter applied tracking payload
249
255
  */
@@ -1,3 +1,3 @@
1
- import { ExploreState, PaginationState } from "../exploreState";
2
- export declare const DEFAULT_PAGINATION_STATE: PaginationState;
3
- export declare const INITIAL_STATE: ExploreState;
1
+ export declare const SELECT_CATEGORY_KEY: {
2
+ SAVED_FILTERS: string;
3
+ };
@@ -1,29 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.INITIAL_STATE = exports.DEFAULT_PAGINATION_STATE = void 0;
4
- // Template constants
5
- exports.DEFAULT_PAGINATION_STATE = {
6
- currentPage: 1,
7
- index: null,
8
- nextIndex: null,
9
- pageSize: 25,
10
- pages: 1,
11
- previousIndex: null,
12
- rows: 0,
13
- };
14
- // Initial state
15
- exports.INITIAL_STATE = {
16
- catalogState: undefined,
17
- categoryViews: [],
18
- entityPageState: {},
19
- featureFlagState: undefined,
20
- filterCount: 0,
21
- filterState: [],
22
- isRelatedView: false,
23
- listItems: [],
24
- listView: undefined,
25
- loading: true,
26
- paginationState: exports.DEFAULT_PAGINATION_STATE,
27
- relatedListItems: undefined,
28
- tabValue: "",
3
+ exports.SELECT_CATEGORY_KEY = void 0;
4
+ exports.SELECT_CATEGORY_KEY = {
5
+ SAVED_FILTERS: "savedFilters",
29
6
  };
@@ -1,6 +1,6 @@
1
1
  import { ColumnSort } from "@tanstack/react-table";
2
- import { SelectCategory, SelectedFilter } from "../../common/entities";
3
- import { CategoryConfig, CategoryGroup, CategoryGroupConfig, EntityPath } from "../../config/entities";
2
+ import { CategoryValueKey, SelectCategory, SelectCategoryView, SelectedFilter } from "../../common/entities";
3
+ import { CategoryConfig, CategoryGroup, CategoryGroupConfig, EntityPath, SavedFilter } from "../../config/entities";
4
4
  export interface EntityPageState {
5
5
  categoryGroupConfigKey: CategoryGroupConfigKey;
6
6
  columnsVisibility: Record<string, boolean>;
@@ -12,8 +12,15 @@ export interface EntityPageStateMapper {
12
12
  export interface EntityState {
13
13
  categoryConfigs?: CategoryConfig[];
14
14
  categoryGroups?: CategoryGroup[];
15
- categoryViews: SelectCategory[];
15
+ categoryViews: SelectCategoryView[];
16
16
  filterState: SelectedFilter[];
17
+ savedFilterByCategoryValueKey?: SavedFilterByCategoryValueKey;
18
+ savedFilterState: SelectedFilter[];
19
+ savedSelectCategories: SelectCategory[];
17
20
  }
18
21
  export declare type EntityStateByCategoryGroupConfigKey = Map<CategoryGroupConfigKey, EntityState>;
19
22
  export declare type CategoryGroupConfigKey = CategoryGroupConfig["key"];
23
+ export interface EntityStateSavedFilter extends Omit<SavedFilter, "sort"> {
24
+ sorting?: ColumnSort[];
25
+ }
26
+ export declare type SavedFilterByCategoryValueKey = Map<CategoryValueKey, EntityStateSavedFilter>;
@@ -4,6 +4,8 @@ exports.INITIAL_STATE = exports.DEFAULT_PAGINATION_STATE = exports.DEFAULT_ENTIT
4
4
  exports.DEFAULT_ENTITY_STATE = {
5
5
  categoryViews: [],
6
6
  filterState: [],
7
+ savedFilterState: [],
8
+ savedSelectCategories: [],
7
9
  };
8
10
  exports.DEFAULT_PAGINATION_STATE = {
9
11
  currentPage: 1,
@@ -3,8 +3,63 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.initReducerArguments = void 0;
4
4
  const utils_1 = require("../../../components/Table/common/utils");
5
5
  const utils_2 = require("../../../config/utils");
6
+ const constants_1 = require("../constants");
6
7
  const utils_3 = require("../utils");
7
- const constants_1 = require("./constants");
8
+ const constants_2 = require("./constants");
9
+ /**
10
+ * Builds category groups from the given category group config (adds the saved filters category to the category groups).
11
+ * @param categoryGroupConfig - Category group config.
12
+ * @returns category groups with saved filters category.
13
+ */
14
+ function buildCategoryGroups(categoryGroupConfig) {
15
+ const { categoryGroups, savedFilters } = categoryGroupConfig;
16
+ if (!savedFilters)
17
+ return categoryGroups;
18
+ const clonedCategoryGroups = [...categoryGroups];
19
+ const savedFiltersCategoryGroup = {
20
+ categoryConfigs: [
21
+ { key: constants_1.SELECT_CATEGORY_KEY.SAVED_FILTERS, label: "Saved Filters" },
22
+ ],
23
+ };
24
+ clonedCategoryGroups.unshift(savedFiltersCategoryGroup);
25
+ return clonedCategoryGroups;
26
+ }
27
+ /**
28
+ * Returns the saved filters as select categories.
29
+ * @param savedFilters - Saved filters.
30
+ * @returns select categories.
31
+ */
32
+ function buildSavedSelectCategories(savedFilters) {
33
+ if (!savedFilters)
34
+ return [];
35
+ return [
36
+ {
37
+ key: constants_1.SELECT_CATEGORY_KEY.SAVED_FILTERS,
38
+ label: "",
39
+ values: savedFilters.map(({ title }) => ({
40
+ count: 1,
41
+ key: title,
42
+ label: title,
43
+ selected: false, // Selected state updated in filter hook.
44
+ })),
45
+ },
46
+ ];
47
+ }
48
+ /**
49
+ * Builds saved filter by category value key.
50
+ * @param savedFilters - Saved filters.
51
+ * @returns saved filter by category value key.
52
+ */
53
+ function buildSavedFilterByCategoryValueKey(savedFilters) {
54
+ if (!savedFilters)
55
+ return;
56
+ const savedFilterByCategoryValueKey = new Map();
57
+ for (const { filters, sort, title } of savedFilters) {
58
+ const sorting = sort ? [sort] : undefined;
59
+ savedFilterByCategoryValueKey.set(title, { filters, sorting, title });
60
+ }
61
+ return savedFilterByCategoryValueKey;
62
+ }
8
63
  /**
9
64
  * Returns entity related configured category group config where entity config takes precedence over site config.
10
65
  * @param siteConfig - Site config.
@@ -71,10 +126,14 @@ function initEntityStateByCategoryGroupConfigKey(config, categoryGroupConfigKey,
71
126
  const categoryGroupConfig = getEntityCategoryGroupConfig(config, entity);
72
127
  if (!categoryGroupConfig)
73
128
  continue;
74
- const { categoryGroups, key } = categoryGroupConfig;
129
+ const { key, savedFilters } = categoryGroupConfig;
75
130
  if (entityStateByCategoryGroupConfigKey.has(key))
76
131
  continue;
77
- entityStateByCategoryGroupConfigKey.set(key, Object.assign(Object.assign({}, constants_1.DEFAULT_ENTITY_STATE), { categoryConfigs: flattenCategoryGroups(categoryGroups), categoryGroups, filterState: key === categoryGroupConfigKey ? filterState : [] }));
132
+ const categoryGroups = buildCategoryGroups(categoryGroupConfig);
133
+ const savedSelectCategories = buildSavedSelectCategories(savedFilters);
134
+ const savedFilterByCategoryValueKey = buildSavedFilterByCategoryValueKey(savedFilters);
135
+ entityStateByCategoryGroupConfigKey.set(key, Object.assign(Object.assign({}, constants_2.DEFAULT_ENTITY_STATE), { categoryConfigs: flattenCategoryGroups(categoryGroups), categoryGroups, filterState: key === categoryGroupConfigKey ? filterState : [], savedFilterByCategoryValueKey,
136
+ savedSelectCategories }));
78
137
  }
79
138
  return entityStateByCategoryGroupConfigKey;
80
139
  }
@@ -109,7 +168,7 @@ function initReducerArguments(config, entityListType, decodedFilterParam, decode
109
168
  const categoryGroupConfigKey = (0, utils_3.getEntityCategoryGroupConfigKey)(entityListType, entityPageState);
110
169
  const entityStateByCategoryGroupConfigKey = initEntityStateByCategoryGroupConfigKey(config, categoryGroupConfigKey, filterState);
111
170
  const categoryGroups = initCategoryGroups(entityStateByCategoryGroupConfigKey, categoryGroupConfigKey);
112
- return Object.assign(Object.assign({}, constants_1.INITIAL_STATE), { catalogState: decodedCatalogParam, categoryGroups,
171
+ return Object.assign(Object.assign({}, constants_2.INITIAL_STATE), { catalogState: decodedCatalogParam, categoryGroups,
113
172
  entityPageState,
114
173
  entityStateByCategoryGroupConfigKey, featureFlagState: decodedFeatureFlagParam, filterCount: (0, utils_3.getFilterCount)(filterState), filterState, tabValue: entityListType });
115
174
  }
@@ -1,6 +1,14 @@
1
1
  import { ColumnSort } from "@tanstack/react-table";
2
2
  import { CategoryKey, CategoryValueKey, PaginationDirectionType, SelectCategory } from "../../../common/entities";
3
3
  import { ENTITY_VIEW, ListItems, PaginationResponse, RelatedListItems } from "../../exploreState";
4
+ /**
5
+ * Apply saved filter payload.
6
+ */
7
+ export interface ApplySavedFilterPayload {
8
+ categoryKey: CategoryKey;
9
+ selected: boolean;
10
+ selectedValue: CategoryValueKey;
11
+ }
4
12
  /**
5
13
  * Process explore response payload.
6
14
  */
@@ -1,7 +1,23 @@
1
- import { SelectedFilter } from "../../common/entities";
2
- import { CategoryConfig } from "../../config/entities";
1
+ import { ColumnSort } from "@tanstack/react-table";
2
+ import { CategoryKey, CategoryValueKey, SelectedFilter } from "../../common/entities";
3
3
  import { ExploreState, PaginationState } from "../exploreState";
4
- import { CategoryGroupConfigKey, EntityPageState, EntityPageStateMapper, EntityState } from "./entities";
4
+ import { CategoryGroupConfigKey, EntityPageState, EntityPageStateMapper, EntityState, EntityStateSavedFilter } from "./entities";
5
+ /**
6
+ * Returns the entity state saved filter state for the given category key.
7
+ * @param categoryKey - Category key.
8
+ * @param selectedValue - Key of category value that has been de/selected.
9
+ * @param selected - True if value is selected, false if de-selected.
10
+ * @returns entity state saved filter state.
11
+ */
12
+ export declare function buildEntityStateSavedFilterState(categoryKey: CategoryKey, selectedValue: CategoryValueKey, selected: boolean): SelectedFilter[];
13
+ /**
14
+ * Build new set of selected filters on de/select of a "saved filter" filter.
15
+ * @param state - Explore state.
16
+ * @param selectedValue - Key of category value that has been de/selected.
17
+ * @param selected - True if value is selected, false if de-selected.
18
+ * @returns new selected filters.
19
+ */
20
+ export declare function buildNextSavedFilterState(state: ExploreState, selectedValue: CategoryValueKey, selected: boolean): SelectedFilter[];
5
21
  /**
6
22
  * Returns the category group config key for the current entity.
7
23
  * @param entityPath - Entity path.
@@ -10,18 +26,27 @@ import { CategoryGroupConfigKey, EntityPageState, EntityPageStateMapper, EntityS
10
26
  */
11
27
  export declare function getEntityCategoryGroupConfigKey(entityPath: string, entityPageState: EntityPageStateMapper): CategoryGroupConfigKey;
12
28
  /**
13
- * Returns the category configs for the current entity.
29
+ * Returns the entity state for the current entity.
14
30
  * @param state - Explore state.
15
- * @returns category configs.
31
+ * @param categoryGroupConfigKey - Category group config key.
32
+ * @returns entity state.
16
33
  */
17
- export declare function getEntityCategoryConfigs(state: ExploreState): CategoryConfig[] | undefined;
34
+ export declare function getEntityState(state: ExploreState, categoryGroupConfigKey?: string): EntityState;
18
35
  /**
19
- * Returns the entity state for the given category group config key.
20
- * @param categoryGroupConfigKey - Category group config key.
36
+ * Returns the saved filter/sorting for the given category key.
21
37
  * @param state - Explore state.
22
- * @returns entity state.
38
+ * @param categoryValueKey - Category key.
39
+ * @returns saved filter/sorting for the category key.
40
+ */
41
+ export declare function getEntityStateSavedFilter(state: ExploreState, categoryValueKey: CategoryValueKey): EntityStateSavedFilter | undefined;
42
+ /**
43
+ * Returns entity state "saved filter" sorting for the given category value key.
44
+ * @param state - Explore state.
45
+ * @param selectedValue - Key of category value that has been de/selected.
46
+ * @param selected - True if value is selected, false if de-selected.
47
+ * @returns sorting.
23
48
  */
24
- export declare function getEntityState(categoryGroupConfigKey: CategoryGroupConfigKey, state: ExploreState): EntityState;
49
+ export declare function getEntityStateSavedSorting(state: ExploreState, selectedValue: CategoryValueKey, selected: boolean): ColumnSort[] | undefined;
25
50
  /**
26
51
  * Returns the filter count.
27
52
  * @param filterState - Filter state.
@@ -43,6 +68,13 @@ export declare function resetPage(paginationState: PaginationState): PaginationS
43
68
  */
44
69
  export declare function updateEntityPageState(entityPath: string, // entityListType.
45
70
  entityPageState: EntityPageStateMapper, nextEntityPageState: Partial<EntityPageState>): EntityPageStateMapper;
71
+ /**
72
+ * Updates entity page state sorting for all entities with the same category group config key.
73
+ * @param state - Explore state.
74
+ * @param sorting - Sorting.
75
+ * @returns entity page state.
76
+ */
77
+ export declare function updateEntityPageStateSorting(state: ExploreState, sorting?: EntityPageState["sorting"]): EntityPageStateMapper;
46
78
  /**
47
79
  * Updates entity state by category group config key.
48
80
  * @param state - Explore state.
@@ -1,7 +1,34 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.updateEntityStateByCategoryGroupConfigKey = exports.updateEntityPageState = exports.resetPage = exports.getFilterCount = exports.getEntityState = exports.getEntityCategoryConfigs = exports.getEntityCategoryGroupConfigKey = void 0;
3
+ exports.updateEntityStateByCategoryGroupConfigKey = exports.updateEntityPageStateSorting = exports.updateEntityPageState = exports.resetPage = exports.getFilterCount = exports.getEntityStateSavedSorting = exports.getEntityStateSavedFilter = exports.getEntityState = exports.getEntityCategoryGroupConfigKey = exports.buildNextSavedFilterState = exports.buildEntityStateSavedFilterState = void 0;
4
4
  const constants_1 = require("./initializer/constants");
5
+ /**
6
+ * Returns the entity state saved filter state for the given category key.
7
+ * @param categoryKey - Category key.
8
+ * @param selectedValue - Key of category value that has been de/selected.
9
+ * @param selected - True if value is selected, false if de-selected.
10
+ * @returns entity state saved filter state.
11
+ */
12
+ function buildEntityStateSavedFilterState(categoryKey, selectedValue, selected) {
13
+ if (!selected)
14
+ return [];
15
+ return [{ categoryKey, value: [selectedValue] }];
16
+ }
17
+ exports.buildEntityStateSavedFilterState = buildEntityStateSavedFilterState;
18
+ /**
19
+ * Build new set of selected filters on de/select of a "saved filter" filter.
20
+ * @param state - Explore state.
21
+ * @param selectedValue - Key of category value that has been de/selected.
22
+ * @param selected - True if value is selected, false if de-selected.
23
+ * @returns new selected filters.
24
+ */
25
+ function buildNextSavedFilterState(state, selectedValue, selected) {
26
+ if (!selected)
27
+ return []; // Clears all filters on de-select of saved filter.
28
+ const savedFilter = getEntityStateSavedFilter(state, selectedValue);
29
+ return (savedFilter === null || savedFilter === void 0 ? void 0 : savedFilter.filters) || [];
30
+ }
31
+ exports.buildNextSavedFilterState = buildNextSavedFilterState;
5
32
  /**
6
33
  * Returns the category group config key for the current entity.
7
34
  * @param entityPath - Entity path.
@@ -13,25 +40,42 @@ function getEntityCategoryGroupConfigKey(entityPath, entityPageState) {
13
40
  }
14
41
  exports.getEntityCategoryGroupConfigKey = getEntityCategoryGroupConfigKey;
15
42
  /**
16
- * Returns the category configs for the current entity.
43
+ * Returns the entity state for the current entity.
17
44
  * @param state - Explore state.
18
- * @returns category configs.
19
- */
20
- function getEntityCategoryConfigs(state) {
21
- return getEntityState(getEntityCategoryGroupConfigKey(state.tabValue, state.entityPageState), state).categoryConfigs;
22
- }
23
- exports.getEntityCategoryConfigs = getEntityCategoryConfigs;
24
- /**
25
- * Returns the entity state for the given category group config key.
26
45
  * @param categoryGroupConfigKey - Category group config key.
27
- * @param state - Explore state.
28
46
  * @returns entity state.
29
47
  */
30
- function getEntityState(categoryGroupConfigKey, state) {
48
+ function getEntityState(state, categoryGroupConfigKey = getEntityCategoryGroupConfigKey(state.tabValue, state.entityPageState)) {
31
49
  return (state.entityStateByCategoryGroupConfigKey.get(categoryGroupConfigKey) ||
32
50
  constants_1.DEFAULT_ENTITY_STATE);
33
51
  }
34
52
  exports.getEntityState = getEntityState;
53
+ /**
54
+ * Returns the saved filter/sorting for the given category key.
55
+ * @param state - Explore state.
56
+ * @param categoryValueKey - Category key.
57
+ * @returns saved filter/sorting for the category key.
58
+ */
59
+ function getEntityStateSavedFilter(state, categoryValueKey) {
60
+ var _a;
61
+ const entityState = getEntityState(state);
62
+ return (_a = entityState.savedFilterByCategoryValueKey) === null || _a === void 0 ? void 0 : _a.get(categoryValueKey);
63
+ }
64
+ exports.getEntityStateSavedFilter = getEntityStateSavedFilter;
65
+ /**
66
+ * Returns entity state "saved filter" sorting for the given category value key.
67
+ * @param state - Explore state.
68
+ * @param selectedValue - Key of category value that has been de/selected.
69
+ * @param selected - True if value is selected, false if de-selected.
70
+ * @returns sorting.
71
+ */
72
+ function getEntityStateSavedSorting(state, selectedValue, selected) {
73
+ if (!selected)
74
+ return;
75
+ const savedFilter = getEntityStateSavedFilter(state, selectedValue);
76
+ return savedFilter === null || savedFilter === void 0 ? void 0 : savedFilter.sorting;
77
+ }
78
+ exports.getEntityStateSavedSorting = getEntityStateSavedSorting;
35
79
  /**
36
80
  * Returns the filter count.
37
81
  * @param filterState - Filter state.
@@ -74,6 +118,24 @@ entityPageState, nextEntityPageState) {
74
118
  return Object.assign(Object.assign({}, entityPageState), { [entityPath]: Object.assign(Object.assign({}, entityPageState[entityPath]), nextEntityPageState) });
75
119
  }
76
120
  exports.updateEntityPageState = updateEntityPageState;
121
+ /**
122
+ * Updates entity page state sorting for all entities with the same category group config key.
123
+ * @param state - Explore state.
124
+ * @param sorting - Sorting.
125
+ * @returns entity page state.
126
+ */
127
+ function updateEntityPageStateSorting(state, sorting) {
128
+ if (!sorting)
129
+ return state.entityPageState;
130
+ const categoryGroupConfigKey = getEntityCategoryGroupConfigKey(state.tabValue, state.entityPageState);
131
+ return Object.entries(state.entityPageState).reduce((acc, [entityPath, entityPageState]) => {
132
+ if (entityPageState.categoryGroupConfigKey === categoryGroupConfigKey) {
133
+ return Object.assign(Object.assign({}, acc), { [entityPath]: Object.assign(Object.assign({}, entityPageState), { sorting }) });
134
+ }
135
+ return Object.assign(Object.assign({}, acc), { [entityPath]: entityPageState });
136
+ }, {});
137
+ }
138
+ exports.updateEntityPageStateSorting = updateEntityPageStateSorting;
77
139
  /**
78
140
  * Updates entity state by category group config key.
79
141
  * @param state - Explore state.
@@ -82,7 +144,7 @@ exports.updateEntityPageState = updateEntityPageState;
82
144
  */
83
145
  function updateEntityStateByCategoryGroupConfigKey(state, nextEntityState) {
84
146
  const categoryGroupConfigKey = getEntityCategoryGroupConfigKey(state.tabValue, state.entityPageState);
85
- const entityState = getEntityState(categoryGroupConfigKey, state);
147
+ const entityState = getEntityState(state, categoryGroupConfigKey);
86
148
  if (entityState) {
87
149
  setEntityStateByCategoryGroupConfigKey(categoryGroupConfigKey, state, Object.assign(Object.assign({}, entityState), nextEntityState));
88
150
  }
@@ -1,9 +1,9 @@
1
1
  import React, { Dispatch, ReactNode } from "react";
2
2
  import { AzulSearchIndex } from "../apis/azul/common/entities";
3
- import { SelectCategory, SelectedFilter } from "../common/entities";
3
+ import { SelectCategoryView, SelectedFilter } from "../common/entities";
4
4
  import { CategoryGroup, SiteConfig } from "../config/entities";
5
5
  import { EntityPageStateMapper, EntityStateByCategoryGroupConfigKey } from "./exploreState/entities";
6
- import { PaginateTablePayload, ProcessExploreResponsePayload, ProcessRelatedResponsePayload, ResetExploreResponsePayload, ToggleEntityViewPayload, UpdateColumnVisibilityPayload, UpdateFilterPayload, UpdateSortingPayload } from "./exploreState/payloads/entities";
6
+ import { ApplySavedFilterPayload, PaginateTablePayload, ProcessExploreResponsePayload, ProcessRelatedResponsePayload, ResetExploreResponsePayload, ToggleEntityViewPayload, UpdateColumnVisibilityPayload, UpdateFilterPayload, UpdateSortingPayload } from "./exploreState/payloads/entities";
7
7
  export declare type CatalogState = string | undefined;
8
8
  /**
9
9
  * Entity view.
@@ -25,7 +25,7 @@ export interface ExploreContext {
25
25
  export declare type ExploreState = {
26
26
  catalogState: CatalogState;
27
27
  categoryGroups?: CategoryGroup[];
28
- categoryViews: SelectCategory[];
28
+ categoryViews: SelectCategoryView[];
29
29
  entityPageState: EntityPageStateMapper;
30
30
  entityStateByCategoryGroupConfigKey: EntityStateByCategoryGroupConfigKey;
31
31
  featureFlagState: FeatureFlagState;
@@ -103,6 +103,7 @@ export declare function ExploreStateProvider({ children, entityListType, }: {
103
103
  * Explore action kind.
104
104
  */
105
105
  export declare enum ExploreActionKind {
106
+ ApplySavedFilter = "APPLY_SAVED_FILTER",
106
107
  ClearFilters = "CLEAR_FILTERS",
107
108
  PaginateTable = "PAGINATE_TABLE",
108
109
  ProcessExploreResponse = "PROCESS_EXPLORE_RESPONSE",
@@ -118,7 +119,14 @@ export declare enum ExploreActionKind {
118
119
  /**
119
120
  * Explore action.
120
121
  */
121
- export declare type ExploreAction = ClearFiltersAction | PaginateTableAction | ProcessExploreResponseAction | ProcessRelatedResponseAction | ResetExploreResponseAction | ResetStateAction | SelectEntityTypeAction | ToggleEntityViewAction | UpdateColumnVisibilityAction | UpdateFilterAction | UpdateSortingAction;
122
+ export declare type ExploreAction = ApplySavedFilterAction | ClearFiltersAction | PaginateTableAction | ProcessExploreResponseAction | ProcessRelatedResponseAction | ResetExploreResponseAction | ResetStateAction | SelectEntityTypeAction | ToggleEntityViewAction | UpdateColumnVisibilityAction | UpdateFilterAction | UpdateSortingAction;
123
+ /**
124
+ * Apply saved filter action.
125
+ */
126
+ declare type ApplySavedFilterAction = {
127
+ payload: ApplySavedFilterPayload;
128
+ type: ExploreActionKind.ApplySavedFilter;
129
+ };
122
130
  /**
123
131
  * Clear filters action.
124
132
  */
@@ -93,6 +93,7 @@ exports.ExploreStateProvider = ExploreStateProvider;
93
93
  */
94
94
  var ExploreActionKind;
95
95
  (function (ExploreActionKind) {
96
+ ExploreActionKind["ApplySavedFilter"] = "APPLY_SAVED_FILTER";
96
97
  ExploreActionKind["ClearFilters"] = "CLEAR_FILTERS";
97
98
  ExploreActionKind["PaginateTable"] = "PAGINATE_TABLE";
98
99
  ExploreActionKind["ProcessExploreResponse"] = "PROCESS_EXPLORE_RESPONSE";
@@ -116,13 +117,30 @@ function exploreReducer(state, action, exploreContext) {
116
117
  const { payload, type } = action;
117
118
  const { config, entityList } = exploreContext;
118
119
  switch (type) {
120
+ /**
121
+ * Apply saved filter
122
+ **/
123
+ case ExploreActionKind.ApplySavedFilter: {
124
+ const filterState = (0, utils_2.buildNextSavedFilterState)(state, payload.selectedValue, payload.selected);
125
+ const savedFilterState = (0, utils_2.buildEntityStateSavedFilterState)(payload.categoryKey, payload.selectedValue, payload.selected);
126
+ const savedSorting = (0, utils_2.getEntityStateSavedSorting)(state, payload.selectedValue, payload.selected);
127
+ (0, utils_2.updateEntityStateByCategoryGroupConfigKey)(state, {
128
+ filterState,
129
+ savedFilterState,
130
+ });
131
+ return Object.assign(Object.assign({}, state), { entityPageState: (0, utils_2.updateEntityPageStateSorting)(state, savedSorting), filterCount: (0, utils_2.getFilterCount)(filterState), filterState, paginationState: (0, utils_2.resetPage)(state.paginationState) });
132
+ }
119
133
  /**
120
134
  * Clear all filters
121
135
  **/
122
136
  case ExploreActionKind.ClearFilters: {
123
137
  const filterCount = 0;
124
138
  const filterState = [];
125
- (0, utils_2.updateEntityStateByCategoryGroupConfigKey)(state, { filterState });
139
+ const savedFilterState = [];
140
+ (0, utils_2.updateEntityStateByCategoryGroupConfigKey)(state, {
141
+ filterState,
142
+ savedFilterState,
143
+ });
126
144
  return Object.assign(Object.assign({}, state), { filterCount,
127
145
  filterState, paginationState: (0, utils_2.resetPage)(state.paginationState) });
128
146
  }
@@ -145,8 +163,12 @@ function exploreReducer(state, action, exploreContext) {
145
163
  * Process explore response
146
164
  **/
147
165
  case ExploreActionKind.ProcessExploreResponse: {
166
+ const entityState = (0, utils_2.getEntityState)(state);
148
167
  const nextCategoryViews = payload.selectCategories
149
- ? (0, useCategoryFilter_1.buildCategoryViews)(payload.selectCategories, (0, utils_2.getEntityCategoryConfigs)(state), state.filterState)
168
+ ? (0, useCategoryFilter_1.buildCategoryViews)([
169
+ ...payload.selectCategories,
170
+ ...entityState.savedSelectCategories, // "savedFilter" select categories are built from config at reducer initialization.
171
+ ], entityState.categoryConfigs, [...state.filterState, ...entityState.savedFilterState])
150
172
  : state.categoryViews;
151
173
  (0, utils_2.updateEntityStateByCategoryGroupConfigKey)(state, {
152
174
  categoryViews: nextCategoryViews,
@@ -178,7 +200,7 @@ function exploreReducer(state, action, exploreContext) {
178
200
  if (payload === state.tabValue) {
179
201
  return state;
180
202
  }
181
- const entityState = (0, utils_2.getEntityState)((0, utils_2.getEntityCategoryGroupConfigKey)(payload, state.entityPageState), state);
203
+ const entityState = (0, utils_2.getEntityState)(state, (0, utils_2.getEntityCategoryGroupConfigKey)(payload, state.entityPageState));
182
204
  return Object.assign(Object.assign({}, state), { categoryGroups: entityState.categoryGroups, categoryViews: entityState.categoryViews, filterCount: (0, utils_2.getFilterCount)(entityState.filterState), filterState: entityState.filterState, listItems: [], loading: true, paginationState: (0, utils_2.resetPage)(state.paginationState), tabValue: payload });
183
205
  }
184
206
  /**
@@ -192,7 +214,11 @@ function exploreReducer(state, action, exploreContext) {
192
214
  **/
193
215
  case ExploreActionKind.UpdateFilter: {
194
216
  const filterState = (0, useCategoryFilter_1.buildNextFilterState)(state.filterState, payload.categoryKey, payload.selectedValue, payload.selected);
195
- (0, utils_2.updateEntityStateByCategoryGroupConfigKey)(state, { filterState });
217
+ const savedFilterState = []; // Clear saved filter state.
218
+ (0, utils_2.updateEntityStateByCategoryGroupConfigKey)(state, {
219
+ filterState,
220
+ savedFilterState,
221
+ });
196
222
  return Object.assign(Object.assign({}, state), { filterCount: (0, utils_2.getFilterCount)(filterState), filterState, paginationState: (0, utils_2.resetPage)(state.paginationState) });
197
223
  }
198
224
  /**
@@ -46,6 +46,7 @@ const useEntityListRelatedView_1 = require("../../hooks/useEntityListRelatedView
46
46
  const useExploreState_1 = require("../../hooks/useExploreState");
47
47
  const useSummary_1 = require("../../hooks/useSummary");
48
48
  const exploreState_1 = require("../../providers/exploreState");
49
+ const constants_1 = require("../../providers/exploreState/constants");
49
50
  const breakpoints_1 = require("../../theme/common/breakpoints");
50
51
  /**
51
52
  * Returns tabs to be used as a prop for the Tabs component.
@@ -93,13 +94,16 @@ const ExploreView = (props) => {
93
94
  */
94
95
  const onFilterChange = (fromSearchAll, categoryKey, selectedCategoryValue, selected, categorySection, searchTerm) => {
95
96
  var _a;
97
+ const dispatchType = categoryKey === constants_1.SELECT_CATEGORY_KEY.SAVED_FILTERS
98
+ ? exploreState_1.ExploreActionKind.ApplySavedFilter
99
+ : exploreState_1.ExploreActionKind.UpdateFilter;
96
100
  exploreDispatch({
97
101
  payload: {
98
102
  categoryKey,
99
103
  selected,
100
104
  selectedValue: selectedCategoryValue,
101
105
  },
102
- type: exploreState_1.ExploreActionKind.UpdateFilter,
106
+ type: dispatchType,
103
107
  });
104
108
  (_a = trackingConfig === null || trackingConfig === void 0 ? void 0 : trackingConfig.trackFilterApplied) === null || _a === void 0 ? void 0 : _a.call(trackingConfig, {
105
109
  category: categoryKey,
@@ -166,18 +170,20 @@ function buildCategoryFilters(selectCategoryViews, categoryGroups) {
166
170
  if (!categoryGroups) {
167
171
  return [{ categoryViews: selectCategoryViews }];
168
172
  }
169
- return categoryGroups.map(({ categoryConfigs, label }) => {
173
+ return categoryGroups.reduce((accGroups, { categoryConfigs, label }) => {
170
174
  // Grab the category views for the configured grouped categories.
171
- const categoryViews = categoryConfigs.reduce((acc, { key: categoryKey }) => {
175
+ const categoryViews = categoryConfigs.reduce((accViews, { key: categoryKey }) => {
172
176
  const categoryView = selectCategoryViews.find(({ key }) => key === categoryKey);
173
177
  if (categoryView) {
174
- acc.push(categoryView);
178
+ accViews.push(categoryView);
175
179
  }
176
- return acc;
180
+ return accViews;
177
181
  }, []);
178
- // Return the configured label and category views.
179
- return { categoryViews, label };
180
- });
182
+ if (categoryViews.length > 0) {
183
+ accGroups.push({ categoryViews, label });
184
+ }
185
+ return accGroups;
186
+ }, []);
181
187
  }
182
188
  /**
183
189
  * Optionally renders component config.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@databiosphere/findable-ui",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "",
5
5
  "scripts": {
6
6
  "test": "jest",
@@ -14,7 +14,7 @@ interface Props {
14
14
  }
15
15
 
16
16
  export const Filters = styled("div", {
17
- shouldForwardProp: (prop) => prop !== "height",
17
+ shouldForwardProp: (prop) => prop !== "height" && prop !== "isBaseStyle",
18
18
  })<Props>`
19
19
  ${(props) => (props.isBaseStyle ? textBody500(props) : textBody400(props))};
20
20
  color: ${(props) => (props.isBaseStyle ? inkMain(props) : inkLight(props))};
@@ -1,7 +1,7 @@
1
1
  import { Divider } from "@mui/material";
2
- import { TrackFilterOpenedFunction } from "config/entities";
3
2
  import React, { Fragment, useEffect, useMemo, useRef, useState } from "react";
4
3
  import { CategoryTag, SelectCategoryView } from "../../../../common/entities";
4
+ import { TrackFilterOpenedFunction } from "../../../../config/entities";
5
5
  import {
6
6
  BREAKPOINT_FN_NAME,
7
7
  useBreakpointHelper,
@@ -7,6 +7,7 @@ import {
7
7
  import React, { CSSProperties, useEffect, useRef } from "react";
8
8
  import { CategoryKey } from "../../../../common/entities";
9
9
  import { OnFilterFn } from "../../../../hooks/useCategoryFilter";
10
+ import { SELECT_CATEGORY_KEY } from "../../../../providers/exploreState/constants";
10
11
  import { TEXT_BODY_SMALL_400 } from "../../../../theme/common/typography";
11
12
  import { CheckedIcon } from "../../../common/CustomIcon/components/CheckedIcon/checkedIcon";
12
13
  import { UncheckedIcon } from "../../../common/CustomIcon/components/UncheckedIcon/uncheckedIcon";
@@ -62,9 +63,11 @@ export default function VariableSizeListItem({
62
63
  disableTypography
63
64
  primary={<HighlightedLabel label={label} ranges={labelRanges} />}
64
65
  secondary={
65
- <Typography color="ink.light" variant={TEXT_BODY_SMALL_400}>
66
- {count}
67
- </Typography>
66
+ categoryKey !== SELECT_CATEGORY_KEY.SAVED_FILTERS && (
67
+ <Typography color="ink.light" variant={TEXT_BODY_SMALL_400}>
68
+ {count}
69
+ </Typography>
70
+ )
68
71
  }
69
72
  />
70
73
  </ListItemButton>
@@ -1,7 +1,11 @@
1
1
  import { TabProps as MTabProps, Theme, ThemeOptions } from "@mui/material";
2
2
  import { ColumnSort } from "@tanstack/react-table";
3
3
  import { JSXElementConstructor, ReactNode } from "react";
4
- import { CategoryKey, SelectedFilterValue } from "../common/entities";
4
+ import {
5
+ CategoryKey,
6
+ SelectedFilter,
7
+ SelectedFilterValue,
8
+ } from "../common/entities";
5
9
  import { HeroTitle } from "../components/common/Title/title";
6
10
  import { FooterProps } from "../components/Layout/components/Footer/footer";
7
11
  import { HeaderProps } from "../components/Layout/components/Header/header";
@@ -56,6 +60,7 @@ export interface BackPageTabConfig extends TabConfig {
56
60
  export interface CategoryGroupConfig {
57
61
  categoryGroups: CategoryGroup[];
58
62
  key: string;
63
+ savedFilters?: SavedFilter[];
59
64
  }
60
65
 
61
66
  /**
@@ -302,6 +307,11 @@ type RelatedSearchFunction = (
302
307
  selectedCategoryValues: SelectedFilterValue | undefined
303
308
  ) => Promise<RelatedSearchResult | undefined>;
304
309
 
310
+ export interface SavedFilter {
311
+ filters: SelectedFilter[];
312
+ sort?: ColumnSort;
313
+ title: string;
314
+ }
305
315
  /**
306
316
  * Filter applied tracking payload
307
317
  */
@@ -0,0 +1,3 @@
1
+ export const SELECT_CATEGORY_KEY = {
2
+ SAVED_FILTERS: "savedFilters",
3
+ };
@@ -1,10 +1,16 @@
1
1
  import { ColumnSort } from "@tanstack/react-table";
2
- import { SelectCategory, SelectedFilter } from "../../common/entities";
2
+ import {
3
+ CategoryValueKey,
4
+ SelectCategory,
5
+ SelectCategoryView,
6
+ SelectedFilter,
7
+ } from "../../common/entities";
3
8
  import {
4
9
  CategoryConfig,
5
10
  CategoryGroup,
6
11
  CategoryGroupConfig,
7
12
  EntityPath,
13
+ SavedFilter,
8
14
  } from "../../config/entities";
9
15
 
10
16
  export interface EntityPageState {
@@ -20,8 +26,11 @@ export interface EntityPageStateMapper {
20
26
  export interface EntityState {
21
27
  categoryConfigs?: CategoryConfig[];
22
28
  categoryGroups?: CategoryGroup[];
23
- categoryViews: SelectCategory[];
29
+ categoryViews: SelectCategoryView[];
24
30
  filterState: SelectedFilter[];
31
+ savedFilterByCategoryValueKey?: SavedFilterByCategoryValueKey;
32
+ savedFilterState: SelectedFilter[];
33
+ savedSelectCategories: SelectCategory[];
25
34
  }
26
35
 
27
36
  export type EntityStateByCategoryGroupConfigKey = Map<
@@ -30,3 +39,12 @@ export type EntityStateByCategoryGroupConfigKey = Map<
30
39
  >;
31
40
 
32
41
  export type CategoryGroupConfigKey = CategoryGroupConfig["key"];
42
+
43
+ export interface EntityStateSavedFilter extends Omit<SavedFilter, "sort"> {
44
+ sorting?: ColumnSort[];
45
+ }
46
+
47
+ export type SavedFilterByCategoryValueKey = Map<
48
+ CategoryValueKey,
49
+ EntityStateSavedFilter
50
+ >;
@@ -4,6 +4,8 @@ import { EntityState } from "../entities";
4
4
  export const DEFAULT_ENTITY_STATE: EntityState = {
5
5
  categoryViews: [],
6
6
  filterState: [],
7
+ savedFilterState: [],
8
+ savedSelectCategories: [],
7
9
  };
8
10
 
9
11
  export const DEFAULT_PAGINATION_STATE: PaginationState = {
@@ -1,22 +1,86 @@
1
- import { SelectedFilter } from "../../../common/entities";
1
+ import { SelectCategory, SelectedFilter } from "../../../common/entities";
2
2
  import { getInitialTableColumnVisibility } from "../../../components/Table/common/utils";
3
3
  import {
4
4
  CategoryConfig,
5
5
  CategoryGroup,
6
6
  CategoryGroupConfig,
7
7
  EntityConfig,
8
+ SavedFilter,
8
9
  SiteConfig,
9
10
  } from "../../../config/entities";
10
11
  import { getDefaultSorting } from "../../../config/utils";
11
12
  import { ExploreState } from "../../exploreState";
13
+ import { SELECT_CATEGORY_KEY } from "../constants";
12
14
  import {
13
15
  CategoryGroupConfigKey,
14
16
  EntityPageStateMapper,
15
17
  EntityStateByCategoryGroupConfigKey,
18
+ SavedFilterByCategoryValueKey,
16
19
  } from "../entities";
17
20
  import { getEntityCategoryGroupConfigKey, getFilterCount } from "../utils";
18
21
  import { DEFAULT_ENTITY_STATE, INITIAL_STATE } from "./constants";
19
22
 
23
+ /**
24
+ * Builds category groups from the given category group config (adds the saved filters category to the category groups).
25
+ * @param categoryGroupConfig - Category group config.
26
+ * @returns category groups with saved filters category.
27
+ */
28
+ function buildCategoryGroups(
29
+ categoryGroupConfig: CategoryGroupConfig
30
+ ): CategoryGroup[] {
31
+ const { categoryGroups, savedFilters } = categoryGroupConfig;
32
+ if (!savedFilters) return categoryGroups;
33
+ const clonedCategoryGroups = [...categoryGroups];
34
+ const savedFiltersCategoryGroup: CategoryGroup = {
35
+ categoryConfigs: [
36
+ { key: SELECT_CATEGORY_KEY.SAVED_FILTERS, label: "Saved Filters" },
37
+ ],
38
+ };
39
+ clonedCategoryGroups.unshift(savedFiltersCategoryGroup);
40
+ return clonedCategoryGroups;
41
+ }
42
+
43
+ /**
44
+ * Returns the saved filters as select categories.
45
+ * @param savedFilters - Saved filters.
46
+ * @returns select categories.
47
+ */
48
+ function buildSavedSelectCategories(
49
+ savedFilters?: SavedFilter[]
50
+ ): SelectCategory[] {
51
+ if (!savedFilters) return [];
52
+ return [
53
+ {
54
+ key: SELECT_CATEGORY_KEY.SAVED_FILTERS,
55
+ label: "", // Label is applied in filter hook where it has access to the config.
56
+ values: savedFilters.map(({ title }) => ({
57
+ count: 1,
58
+ key: title,
59
+ label: title,
60
+ selected: false, // Selected state updated in filter hook.
61
+ })),
62
+ },
63
+ ];
64
+ }
65
+
66
+ /**
67
+ * Builds saved filter by category value key.
68
+ * @param savedFilters - Saved filters.
69
+ * @returns saved filter by category value key.
70
+ */
71
+ function buildSavedFilterByCategoryValueKey(
72
+ savedFilters?: SavedFilter[]
73
+ ): SavedFilterByCategoryValueKey | undefined {
74
+ if (!savedFilters) return;
75
+ const savedFilterByCategoryValueKey: SavedFilterByCategoryValueKey =
76
+ new Map();
77
+ for (const { filters, sort, title } of savedFilters) {
78
+ const sorting = sort ? [sort] : undefined;
79
+ savedFilterByCategoryValueKey.set(title, { filters, sorting, title });
80
+ }
81
+ return savedFilterByCategoryValueKey;
82
+ }
83
+
20
84
  /**
21
85
  * Returns entity related configured category group config where entity config takes precedence over site config.
22
86
  * @param siteConfig - Site config.
@@ -109,13 +173,20 @@ function initEntityStateByCategoryGroupConfigKey(
109
173
  for (const entity of config.entities) {
110
174
  const categoryGroupConfig = getEntityCategoryGroupConfig(config, entity);
111
175
  if (!categoryGroupConfig) continue;
112
- const { categoryGroups, key } = categoryGroupConfig;
176
+ const { key, savedFilters } = categoryGroupConfig;
113
177
  if (entityStateByCategoryGroupConfigKey.has(key)) continue;
178
+ const categoryGroups = buildCategoryGroups(categoryGroupConfig);
179
+ const savedSelectCategories: SelectCategory[] =
180
+ buildSavedSelectCategories(savedFilters);
181
+ const savedFilterByCategoryValueKey =
182
+ buildSavedFilterByCategoryValueKey(savedFilters);
114
183
  entityStateByCategoryGroupConfigKey.set(key, {
115
184
  ...DEFAULT_ENTITY_STATE,
116
185
  categoryConfigs: flattenCategoryGroups(categoryGroups),
117
186
  categoryGroups,
118
187
  filterState: key === categoryGroupConfigKey ? filterState : [],
188
+ savedFilterByCategoryValueKey,
189
+ savedSelectCategories,
119
190
  });
120
191
  }
121
192
  return entityStateByCategoryGroupConfigKey;
@@ -12,6 +12,15 @@ import {
12
12
  RelatedListItems,
13
13
  } from "../../exploreState";
14
14
 
15
+ /**
16
+ * Apply saved filter payload.
17
+ */
18
+ export interface ApplySavedFilterPayload {
19
+ categoryKey: CategoryKey;
20
+ selected: boolean;
21
+ selectedValue: CategoryValueKey;
22
+ }
23
+
15
24
  /**
16
25
  * Process explore response payload.
17
26
  */
@@ -1,14 +1,52 @@
1
- import { SelectedFilter } from "../../common/entities";
2
- import { CategoryConfig } from "../../config/entities";
1
+ import { ColumnSort } from "@tanstack/react-table";
2
+ import {
3
+ CategoryKey,
4
+ CategoryValueKey,
5
+ SelectedFilter,
6
+ } from "../../common/entities";
3
7
  import { ExploreState, PaginationState } from "../exploreState";
4
8
  import {
5
9
  CategoryGroupConfigKey,
6
10
  EntityPageState,
7
11
  EntityPageStateMapper,
8
12
  EntityState,
13
+ EntityStateSavedFilter,
9
14
  } from "./entities";
10
15
  import { DEFAULT_ENTITY_STATE } from "./initializer/constants";
11
16
 
17
+ /**
18
+ * Returns the entity state saved filter state for the given category key.
19
+ * @param categoryKey - Category key.
20
+ * @param selectedValue - Key of category value that has been de/selected.
21
+ * @param selected - True if value is selected, false if de-selected.
22
+ * @returns entity state saved filter state.
23
+ */
24
+ export function buildEntityStateSavedFilterState(
25
+ categoryKey: CategoryKey,
26
+ selectedValue: CategoryValueKey,
27
+ selected: boolean
28
+ ): SelectedFilter[] {
29
+ if (!selected) return [];
30
+ return [{ categoryKey, value: [selectedValue] }];
31
+ }
32
+
33
+ /**
34
+ * Build new set of selected filters on de/select of a "saved filter" filter.
35
+ * @param state - Explore state.
36
+ * @param selectedValue - Key of category value that has been de/selected.
37
+ * @param selected - True if value is selected, false if de-selected.
38
+ * @returns new selected filters.
39
+ */
40
+ export function buildNextSavedFilterState(
41
+ state: ExploreState,
42
+ selectedValue: CategoryValueKey,
43
+ selected: boolean
44
+ ): SelectedFilter[] {
45
+ if (!selected) return []; // Clears all filters on de-select of saved filter.
46
+ const savedFilter = getEntityStateSavedFilter(state, selectedValue);
47
+ return savedFilter?.filters || [];
48
+ }
49
+
12
50
  /**
13
51
  * Returns the category group config key for the current entity.
14
52
  * @param entityPath - Entity path.
@@ -23,28 +61,17 @@ export function getEntityCategoryGroupConfigKey(
23
61
  }
24
62
 
25
63
  /**
26
- * Returns the category configs for the current entity.
64
+ * Returns the entity state for the current entity.
27
65
  * @param state - Explore state.
28
- * @returns category configs.
29
- */
30
- export function getEntityCategoryConfigs(
31
- state: ExploreState
32
- ): CategoryConfig[] | undefined {
33
- return getEntityState(
34
- getEntityCategoryGroupConfigKey(state.tabValue, state.entityPageState),
35
- state
36
- ).categoryConfigs;
37
- }
38
-
39
- /**
40
- * Returns the entity state for the given category group config key.
41
66
  * @param categoryGroupConfigKey - Category group config key.
42
- * @param state - Explore state.
43
67
  * @returns entity state.
44
68
  */
45
69
  export function getEntityState(
46
- categoryGroupConfigKey: CategoryGroupConfigKey,
47
- state: ExploreState
70
+ state: ExploreState,
71
+ categoryGroupConfigKey = getEntityCategoryGroupConfigKey(
72
+ state.tabValue,
73
+ state.entityPageState
74
+ )
48
75
  ): EntityState {
49
76
  return (
50
77
  state.entityStateByCategoryGroupConfigKey.get(categoryGroupConfigKey) ||
@@ -52,6 +79,37 @@ export function getEntityState(
52
79
  );
53
80
  }
54
81
 
82
+ /**
83
+ * Returns the saved filter/sorting for the given category key.
84
+ * @param state - Explore state.
85
+ * @param categoryValueKey - Category key.
86
+ * @returns saved filter/sorting for the category key.
87
+ */
88
+ export function getEntityStateSavedFilter(
89
+ state: ExploreState,
90
+ categoryValueKey: CategoryValueKey
91
+ ): EntityStateSavedFilter | undefined {
92
+ const entityState = getEntityState(state);
93
+ return entityState.savedFilterByCategoryValueKey?.get(categoryValueKey);
94
+ }
95
+
96
+ /**
97
+ * Returns entity state "saved filter" sorting for the given category value key.
98
+ * @param state - Explore state.
99
+ * @param selectedValue - Key of category value that has been de/selected.
100
+ * @param selected - True if value is selected, false if de-selected.
101
+ * @returns sorting.
102
+ */
103
+ export function getEntityStateSavedSorting(
104
+ state: ExploreState,
105
+ selectedValue: CategoryValueKey,
106
+ selected: boolean
107
+ ): ColumnSort[] | undefined {
108
+ if (!selected) return;
109
+ const savedFilter = getEntityStateSavedFilter(state, selectedValue);
110
+ return savedFilter?.sorting;
111
+ }
112
+
55
113
  /**
56
114
  * Returns the filter count.
57
115
  * @param filterState - Filter state.
@@ -111,6 +169,32 @@ export function updateEntityPageState(
111
169
  };
112
170
  }
113
171
 
172
+ /**
173
+ * Updates entity page state sorting for all entities with the same category group config key.
174
+ * @param state - Explore state.
175
+ * @param sorting - Sorting.
176
+ * @returns entity page state.
177
+ */
178
+ export function updateEntityPageStateSorting(
179
+ state: ExploreState,
180
+ sorting?: EntityPageState["sorting"]
181
+ ): EntityPageStateMapper {
182
+ if (!sorting) return state.entityPageState;
183
+ const categoryGroupConfigKey = getEntityCategoryGroupConfigKey(
184
+ state.tabValue,
185
+ state.entityPageState
186
+ );
187
+ return Object.entries(state.entityPageState).reduce(
188
+ (acc, [entityPath, entityPageState]) => {
189
+ if (entityPageState.categoryGroupConfigKey === categoryGroupConfigKey) {
190
+ return { ...acc, [entityPath]: { ...entityPageState, sorting } };
191
+ }
192
+ return { ...acc, [entityPath]: entityPageState };
193
+ },
194
+ {} as EntityPageStateMapper
195
+ );
196
+ }
197
+
114
198
  /**
115
199
  * Updates entity state by category group config key.
116
200
  * @param state - Explore state.
@@ -125,7 +209,7 @@ export function updateEntityStateByCategoryGroupConfigKey(
125
209
  state.tabValue,
126
210
  state.entityPageState
127
211
  );
128
- const entityState = getEntityState(categoryGroupConfigKey, state);
212
+ const entityState = getEntityState(state, categoryGroupConfigKey);
129
213
  if (entityState) {
130
214
  setEntityStateByCategoryGroupConfigKey(categoryGroupConfigKey, state, {
131
215
  ...entityState,
@@ -8,7 +8,7 @@ import React, {
8
8
  useState,
9
9
  } from "react";
10
10
  import { AzulSearchIndex } from "../apis/azul/common/entities";
11
- import { SelectCategory, SelectedFilter } from "../common/entities";
11
+ import { SelectCategoryView, SelectedFilter } from "../common/entities";
12
12
  import { CategoryGroup, SiteConfig } from "../config/entities";
13
13
  import { useAuthentication } from "../hooks/useAuthentication/useAuthentication";
14
14
  import {
@@ -27,6 +27,7 @@ import {
27
27
  } from "./exploreState/initializer/constants";
28
28
  import { initReducerArguments } from "./exploreState/initializer/utils";
29
29
  import {
30
+ ApplySavedFilterPayload,
30
31
  PaginateTablePayload,
31
32
  ProcessExploreResponsePayload,
32
33
  ProcessRelatedResponsePayload,
@@ -37,12 +38,15 @@ import {
37
38
  UpdateSortingPayload,
38
39
  } from "./exploreState/payloads/entities";
39
40
  import {
40
- getEntityCategoryConfigs,
41
+ buildEntityStateSavedFilterState,
42
+ buildNextSavedFilterState,
41
43
  getEntityCategoryGroupConfigKey,
42
44
  getEntityState,
45
+ getEntityStateSavedSorting,
43
46
  getFilterCount,
44
47
  resetPage,
45
48
  updateEntityPageState,
49
+ updateEntityPageStateSorting,
46
50
  updateEntityStateByCategoryGroupConfigKey,
47
51
  } from "./exploreState/utils";
48
52
 
@@ -70,7 +74,7 @@ export interface ExploreContext {
70
74
  export type ExploreState = {
71
75
  catalogState: CatalogState;
72
76
  categoryGroups?: CategoryGroup[];
73
- categoryViews: SelectCategory[];
77
+ categoryViews: SelectCategoryView[];
74
78
  entityPageState: EntityPageStateMapper;
75
79
  entityStateByCategoryGroupConfigKey: EntityStateByCategoryGroupConfigKey;
76
80
  featureFlagState: FeatureFlagState;
@@ -218,6 +222,7 @@ export function ExploreStateProvider({
218
222
  * Explore action kind.
219
223
  */
220
224
  export enum ExploreActionKind {
225
+ ApplySavedFilter = "APPLY_SAVED_FILTER",
221
226
  ClearFilters = "CLEAR_FILTERS",
222
227
  PaginateTable = "PAGINATE_TABLE",
223
228
  ProcessExploreResponse = "PROCESS_EXPLORE_RESPONSE",
@@ -235,6 +240,7 @@ export enum ExploreActionKind {
235
240
  * Explore action.
236
241
  */
237
242
  export type ExploreAction =
243
+ | ApplySavedFilterAction
238
244
  | ClearFiltersAction
239
245
  | PaginateTableAction
240
246
  | ProcessExploreResponseAction
@@ -247,6 +253,14 @@ export type ExploreAction =
247
253
  | UpdateFilterAction
248
254
  | UpdateSortingAction;
249
255
 
256
+ /**
257
+ * Apply saved filter action.
258
+ */
259
+ type ApplySavedFilterAction = {
260
+ payload: ApplySavedFilterPayload;
261
+ type: ExploreActionKind.ApplySavedFilter;
262
+ };
263
+
250
264
  /**
251
265
  * Clear filters action.
252
266
  */
@@ -351,13 +365,48 @@ function exploreReducer(
351
365
  const { config, entityList } = exploreContext;
352
366
 
353
367
  switch (type) {
368
+ /**
369
+ * Apply saved filter
370
+ **/
371
+ case ExploreActionKind.ApplySavedFilter: {
372
+ const filterState = buildNextSavedFilterState(
373
+ state,
374
+ payload.selectedValue,
375
+ payload.selected
376
+ );
377
+ const savedFilterState = buildEntityStateSavedFilterState(
378
+ payload.categoryKey,
379
+ payload.selectedValue,
380
+ payload.selected
381
+ );
382
+ const savedSorting = getEntityStateSavedSorting(
383
+ state,
384
+ payload.selectedValue,
385
+ payload.selected
386
+ );
387
+ updateEntityStateByCategoryGroupConfigKey(state, {
388
+ filterState,
389
+ savedFilterState,
390
+ });
391
+ return {
392
+ ...state,
393
+ entityPageState: updateEntityPageStateSorting(state, savedSorting),
394
+ filterCount: getFilterCount(filterState),
395
+ filterState,
396
+ paginationState: resetPage(state.paginationState),
397
+ };
398
+ }
354
399
  /**
355
400
  * Clear all filters
356
401
  **/
357
402
  case ExploreActionKind.ClearFilters: {
358
403
  const filterCount = 0;
359
404
  const filterState: SelectedFilter[] = [];
360
- updateEntityStateByCategoryGroupConfigKey(state, { filterState });
405
+ const savedFilterState: SelectedFilter[] = [];
406
+ updateEntityStateByCategoryGroupConfigKey(state, {
407
+ filterState,
408
+ savedFilterState,
409
+ });
361
410
  return {
362
411
  ...state,
363
412
  filterCount,
@@ -386,11 +435,15 @@ function exploreReducer(
386
435
  * Process explore response
387
436
  **/
388
437
  case ExploreActionKind.ProcessExploreResponse: {
438
+ const entityState = getEntityState(state);
389
439
  const nextCategoryViews = payload.selectCategories
390
440
  ? buildCategoryViews(
391
- payload.selectCategories,
392
- getEntityCategoryConfigs(state),
393
- state.filterState
441
+ [
442
+ ...payload.selectCategories,
443
+ ...entityState.savedSelectCategories, // "savedFilter" select categories are built from config at reducer initialization.
444
+ ],
445
+ entityState.categoryConfigs,
446
+ [...state.filterState, ...entityState.savedFilterState]
394
447
  )
395
448
  : state.categoryViews;
396
449
  updateEntityStateByCategoryGroupConfigKey(state, {
@@ -441,8 +494,8 @@ function exploreReducer(
441
494
  return state;
442
495
  }
443
496
  const entityState = getEntityState(
444
- getEntityCategoryGroupConfigKey(payload, state.entityPageState),
445
- state
497
+ state,
498
+ getEntityCategoryGroupConfigKey(payload, state.entityPageState)
446
499
  );
447
500
  return {
448
501
  ...state,
@@ -476,7 +529,11 @@ function exploreReducer(
476
529
  payload.selectedValue,
477
530
  payload.selected
478
531
  );
479
- updateEntityStateByCategoryGroupConfigKey(state, { filterState });
532
+ const savedFilterState: SelectedFilter[] = []; // Clear saved filter state.
533
+ updateEntityStateByCategoryGroupConfigKey(state, {
534
+ filterState,
535
+ savedFilterState,
536
+ });
480
537
  return {
481
538
  ...state,
482
539
  filterCount: getFilterCount(filterState),
@@ -41,6 +41,7 @@ import { useEntityListRelatedView } from "../../hooks/useEntityListRelatedView";
41
41
  import { useExploreState } from "../../hooks/useExploreState";
42
42
  import { useSummary } from "../../hooks/useSummary";
43
43
  import { ExploreActionKind, ExploreState } from "../../providers/exploreState";
44
+ import { SELECT_CATEGORY_KEY } from "../../providers/exploreState/constants";
44
45
  import { DESKTOP_SM } from "../../theme/common/breakpoints";
45
46
 
46
47
  export type ExploreViewProps = AzulEntitiesStaticResponse;
@@ -111,13 +112,18 @@ export const ExploreView = (props: ExploreViewProps): JSX.Element => {
111
112
  categorySection?: string,
112
113
  searchTerm?: string
113
114
  ): void => {
115
+ const dispatchType =
116
+ categoryKey === SELECT_CATEGORY_KEY.SAVED_FILTERS
117
+ ? ExploreActionKind.ApplySavedFilter
118
+ : ExploreActionKind.UpdateFilter;
119
+
114
120
  exploreDispatch({
115
121
  payload: {
116
122
  categoryKey,
117
123
  selected,
118
124
  selectedValue: selectedCategoryValue,
119
125
  },
120
- type: ExploreActionKind.UpdateFilter,
126
+ type: dispatchType,
121
127
  });
122
128
 
123
129
  trackingConfig?.trackFilterApplied?.({
@@ -226,23 +232,25 @@ function buildCategoryFilters(
226
232
  if (!categoryGroups) {
227
233
  return [{ categoryViews: selectCategoryViews }];
228
234
  }
229
- return categoryGroups.map(({ categoryConfigs, label }) => {
235
+ return categoryGroups.reduce((accGroups, { categoryConfigs, label }) => {
230
236
  // Grab the category views for the configured grouped categories.
231
237
  const categoryViews = categoryConfigs.reduce(
232
- (acc, { key: categoryKey }) => {
238
+ (accViews, { key: categoryKey }) => {
233
239
  const categoryView = selectCategoryViews.find(
234
240
  ({ key }) => key === categoryKey
235
241
  );
236
242
  if (categoryView) {
237
- acc.push(categoryView);
243
+ accViews.push(categoryView);
238
244
  }
239
- return acc;
245
+ return accViews;
240
246
  },
241
247
  [] as SelectCategoryView[]
242
248
  );
243
- // Return the configured label and category views.
244
- return { categoryViews, label };
245
- });
249
+ if (categoryViews.length > 0) {
250
+ accGroups.push({ categoryViews, label });
251
+ }
252
+ return accGroups;
253
+ }, [] as CategoryFilter[]);
246
254
  }
247
255
 
248
256
  /**