From acc30c915af441831a7f74d7a58694530c4fc270 Mon Sep 17 00:00:00 2001 From: Jeffrey Dowdle Date: Fri, 29 Dec 2023 08:55:56 +1100 Subject: [PATCH 1/7] fix(@dpc-sdp/nuxt-ripple): fixed 'A composable that requires access to the Nuxt instance...' error --- packages/nuxt-ripple/composables/use-merge-section-tags.ts | 4 +--- packages/nuxt-ripple/composables/use-tide-page.ts | 4 ++-- packages/nuxt-ripple/composables/use-tide-site.ts | 5 +++-- 3 files changed, 6 insertions(+), 7 deletions(-) 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 cb11b1bb05..622bb827a1 100644 --- a/packages/nuxt-ripple/composables/use-tide-page.ts +++ b/packages/nuxt-ripple/composables/use-tide-page.ts @@ -73,7 +73,7 @@ export const useTidePage = async ( headers.cookie = `${AuthCookieNames.ACCESS_TOKEN}=${accessTokenCookie.value};${AuthCookieNames.ACCESS_TOKEN_EXPIRY}=${accessTokenExpiryCookie.value}` } - let sectionCacheTags + let sectionCacheTags: string | null = null if (!pageData.value) { const { data, error } = await useFetch('/api/tide/page', { @@ -95,7 +95,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 867cc63a46..672dc3cb7b 100644 --- a/packages/nuxt-ripple/composables/use-tide-site.ts +++ b/packages/nuxt-ripple/composables/use-tide-site.ts @@ -4,8 +4,9 @@ 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() - let sectionCacheTags + let sectionCacheTags: string | null = null if (!siteData.value) { const { data, error } = await useFetch('/api/tide/site', { @@ -26,7 +27,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 From b7ee300587761d5de658dc03513f2888987dff31 Mon Sep 17 00:00:00 2001 From: Jeffrey Dowdle Date: Fri, 29 Dec 2023 09:00:20 +1100 Subject: [PATCH 2/7] fix(@dpc-sdp/nuxt-ripple): prevented url params being stripped for cached pages --- packages/nuxt-ripple/plugins/ssr-url-params-fix.ts | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 packages/nuxt-ripple/plugins/ssr-url-params-fix.ts 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 +}) From cfc97fb9f8f1362ab3a7b64ed516b3a2513d4fd2 Mon Sep 17 00:00:00 2001 From: Dylan Kelly Date: Tue, 2 Jan 2024 16:00:30 +1100 Subject: [PATCH 3/7] feat(@dpc-sdp/ripple-tide-search): :construction: poc for adding dependent filters --- .../features/search-listing/filters.feature | 10 ++ .../hierarchical-filters/page.json | 160 +++++++++++++++++ .../hierarchical-filters/request-raw.json | 38 ++++ .../hierarchical-filters/response.json | 170 ++++++++++++++++++ .../global/TideSearchFilterDependent.vue | 84 +++++++++ 5 files changed, 462 insertions(+) create mode 100644 examples/nuxt-app/test/fixtures/search-listing/hierarchical-filters/page.json create mode 100644 examples/nuxt-app/test/fixtures/search-listing/hierarchical-filters/request-raw.json create mode 100644 examples/nuxt-app/test/fixtures/search-listing/hierarchical-filters/response.json create mode 100644 packages/ripple-tide-search/components/global/TideSearchFilterDependent.vue diff --git a/examples/nuxt-app/test/features/search-listing/filters.feature b/examples/nuxt-app/test/features/search-listing/filters.feature index 1480f00385..4cef5ce31f 100644 --- a/examples/nuxt-app/test/features/search-listing/filters.feature +++ b/examples/nuxt-app/test/features/search-listing/filters.feature @@ -23,6 +23,7 @@ Feature: Search listing - Filter 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,3 +178,12 @@ Feature: Search listing - Filter | Apples | | Oranges | + @mockserver @focus + Example: Hierarchical filter from taxonomy + Given the page endpoint for path "/filters" returns fixture "/search-listing/hierarchical-filters/page" with status 200 + And the search network request is stubbed with fixture "/search-listing/hierarchical-filters/response" and status 200 + And the current date is "Fri, 02 Feb 2050 03:04:05 GMT" + + When I visit the page "/filters?rawFilter=Birds&rawFilter=Dogs" +# Then the search listing page should have 2 results +# And the search network request should be called with the "/search-listing/hierarchical-filters/request-raw" fixture diff --git a/examples/nuxt-app/test/fixtures/search-listing/hierarchical-filters/page.json b/examples/nuxt-app/test/fixtures/search-listing/hierarchical-filters/page.json new file mode 100644 index 0000000000..15ab4e5b8d --- /dev/null +++ b/examples/nuxt-app/test/fixtures/search-listing/hierarchical-filters/page.json @@ -0,0 +1,160 @@ +{ + "title": "Hierarchical 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": "raw", + "value": "{\"ids\":{\"values\":{{value}}}}" + }, + "aggregations": { + "field": "rawFilter", + "source": "taxonomy" + }, + "props": { + "id": "dependentFilter", + "label": "Dependent filter", + "placeholder": "Select a pet", + "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" + } + ] + } + } + ] + } +} diff --git a/examples/nuxt-app/test/fixtures/search-listing/hierarchical-filters/request-raw.json b/examples/nuxt-app/test/fixtures/search-listing/hierarchical-filters/request-raw.json new file mode 100644 index 0000000000..a67b73b378 --- /dev/null +++ b/examples/nuxt-app/test/fixtures/search-listing/hierarchical-filters/request-raw.json @@ -0,0 +1,38 @@ +{ + "query": { + "bool": { + "must": [ + { + "match_all": {} + } + ], + "filter": [ + { + "terms": { + "type": ["grant"] + } + }, + { + "terms": { + "field_node_site": [8888] + } + }, + { + "ids": { + "values": ["Birds", "Dogs"] + } + } + ] + } + }, + "size": 10, + "from": 0, + "sort": [ + { + "_score": "desc" + }, + { + "_doc": "desc" + } + ] +} diff --git a/examples/nuxt-app/test/fixtures/search-listing/hierarchical-filters/response.json b/examples/nuxt-app/test/fixtures/search-listing/hierarchical-filters/response.json new file mode 100644 index 0000000000..391f14a63f --- /dev/null +++ b/examples/nuxt-app/test/fixtures/search-listing/hierarchical-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/ripple-tide-search/components/global/TideSearchFilterDependent.vue b/packages/ripple-tide-search/components/global/TideSearchFilterDependent.vue new file mode 100644 index 0000000000..cde9d52c41 --- /dev/null +++ b/packages/ripple-tide-search/components/global/TideSearchFilterDependent.vue @@ -0,0 +1,84 @@ + + + From 6381e7fc85a61a893008be37f33e738652b190c3 Mon Sep 17 00:00:00 2001 From: Dylan Kelly Date: Tue, 2 Jan 2024 16:00:30 +1100 Subject: [PATCH 4/7] feat(@dpc-sdp/ripple-tide-search): :construction: poc for adding dependent filters --- packages/ripple-tide-search/mapping/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/ripple-tide-search/mapping/index.ts b/packages/ripple-tide-search/mapping/index.ts index e6a86c856b..b7ea8994a3 100644 --- a/packages/ripple-tide-search/mapping/index.ts +++ b/packages/ripple-tide-search/mapping/index.ts @@ -151,6 +151,7 @@ const processConfig = async (config, tidePageApi) => { const activeTaxonomies = taxonomyResults .filter((tax) => tax.status === true) .map((item) => ({ + parent: item.parent[0]?.meta.drupal_internal__target_id || null, id: item.drupal_internal__tid, label: item.name, value: item.name From d1ab7b1d8136141b8daaca90c99fa5031570c872 Mon Sep 17 00:00:00 2001 From: David Featherston Date: Fri, 5 Jan 2024 16:58:36 +1100 Subject: [PATCH 5/7] feat(@dpc-sdp/ripple-tide-search): wip for grouped dependent field --- .../features/search-listing/filters.feature | 47 ++++-- .../page.json | 30 ++-- .../dependent-filters/request-empty.json | 34 +++++ .../request.json} | 4 +- .../response.json | 0 .../step_definitions/content-types/listing.ts | 11 ++ .../components/TideSearchListingPage.vue | 23 ++- .../global/TideSearchFilterDependent.vue | 135 ++++++++++-------- .../composables/useTideSearch.ts | 111 +++++++++++++- packages/ripple-tide-search/mapping/index.ts | 2 +- packages/ripple-tide-search/types.ts | 2 +- packages/ripple-tide-search/utils/search.ts | 21 +++ 12 files changed, 319 insertions(+), 101 deletions(-) rename examples/nuxt-app/test/fixtures/search-listing/{hierarchical-filters => dependent-filters}/page.json (83%) create mode 100644 examples/nuxt-app/test/fixtures/search-listing/dependent-filters/request-empty.json rename examples/nuxt-app/test/fixtures/search-listing/{hierarchical-filters/request-raw.json => dependent-filters/request.json} (86%) rename examples/nuxt-app/test/fixtures/search-listing/{hierarchical-filters => dependent-filters}/response.json (100%) diff --git a/examples/nuxt-app/test/features/search-listing/filters.feature b/examples/nuxt-app/test/features/search-listing/filters.feature index 4cef5ce31f..e1315a7751 100644 --- a/examples/nuxt-app/test/features/search-listing/filters.feature +++ b/examples/nuxt-app/test/features/search-listing/filters.feature @@ -22,8 +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 @@ -178,12 +176,43 @@ Feature: Search listing - Filter | Apples | | Oranges | - @mockserver @focus - Example: Hierarchical filter from taxonomy - Given the page endpoint for path "/filters" returns fixture "/search-listing/hierarchical-filters/page" with status 200 - And the search network request is stubbed with fixture "/search-listing/hierarchical-filters/response" and status 200 + @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 And the current date is "Fri, 02 Feb 2050 03:04:05 GMT" - When I visit the page "/filters?rawFilter=Birds&rawFilter=Dogs" -# Then the search listing page should have 2 results -# And the search network request should be called with the "/search-listing/hierarchical-filters/request-raw" fixture + 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 + And the current date is "Fri, 02 Feb 2050 03:04:05 GMT" + + 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 | diff --git a/examples/nuxt-app/test/fixtures/search-listing/hierarchical-filters/page.json b/examples/nuxt-app/test/fixtures/search-listing/dependent-filters/page.json similarity index 83% rename from examples/nuxt-app/test/fixtures/search-listing/hierarchical-filters/page.json rename to examples/nuxt-app/test/fixtures/search-listing/dependent-filters/page.json index 15ab4e5b8d..585fd2e6b8 100644 --- a/examples/nuxt-app/test/fixtures/search-listing/hierarchical-filters/page.json +++ b/examples/nuxt-app/test/fixtures/search-listing/dependent-filters/page.json @@ -1,5 +1,5 @@ { - "title": "Hierarchical filters", + "title": "Depenedent filters", "changed": "2022-11-02T12:47:29+11:00", "created": "2022-11-02T12:47:29+11:00", "type": "tide_search_listing", @@ -112,22 +112,26 @@ "component": "TideSearchFilterDependent", "columns": "rpl-grid", "filter": { - "type": "raw", - "value": "{\"ids\":{\"values\":{{value}}}}" + "type": "dependent", + "multiple": false, + "value": "field_species_name" }, "aggregations": { - "field": "rawFilter", + "field": "topic", "source": "taxonomy" }, "props": { "id": "dependentFilter", - "label": "Dependent filter", - "placeholder": "Select a pet", + "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" + "value": "Mammals" }, { "id": "2", @@ -138,18 +142,24 @@ { "id": "3", "label": "Birds", - "value": "birds" + "value": "Birds" }, { "id": "4", "label": "Cats", - "value": "cats", + "value": "Cats", "parent": "1" }, { "id": "5", "label": "Parrot", - "value": "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/hierarchical-filters/request-raw.json b/examples/nuxt-app/test/fixtures/search-listing/dependent-filters/request.json similarity index 86% rename from examples/nuxt-app/test/fixtures/search-listing/hierarchical-filters/request-raw.json rename to examples/nuxt-app/test/fixtures/search-listing/dependent-filters/request.json index a67b73b378..dffa693db0 100644 --- a/examples/nuxt-app/test/fixtures/search-listing/hierarchical-filters/request-raw.json +++ b/examples/nuxt-app/test/fixtures/search-listing/dependent-filters/request.json @@ -18,8 +18,8 @@ } }, { - "ids": { - "values": ["Birds", "Dogs"] + "terms": { + "field_species_name": ["Dogs", "Cats"] } } ] diff --git a/examples/nuxt-app/test/fixtures/search-listing/hierarchical-filters/response.json b/examples/nuxt-app/test/fixtures/search-listing/dependent-filters/response.json similarity index 100% rename from examples/nuxt-app/test/fixtures/search-listing/hierarchical-filters/response.json rename to examples/nuxt-app/test/fixtures/search-listing/dependent-filters/response.json 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 dcee8a9f7d..1cbe51bd38 100644 --- a/packages/ripple-test-utils/step_definitions/content-types/listing.ts +++ b/packages/ripple-test-utils/step_definitions/content-types/listing.ts @@ -205,6 +205,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 d45900c7f0..917601911b 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 7275d46011..63400bd4da 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) + }) +} + export default ( queryConfig: TideSearchListingConfig['queryConfig'], userFilterConfig: TideSearchListingConfig['userFilters'], @@ -204,6 +210,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. @@ -381,8 +422,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) ) await navigateTo({ @@ -437,6 +504,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 @@ -444,6 +530,22 @@ export default ( }, {}) } + /** + * 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. * @@ -458,7 +560,9 @@ export default ( sortOptions?.[0]?.id || null - filterForm.value = getFiltersFromRoute(newRoute) + const routeFilters = getFiltersFromRoute(newRoute) + + resetFilters(routeFilters) getSearchResults(isFirstRun) } @@ -488,6 +592,7 @@ export default ( suggestions, filterForm, appliedFilters, + resetFilters, submitSearch, goToPage, page, diff --git a/packages/ripple-tide-search/mapping/index.ts b/packages/ripple-tide-search/mapping/index.ts index b7ea8994a3..c7c6b9798c 100644 --- a/packages/ripple-tide-search/mapping/index.ts +++ b/packages/ripple-tide-search/mapping/index.ts @@ -151,7 +151,7 @@ const processConfig = async (config, tidePageApi) => { const activeTaxonomies = taxonomyResults .filter((tax) => tax.status === true) .map((item) => ({ - parent: item.parent[0]?.meta.drupal_internal__target_id || null, + parent: item?.parent?.[0]?.meta.drupal_internal__target_id || null, id: item.drupal_internal__tid, label: item.name, value: item.name diff --git a/packages/ripple-tide-search/types.ts b/packages/ripple-tide-search/types.ts index 6f9c62e554..42ca81b0a6 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) +} From ff239ccdd211cd36a8a673fd3d3872d3627f14aa Mon Sep 17 00:00:00 2001 From: David Featherston Date: Thu, 11 Jan 2024 11:43:01 +1100 Subject: [PATCH 6/7] chore(nuxt-app): push up failing tests --- .../features/search-listing/filters.feature | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/examples/nuxt-app/test/features/search-listing/filters.feature b/examples/nuxt-app/test/features/search-listing/filters.feature index e1315a7751..b4a8af0acb 100644 --- a/examples/nuxt-app/test/features/search-listing/filters.feature +++ b/examples/nuxt-app/test/features/search-listing/filters.feature @@ -189,15 +189,15 @@ Feature: Search listing - Filter 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" + 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 | + 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 @@ -212,7 +212,7 @@ Feature: Search listing - Filter 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 | + And I click the search listing dropdown field labelled "Terms dependent child example" + Then the selected dropdown field should have the items: + | Parrot | + | Cockatoo | From 0603182baea49a51fc628ec3e01cc3e4ac86bafa Mon Sep 17 00:00:00 2001 From: Dylan Kelly Date: Fri, 12 Jan 2024 15:01:53 +1100 Subject: [PATCH 7/7] fix(@dpc-sdp/ripple-tide-search): :white_check_mark: fix failing dependent filter tests --- .../nuxt-app/test/features/search-listing/filters.feature | 3 --- packages/ripple-test-utils/step_definitions/common/mocks.ts | 4 ++++ .../components/global/TideSearchFilterDependent.vue | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/examples/nuxt-app/test/features/search-listing/filters.feature b/examples/nuxt-app/test/features/search-listing/filters.feature index ef5ba80f63..c1046d67ff 100644 --- a/examples/nuxt-app/test/features/search-listing/filters.feature +++ b/examples/nuxt-app/test/features/search-listing/filters.feature @@ -180,7 +180,6 @@ Feature: Search listing - Filter 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 - And the current date is "Fri, 02 Feb 2050 03:04:05 GMT" When I visit the page "/filters?dependentFilter=Mammals:Dogs,Cats" Then the search listing page should have 2 results @@ -203,8 +202,6 @@ Feature: Search listing - Filter 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 - And the current date is "Fri, 02 Feb 2050 03:04:05 GMT" - 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 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-tide-search/components/global/TideSearchFilterDependent.vue b/packages/ripple-tide-search/components/global/TideSearchFilterDependent.vue index 1cc4adb8e0..5978da74f4 100644 --- a/packages/ripple-tide-search/components/global/TideSearchFilterDependent.vue +++ b/packages/ripple-tide-search/components/global/TideSearchFilterDependent.vue @@ -44,8 +44,8 @@ const handleSelect = (value: string) => { if (formNode && initialChild.value) { nextTick(() => { formNode.input({ - 'topic-parent': value, - 'topic-child': initialChild.value + [`${props.id}-parent`]: value, + [`${props.id}-child`]: initialChild.value }) initialChild.value = null })