diff --git a/examples/nuxt-app/test/features/search-listing/filters.feature b/examples/nuxt-app/test/features/search-listing/filters.feature index cc50ab689d..c1046d67ff 100644 --- a/examples/nuxt-app/test/features/search-listing/filters.feature +++ b/examples/nuxt-app/test/features/search-listing/filters.feature @@ -22,7 +22,6 @@ Feature: Search listing - Filter When I toggle the search listing filters section Then the search listing dropdown field labelled "Raw filter example" should have the value "Dogs, Birds" - @mockserver Example: Term filter - Should reflect a single value from the URL Given the page endpoint for path "/filters" returns fixture "/search-listing/filters/page" with status 200 @@ -177,6 +176,44 @@ Feature: Search listing - Filter | Apples | | Oranges | + @mockserver + Example: Dependent filter - Should reflect the value from the URL + Given the page endpoint for path "/filters" returns fixture "/search-listing/dependent-filters/page" with status 200 + And the search network request is stubbed with fixture "/search-listing/dependent-filters/response" and status 200 + + When I visit the page "/filters?dependentFilter=Mammals:Dogs,Cats" + Then the search listing page should have 2 results + And the search network request should be called with the "/search-listing/dependent-filters/request" fixture + Then the filters toggle should show 2 applied filters + + When I toggle the search listing filters section + Then the search listing dropdown field labelled "Terms dependent example" should have the value "Mammals" + Then the search listing dropdown field labelled "Terms dependent child example" should have the value "Dogs, Cats" + When I click the search listing dropdown field labelled "Terms dependent example" + Then the selected dropdown field should have the items: + | Mammals | + | Birds | + When I click the search listing dropdown field labelled "Terms dependent child example" + Then the selected dropdown field should have the items: + | Dogs | + | Cats | + + @mockserver + Example: Dependent filter - Child options should update on parent selection + Given the page endpoint for path "/filters" returns fixture "/search-listing/dependent-filters/page" with status 200 + And the search network request is stubbed with fixture "/search-listing/dependent-filters/response" and status 200 + When I visit the page "/filters" + Then the search listing page should have 2 results + And the search network request should be called with the "/search-listing/dependent-filters/request-empty" fixture + + When I toggle the search listing filters section + And I click the search listing dropdown field labelled "Terms dependent example" + Then I click the option labelled "Birds" in the selected dropdown + And I click the search listing dropdown field labelled "Terms dependent child example" + Then the selected dropdown field should have the items: + | Parrot | + | Cockatoo | + @mockserver Example: Should hide the search form when hideSearchForm is set Given the page endpoint for path "/no-search-form" returns fixture "/search-listing/filters/page-no-search-form" with status 200 diff --git a/examples/nuxt-app/test/fixtures/search-listing/dependent-filters/page.json b/examples/nuxt-app/test/fixtures/search-listing/dependent-filters/page.json new file mode 100644 index 0000000000..585fd2e6b8 --- /dev/null +++ b/examples/nuxt-app/test/fixtures/search-listing/dependent-filters/page.json @@ -0,0 +1,170 @@ +{ + "title": "Depenedent filters", + "changed": "2022-11-02T12:47:29+11:00", + "created": "2022-11-02T12:47:29+11:00", + "type": "tide_search_listing", + "nid": "11dede11-10c0-111e1-1100-000000000330", + "showTopicTags": true, + "summary": "", + "config": { + "searchListingConfig": { + "resultsPerPage": 10 + }, + "queryConfig": { + "multi_match": { + "query": "{{query}}", + "fields": [ + "title^3", + "field_landing_page_summary^2", + "body", + "field_paragraph_body", + "summary_processed" + ] + } + }, + "results": { + "layout": { + "component": "TideSearchResultsList" + }, + "item": { + "grant": { + "component": "TideGrantSearchResult" + } + } + }, + "globalFilters": [ + { "terms": { "type": ["grant"] } }, + { "terms": { "field_node_site": [8888] } } + ], + "userFilters": [ + { + "id": "termFilter", + "component": "TideSearchFilterDropdown", + "filter": { + "type": "term", + "value": "termFilter.keyword" + }, + "aggregations": { + "field": "termFilter", + "source": "taxonomy" + }, + "props": { + "id": "termFilter", + "label": "Term filter example", + "placeholder": "Select a colour", + "multiple": true, + "options": [ + { + "id": "1", + "label": "Red", + "value": "Red" + }, + { + "id": "2", + "label": "Green", + "value": "Green" + }, + { + "id": "3", + "label": "Blue", + "value": "Blue" + } + ] + } + }, + { + "id": "termsFilter", + "component": "TideSearchFilterDropdown", + "filter": { + "type": "terms", + "value": "termsFilter.keyword" + }, + "aggregations": { + "field": "termsFilter", + "source": "taxonomy" + }, + "props": { + "id": "termsFilter", + "label": "Terms filter example", + "placeholder": "Select a colour", + "multiple": true, + "options": [ + { + "id": "1", + "label": "Orange", + "value": "Orange" + }, + { + "id": "2", + "label": "Purple", + "value": "Purple" + }, + { + "id": "3", + "label": "Yellow", + "value": "Yellow" + } + ] + } + }, + { + "id": "dependentFilter", + "component": "TideSearchFilterDependent", + "columns": "rpl-grid", + "filter": { + "type": "dependent", + "multiple": false, + "value": "field_species_name" + }, + "aggregations": { + "field": "topic", + "source": "taxonomy" + }, + "props": { + "id": "dependentFilter", + "label": "Terms dependent example", + "placeholder": "Select a species", + "dependantLabel": "Terms dependent child example", + "dependantPlaceholder": "All sub species", + "multiple": true, + "options": [ + { + "id": "1", + "label": "Mammals", + "value": "Mammals" + }, + { + "id": "2", + "label": "Dogs", + "value": "Dogs", + "parent": "1" + }, + { + "id": "3", + "label": "Birds", + "value": "Birds" + }, + { + "id": "4", + "label": "Cats", + "value": "Cats", + "parent": "1" + }, + { + "id": "5", + "label": "Parrot", + "value": "Parrot", + "parent": "3" + }, + { + "id": "6", + "label": "Cockatoo", + "value": "Cockatoo", + "parent": "3" + } + ] + } + } + ] + } +} diff --git a/examples/nuxt-app/test/fixtures/search-listing/dependent-filters/request-empty.json b/examples/nuxt-app/test/fixtures/search-listing/dependent-filters/request-empty.json new file mode 100644 index 0000000000..13f83e3d1d --- /dev/null +++ b/examples/nuxt-app/test/fixtures/search-listing/dependent-filters/request-empty.json @@ -0,0 +1,34 @@ +{ + "query": { + "bool": { + "must": [ + { + "match_all": {} + } + ], + "filter": [ + { + "terms": { + "type": ["grant"] + } + }, + { + "terms": { + "field_node_site": [8888] + } + }, + null + ] + } + }, + "size": 10, + "from": 0, + "sort": [ + { + "_score": "desc" + }, + { + "_doc": "desc" + } + ] +} diff --git a/examples/nuxt-app/test/fixtures/search-listing/dependent-filters/request.json b/examples/nuxt-app/test/fixtures/search-listing/dependent-filters/request.json new file mode 100644 index 0000000000..dffa693db0 --- /dev/null +++ b/examples/nuxt-app/test/fixtures/search-listing/dependent-filters/request.json @@ -0,0 +1,38 @@ +{ + "query": { + "bool": { + "must": [ + { + "match_all": {} + } + ], + "filter": [ + { + "terms": { + "type": ["grant"] + } + }, + { + "terms": { + "field_node_site": [8888] + } + }, + { + "terms": { + "field_species_name": ["Dogs", "Cats"] + } + } + ] + } + }, + "size": 10, + "from": 0, + "sort": [ + { + "_score": "desc" + }, + { + "_doc": "desc" + } + ] +} diff --git a/examples/nuxt-app/test/fixtures/search-listing/dependent-filters/response.json b/examples/nuxt-app/test/fixtures/search-listing/dependent-filters/response.json new file mode 100644 index 0000000000..391f14a63f --- /dev/null +++ b/examples/nuxt-app/test/fixtures/search-listing/dependent-filters/response.json @@ -0,0 +1,170 @@ +{ + "took": 1, + "timed_out": false, + "_shards": { + "total": 5, + "successful": 5, + "skipped": 0, + "failed": 0 + }, + "hits": { + "total": { + "value": 2, + "relation": "eq" + }, + "max_score": null, + "hits": [ + { + "_index": "a83890f7a31dea14e1ae83c6f0afacca--elasticsearch_index_default_node", + "_id": "entity:node/32830:en", + "_score": 1.0, + "_source": { + "_language": "en", + "node_grants": ["node_access_all:0"], + "url": [ + "/site-8888/tc-9b-grant-page-closed", + "/site-57/tc-9b-grant-page-closed", + "/site-56/tc-9b-grant-page-closed", + "/site-129/tc-9b-grant-page-closed", + "/site-157/tc-9b-grant-page-closed", + "/site-283/tc-9b-grant-page-closed", + "/site-125/tc-9b-grant-page-closed", + "/site-281/tc-9b-grant-page-closed", + "/site-290/tc-9b-grant-page-closed", + "/site-287/tc-9b-grant-page-closed", + "/site-4/tc-9b-grant-page-closed", + "/site-224/tc-9b-grant-page-closed", + "/site-408/tc-9b-grant-page-closed", + "/site-507/tc-9b-grant-page-closed", + "/site-509/tc-9b-grant-page-closed", + "/site-515/tc-9b-grant-page-closed", + "/site-581/tc-9b-grant-page-closed", + "/site-582/tc-9b-grant-page-closed", + "/site-622/tc-9b-grant-page-closed", + "/site-679/tc-9b-grant-page-closed", + "/site-1285/tc-9b-grant-page-closed", + "/site-1287/tc-9b-grant-page-closed", + "/site-8891/tc-9b-grant-page-closed", + "/site-8896/tc-9b-grant-page-closed" + ], + "changed": ["2023-05-09T15:00:46+10:00"], + "created": ["2023-05-09T15:00:46+10:00"], + "field_audience": [83], + "field_audience_name": ["Business"], + "field_audience_uuid": ["eb8e493b-d633-4ec9-99f6-1a825e7242c8"], + "field_content_category": [1340], + "field_content_category_name": ["Grants"], + "field_event_intro_text": ["news intro test"], + "field_landing_page_intro_text": ["Blah blah"], + "field_landing_page_summary": [ + "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam tincidunt sit amet ligula sit amet lacinia. In a leo nec tortor aliquet faucibus." + ], + "field_media_image_absolute_path": [ + "https://nginx-php.pr-1500.content-vic.sdp4.sdp.vic.gov.au/sites/default/files/tide_demo_content/Melbourne-tram.jpg" + ], + "field_node_dates_end_value": ["2019-04-09T20:00:00+10:00"], + "field_node_dates_start_value": ["2019-03-09T19:00:00+11:00"], + "field_node_on_going": [false], + "field_node_primary_csite": [8888], + "field_node_site": [ + 8888, 57, 56, 129, 157, 283, 125, 281, 290, 287, 4, 224, 408, 507, + 509, 515, 581, 582, 622, 679, 1285, 1287, 8891, 8896 + ], + "field_tags": [1466], + "field_tags_name": ["Demo Tag"], + "field_tags_path": ["/tags/demo-tag"], + "field_tags_uuid": ["11dede11-10c0-111e1-1101-000000000010"], + "field_topic": [1478], + "field_topic_name": ["Demo Topic"], + "field_topic_path": ["/topic/demo-topic"], + "field_topic_uuid": ["11dede11-10c0-111e1-1102-000000000020"], + "funding_level_from": [11326], + "funding_level_to": [26494], + "langcode": ["en"], + "nid": [32830], + "status": [true], + "title": ["Apples"], + "type": ["grant"], + "uid": [1], + "uuid": ["0327b98e-d85e-4386-8bd0-02be571cafcc"] + }, + "sort": [1.0, 3523] + }, + { + "_index": "a83890f7a31dea14e1ae83c6f0afacca--elasticsearch_index_default_node", + "_id": "entity:node/32829:en", + "_score": 1.0, + "_source": { + "_language": "en", + "node_grants": ["node_access_all:0"], + "url": ["/site-8888/tc-9a-grant-simple-test-date-range"], + "changed": ["2023-05-09T15:00:46+10:00"], + "created": ["2023-05-09T15:00:46+10:00"], + "field_audience": [83], + "field_audience_name": ["Not-for-profit groups", "Government"], + "field_audience_uuid": ["eb8e493b-d633-4ec9-99f6-1a825e7242c8"], + "field_content_category": [1340], + "field_content_category_name": ["Grants"], + "field_event_intro_text": ["news intro test"], + "field_landing_page_intro_text": ["Blah blah"], + "field_landing_page_summary": [ + "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam tincidunt sit amet ligula sit amet lacinia. In a leo nec tortor aliquet faucibus." + ], + "field_media_image_absolute_path": [ + "https://nginx-php.pr-1500.content-vic.sdp4.sdp.vic.gov.au/sites/default/files/tide_demo_content/Melbourne-tram.jpg" + ], + "field_node_dates_end_value": ["2050-07-16T00:00:00+10:00"], + "field_node_dates_start_value": ["2019-04-08T18:00:00+10:00"], + "field_node_on_going": [false], + "field_node_primary_csite": [8888], + "field_node_site": [8888], + "field_tags": [1466, 1467], + "field_tags_name": ["Demo Tag", "Another Demo Tag"], + "field_tags_path": ["/tags/demo-tag", "/tags/another-demo-tag"], + "field_tags_uuid": [ + "11dede11-10c0-111e1-1101-000000000010", + "11dede11-10c0-111e1-1101-000000000011" + ], + "field_topic": [1478], + "field_topic_name": ["Demo Topic"], + "field_topic_path": ["/topic/demo-topic"], + "field_topic_uuid": ["11dede11-10c0-111e1-1102-000000000020"], + "funding_level_from": [11326], + "funding_level_to": [26494], + "langcode": ["en"], + "nid": [32829], + "status": [true], + "title": ["Oranges"], + "type": ["grant"], + "uid": [1], + "uuid": ["244b27ce-634c-4721-b70a-fea107b2ba9f"] + }, + "sort": [1.0, 3522] + } + ] + }, + "aggregations": { + "audience": { + "doc_count_error_upper_bound": 0, + "sum_other_doc_count": 0, + "buckets": [ + { + "key": "Business", + "doc_count": 66 + }, + { + "key": "Government", + "doc_count": 73 + }, + { + "key": "Individual", + "doc_count": 28 + }, + { + "key": "Not-for-profit groups", + "doc_count": 121 + } + ] + } + } +} diff --git a/packages/nuxt-ripple/composables/use-merge-section-tags.ts b/packages/nuxt-ripple/composables/use-merge-section-tags.ts index f1a6e4eb40..45870a3258 100644 --- a/packages/nuxt-ripple/composables/use-merge-section-tags.ts +++ b/packages/nuxt-ripple/composables/use-merge-section-tags.ts @@ -9,9 +9,7 @@ const mergeTags = (existingTags: string, newTags: string): string => { return tags.join(' ') } -export const useMergeSectionTags = async ( - sectionCacheTags: any -): Promise => { +export const useMergeSectionTags = (sectionCacheTags: string | null): void => { // event will be undefined if the request is on the client side const event = useRequestEvent() diff --git a/packages/nuxt-ripple/composables/use-tide-page.ts b/packages/nuxt-ripple/composables/use-tide-page.ts index e5fc3e801d..2f890649c6 100644 --- a/packages/nuxt-ripple/composables/use-tide-page.ts +++ b/packages/nuxt-ripple/composables/use-tide-page.ts @@ -120,7 +120,7 @@ export const useTidePage = async ( headers['x-section-request-id'] = sectionRequestId } - let sectionCacheTags + let sectionCacheTags: string | null = null if (!pageData.value) { debugLogger('No cached page found, fetching the page data...') @@ -144,7 +144,7 @@ export const useTidePage = async ( // Section.io cache tags must be set on the response header to invalidate the cache after a change in drupal if (sectionCacheTags) { - useMergeSectionTags(sectionCacheTags) + nuxt.runWithContext(() => useMergeSectionTags(sectionCacheTags)) } if (error && error.value?.statusCode) { diff --git a/packages/nuxt-ripple/composables/use-tide-site.ts b/packages/nuxt-ripple/composables/use-tide-site.ts index 7e08978a3f..a36a58ffb3 100644 --- a/packages/nuxt-ripple/composables/use-tide-site.ts +++ b/packages/nuxt-ripple/composables/use-tide-site.ts @@ -5,6 +5,7 @@ export const useTideSite = async (id?: number): Promise => { const { public: config } = useRuntimeConfig() const siteId = id || config.tide?.site const { data: siteData } = useNuxtData(`site-${siteId}`) + const nuxt = useNuxtApp() const sectionRequestId = useSectionId() @@ -16,7 +17,7 @@ export const useTideSite = async (id?: number): Promise => { headers['x-section-request-id'] = sectionRequestId } - let sectionCacheTags + let sectionCacheTags: string | null = null if (!siteData.value) { const { data, error } = await useFetch('/api/tide/site', { @@ -38,7 +39,7 @@ export const useTideSite = async (id?: number): Promise => { // Section.io cache tags must be set on the response header to invalidate the cache after a change in drupal if (sectionCacheTags) { - useMergeSectionTags(sectionCacheTags) + nuxt.runWithContext(() => useMergeSectionTags(sectionCacheTags)) } return data.value diff --git a/packages/nuxt-ripple/plugins/ssr-url-params-fix.ts b/packages/nuxt-ripple/plugins/ssr-url-params-fix.ts new file mode 100644 index 0000000000..94bf13b2e9 --- /dev/null +++ b/packages/nuxt-ripple/plugins/ssr-url-params-fix.ts @@ -0,0 +1,7 @@ +export default defineNuxtPlugin((nuxtApp) => { + // Remove path from nuxt payload to prevent query params from being stripped + // The path we are removing is the path that was cached during the server side render + // https://github.com/nuxt/nuxt/pull/21408 + // https://github.com/nuxt/nuxt/issues/23153 + delete nuxtApp?.payload?.path +}) diff --git a/packages/ripple-test-utils/step_definitions/common/mocks.ts b/packages/ripple-test-utils/step_definitions/common/mocks.ts index 944055460c..a17530038d 100644 --- a/packages/ripple-test-utils/step_definitions/common/mocks.ts +++ b/packages/ripple-test-utils/step_definitions/common/mocks.ts @@ -199,3 +199,7 @@ Given('the current date is {string}', (dateString: string) => { Given('the current date is restored', () => { cy.clock().invoke('restore') }) + +Given('time moves {int} second', (sec: number = 1) => { + cy.tick(sec * 1000) +}) diff --git a/packages/ripple-test-utils/step_definitions/content-types/listing.ts b/packages/ripple-test-utils/step_definitions/content-types/listing.ts index c1d9f763a3..b7348170f4 100644 --- a/packages/ripple-test-utils/step_definitions/content-types/listing.ts +++ b/packages/ripple-test-utils/step_definitions/content-types/listing.ts @@ -207,6 +207,17 @@ Then( } ) +Then( + `I click the option labelled {string} in the selected dropdown`, + (label: string) => { + cy.get(`@selectedDropdown`) + .siblings('[role="listbox"]') + .find('[role="option"]') + .contains(label) + .click() + } +) + Then( `the selected dropdown field should have the items:`, (dataTable: DataTable) => { diff --git a/packages/ripple-tide-search/components/TideSearchListingPage.vue b/packages/ripple-tide-search/components/TideSearchListingPage.vue index f998d98363..2d3a5e893c 100644 --- a/packages/ripple-tide-search/components/TideSearchListingPage.vue +++ b/packages/ripple-tide-search/components/TideSearchListingPage.vue @@ -1,5 +1,11 @@ + + diff --git a/packages/ripple-tide-search/composables/useTideSearch.ts b/packages/ripple-tide-search/composables/useTideSearch.ts index e476ad2afb..844d2f073d 100644 --- a/packages/ripple-tide-search/composables/useTideSearch.ts +++ b/packages/ripple-tide-search/composables/useTideSearch.ts @@ -23,6 +23,12 @@ const escapeJSONString = (raw: string): string => { .replace(/[\t]/g, '\\t') } +const encodeCommasAndColons = (value: string): string => { + return value.replace(/[:|,]/g, function (match) { + return '%' + match.charCodeAt(0).toString(16) + }) +} + interface Config { queryConfig: TideSearchListingConfig['queryConfig'] userFilters: TideSearchListingConfig['userFilters'] @@ -254,6 +260,41 @@ export default ({ } } + /** + * Dependent queries - create a custom query based off the values of a parent and child field combo + */ + if (itm.filter.type === 'dependent') { + const parent = filterVal?.[`${itm?.id}-parent`] + const child = filterVal?.[`${itm?.id}-child`] + + // If we're searching for specific subcategories, let's use those subcategories + if (child?.length) { + return { + terms: { + [itm?.filter?.value]: Array.isArray(child) ? child : [child] + } + } + } + + // Otherwise we'll search for the selected parent category and all subcategories + if (parent) { + const parentID = itm.props.options?.find( + (i) => i.value === parent + )?.id + + return { + terms: { + [itm?.filter?.value]: itm.props.options + ?.filter( + (option) => + option.parent === parentID || option.id === parentID + ) + .map((i) => i.value) + } + } + } + } + /** * Call a function passed from app.config to to allow extending and overriding. The function should * return a valid DSL query. @@ -465,8 +506,34 @@ export default ({ */ const submitSearch = async () => { const filterFormValues = Object.fromEntries( - // eslint-disable-next-line @typescript-eslint/no-unused-vars - Object.entries(filterForm.value).filter(([key, value]) => value) + Object.entries(filterForm.value) + .map(([key, value]) => { + const filterConfig = userFilterConfig.find( + (itm: any) => itm.id === key + ) + + if (filterConfig.component === 'TideSearchFilterDependent') { + const parent = value[`${filterConfig.id}-parent`] + const child = value[`${filterConfig.id}-child`] + value = null + + if (parent) { + value = encodeCommasAndColons(parent) + + if (child) { + const childValue = Array.isArray(child) ? child : [child] + + value = `${value}:${childValue + .map(encodeCommasAndColons) + .join(',')}` + } + } + } + + return [key, value] + }) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + .filter(([key, value]) => value) ) // flatten locationQuery into an object for adding to the query string @@ -541,6 +608,25 @@ export default ({ parsedValue = Array.isArray(parsedValue) ? parsedValue : [parsedValue] } + if (filterConfig.component === 'TideSearchFilterDependent') { + const [parent, child = ''] = parsedValue.split(':') + + parsedValue = { + [`${filterConfig.id}-parent`]: decodeURIComponent(parent) + } + + if (child) { + const childValue = child.split(',').map(decodeURIComponent) + + parsedValue = { + ...parsedValue, + [`${filterConfig.id}-child`]: filterConfig?.props?.multiple + ? childValue + : childValue[0] + } + } + } + return { ...obj, [key]: parsedValue @@ -562,6 +648,22 @@ export default ({ return location } + /** + * Resets the filters to their default values. + * + * This is mostly needed for grouped fields which must be set back to an object. + */ + const resetFilters = (withValues = {}) => { + const defaultValues = userFilterConfig.reduce((acc, curr) => { + if (curr.component === 'TideSearchFilterDependent') { + return { ...acc, [curr.id]: {} } + } + return { ...acc } + }, {}) + + filterForm.value = { ...defaultValues, ...withValues } + } + /** * The URL is the source of truth for what is shown in the search results. * @@ -580,7 +682,9 @@ export default ({ sortOptions?.[0]?.id || null - filterForm.value = getFiltersFromRoute(newRoute) + const routeFilters = getFiltersFromRoute(newRoute) + + resetFilters(routeFilters) locationQuery.value = getLocationQueryFromRoute(newRoute) @@ -613,6 +717,7 @@ export default ({ suggestions, filterForm, appliedFilters, + resetFilters, submitSearch, goToPage, page, diff --git a/packages/ripple-tide-search/mapping/utils/index.ts b/packages/ripple-tide-search/mapping/utils/index.ts index b5d1729657..2b690e3eba 100644 --- a/packages/ripple-tide-search/mapping/utils/index.ts +++ b/packages/ripple-tide-search/mapping/utils/index.ts @@ -25,7 +25,8 @@ export const processConfig = async (config, tidePageApi) => { .map((item) => ({ id: item.drupal_internal__tid, label: item.name, - value: item.name + value: item.name, + parent: item?.parent?.[0]?.meta.drupal_internal__target_id || null })) if (activeTaxonomies && activeTaxonomies.length > 0) { diff --git a/packages/ripple-tide-search/types.ts b/packages/ripple-tide-search/types.ts index baa98abc67..2fcab5ec98 100644 --- a/packages/ripple-tide-search/types.ts +++ b/packages/ripple-tide-search/types.ts @@ -22,7 +22,7 @@ export interface FilterConfigItem { */ component: 'TideSearchFilterDropdown' | string filter?: { - type: 'raw' | 'term' | 'terms' | 'function' + type: 'raw' | 'term' | 'terms' | 'dependent' | 'function' value: string } aggregations?: { diff --git a/packages/ripple-tide-search/utils/search.ts b/packages/ripple-tide-search/utils/search.ts index 8fd11cbac0..835dc85632 100644 --- a/packages/ripple-tide-search/utils/search.ts +++ b/packages/ripple-tide-search/utils/search.ts @@ -52,3 +52,24 @@ export const getActiveFilterURL = (filters) => { return new URLSearchParams(activeFilters).toString() } + +/** + * @description Helper to calculate the number of applied filters + */ +export const getActiveFiltersTally = (values): number => { + return Object.values(values).reduce((acc: number, value): number => { + if (!value) { + return acc + } + + if (Array.isArray(value) && !value.length) { + return acc + } + + if (typeof value === 'object' && !Array.isArray(value)) { + return acc + getActiveFiltersTally(value) + } + + return acc + 1 + }, 0) +}