From 33f42b2bdbda7e306e0145381ed2f3c3270acbbd Mon Sep 17 00:00:00 2001 From: Jeffrey Dowdle Date: Wed, 19 Jul 2023 08:47:32 +1000 Subject: [PATCH] feat(@dpc-sdp/ripple-tide-search): added support for function filters, grantStatus function filter --- examples/nuxt-app/app.config.ts | 11 +++ .../features/search-listing/filters.feature | 40 ++++++++--- .../fixtures/search-listing/filters/page.json | 26 +++++++ .../filters/request-function-filter.json | 62 ++++++++++++++++ .../fixtures/search-listing/grants/page.json | 37 ++++++++++ packages/nuxt-ripple/app.config.ts | 6 ++ packages/ripple-tide-grant/app.config.ts | 71 +++++++++++++++++++ .../composables/useTideSearch.ts | 47 ++++++++++-- 8 files changed, 284 insertions(+), 16 deletions(-) create mode 100644 examples/nuxt-app/test/fixtures/search-listing/filters/request-function-filter.json create mode 100644 packages/ripple-tide-grant/app.config.ts diff --git a/examples/nuxt-app/app.config.ts b/examples/nuxt-app/app.config.ts index 5402d99e7e..bcf718943d 100644 --- a/examples/nuxt-app/app.config.ts +++ b/examples/nuxt-app/app.config.ts @@ -20,6 +20,17 @@ export default defineAppConfig({ 'linear-gradient(90deg, #382484 0%, #5A0099 20%, #7623B0 35%, #2E7478 50%, #2FA26F 70%, #2FCE6A 80%)', 'rpl-clr-gradient-vertical': 'linear-gradient(180deg, #382484 0%, #5A0099 20%, #7623B0 35%, #2E7478 50%, #2FA26F 70%, #2FCE6A 80%)' + }, + search: { + filterFunctions: { + // `dummyFunctionFilter` is used in a cypress test to check that the correct parameters are passed to custom filter functions + dummyFunctionFilter: (filterConfig, values) => { + return { + providedFilterConfig: filterConfig, + providedValues: values + } + } + } } } }) diff --git a/examples/nuxt-app/test/features/search-listing/filters.feature b/examples/nuxt-app/test/features/search-listing/filters.feature index 216d2c7c77..da6bce2c2f 100644 --- a/examples/nuxt-app/test/features/search-listing/filters.feature +++ b/examples/nuxt-app/test/features/search-listing/filters.feature @@ -125,25 +125,47 @@ Feature: Search listing - Filter | Oranges | @mockserver - Example: Should update the URL when the filters are applied + Example: Custom function filters - Should reflect an array from the URL Given the endpoint "/api/tide/page" with query "?path=/filters&site=8888" returns fixture "/search-listing/filters/page" with status 200 And the search network request is stubbed with fixture "/search-listing/filters/response" and status 200 And the current date is "Fri, 02 Feb 2050 03:04:05 GMT" - When I visit the page "/filters?termsFilter=Purple&termsFilter=Orange" + When I visit the page "/filters?functionFilter=closed&functionFilter=open" Then the search listing page should have 2 results - And the search network request should be called with the "/search-listing/filters/request-terms-array" fixture + And the search network request should be called with the "/search-listing/filters/request-function-filter" fixture - Then the search listing dropdown field labelled "Terms filter example" should have the value "Orange, Purple" - When I click the search listing dropdown field labelled "Terms filter example" + Then the search listing dropdown field labelled "Custom function filter example" should have the value "Open, Closed" + When I click the search listing dropdown field labelled "Custom function filter example" Then the selected dropdown field should have the items: - | Orange | - | Purple | - | Yellow | + | Open | + | Closed | # Close the dropdown - When I click the search listing dropdown field labelled "Terms filter example" + When I click the search listing dropdown field labelled "Custom function filter example" And the search listing results should have following items: | title | | Apples | | Oranges | +# @mockserver +# Example: Should update the URL when the filters are applied +# Given the endpoint "/api/tide/page" with query "?path=/filters&site=8888" returns fixture "/search-listing/filters/page" with status 200 +# And the search network request is stubbed with fixture "/search-listing/filters/response" and status 200 +# And the current date is "Fri, 02 Feb 2050 03:04:05 GMT" + +# When I visit the page "/filters?termsFilter=Purple&termsFilter=Orange" +# Then the search listing page should have 2 results +# And the search network request should be called with the "/search-listing/filters/request-terms-array" fixture + +# Then the search listing dropdown field labelled "Terms filter example" should have the value "Orange, Purple" +# When I click the search listing dropdown field labelled "Terms filter example" +# Then the selected dropdown field should have the items: +# | Orange | +# | Purple | +# | Yellow | +# # Close the dropdown +# When I click the search listing dropdown field labelled "Terms filter example" +# And the search listing results should have following items: +# | title | +# | Apples | +# | Oranges | + diff --git a/examples/nuxt-app/test/fixtures/search-listing/filters/page.json b/examples/nuxt-app/test/fixtures/search-listing/filters/page.json index fda8f28ded..d3a8c650b2 100644 --- a/examples/nuxt-app/test/fixtures/search-listing/filters/page.json +++ b/examples/nuxt-app/test/fixtures/search-listing/filters/page.json @@ -141,6 +141,32 @@ } ] } + }, + { + "id": "functionFilter", + "component": "TideSearchFilterDropdown", + "filter": { + "type": "function", + "value": "dummyFunctionFilter" + }, + "props": { + "id": "functionFilter", + "label": "Custom function filter example", + "placeholder": "Select a grant status", + "multiple": true, + "options": [ + { + "id": "open", + "label": "Open", + "value": "open" + }, + { + "id": "closed", + "label": "Closed", + "value": "closed" + } + ] + } } ] } diff --git a/examples/nuxt-app/test/fixtures/search-listing/filters/request-function-filter.json b/examples/nuxt-app/test/fixtures/search-listing/filters/request-function-filter.json new file mode 100644 index 0000000000..b64d980379 --- /dev/null +++ b/examples/nuxt-app/test/fixtures/search-listing/filters/request-function-filter.json @@ -0,0 +1,62 @@ +{ + "query": { + "bool": { + "must": [ + { + "match_all": {} + } + ], + "filter": [ + { + "terms": { + "type": ["grant"] + } + }, + { + "terms": { + "field_node_site": [8888] + } + }, + { + "providedFilterConfig": { + "id": "functionFilter", + "component": "TideSearchFilterDropdown", + "filter": { + "type": "function", + "value": "dummyFunctionFilter" + }, + "props": { + "id": "functionFilter", + "label": "Custom function filter example", + "placeholder": "Select a grant status", + "multiple": true, + "options": [ + { + "id": "open", + "label": "Open", + "value": "open" + }, + { + "id": "closed", + "label": "Closed", + "value": "closed" + } + ] + } + }, + "providedValues": ["closed", "open"] + } + ] + } + }, + "size": 10, + "from": 0, + "sort": [ + { + "_score": "desc" + }, + { + "_doc": "desc" + } + ] +} diff --git a/examples/nuxt-app/test/fixtures/search-listing/grants/page.json b/examples/nuxt-app/test/fixtures/search-listing/grants/page.json index 2f214d6425..f5dfaf2c4f 100644 --- a/examples/nuxt-app/test/fixtures/search-listing/grants/page.json +++ b/examples/nuxt-app/test/fixtures/search-listing/grants/page.json @@ -93,6 +93,43 @@ } ] } + }, + { + "id": "status", + "component": "TideSearchFilterDropdown", + "filter": { + "type": "function", + "value": "grantStatus" + }, + "props": { + "id": "status", + "label": "Status", + "placeholder": "Select for opened, closed or upcoming grants", + "type": "RplFormDropdown", + "multiple": true, + "options": [ + { + "id": "open", + "label": "Open", + "value": "open" + }, + { + "id": "closed", + "label": "Closed", + "value": "closed" + }, + { + "id": "ongoing", + "label": "Ongoing", + "value": "ongoing" + }, + { + "id": "opening_soon", + "label": "Opening soon", + "value": "opening_soon" + } + ] + } } ] } diff --git a/packages/nuxt-ripple/app.config.ts b/packages/nuxt-ripple/app.config.ts index a1c618ae02..afccf152a5 100644 --- a/packages/nuxt-ripple/app.config.ts +++ b/packages/nuxt-ripple/app.config.ts @@ -13,6 +13,12 @@ declare module '@nuxt/schema' { ['rpl-clr-focus']?: string ['rpl-clr-type-focus-contrast']?: string } + search?: { + filterFunctions?: Record< + string, + (filterConfig: any, values: string[]) => void + > + } } } } diff --git a/packages/ripple-tide-grant/app.config.ts b/packages/ripple-tide-grant/app.config.ts new file mode 100644 index 0000000000..9362a80db2 --- /dev/null +++ b/packages/ripple-tide-grant/app.config.ts @@ -0,0 +1,71 @@ +export default defineAppConfig({ + ripple: { + search: { + filterFunctions: { + grantStatus: (filterConfig: string, statuses: unknown) => { + const mapStatusToFilterDSL = (status) => { + switch (status) { + case 'open': + return { + bool: { + must: [ + { + range: { + field_node_dates_start_value: { + lte: 'now' + } + } + }, + { + range: { + field_node_dates_end_value: { + gte: 'now' + } + } + } + ] + } + } + case 'closed': + return { + range: { + field_node_dates_end_value: { + lte: 'now' + } + } + } + + case 'ongoing': + return { + term: { + field_node_on_going: 'true' + } + } + + case 'opening_soon': + return { + range: { + field_node_dates_start_value: { + gte: 'now' + } + } + } + default: + return null + } + } + + const filters = statuses + .map(mapStatusToFilterDSL) + .filter((query) => !!query) // filter null values + + return { + bool: { + should: filters + } + } + } + } + } + } +}) diff --git a/packages/ripple-tide-search/composables/useTideSearch.ts b/packages/ripple-tide-search/composables/useTideSearch.ts index fefe71d0ed..734b2554fc 100644 --- a/packages/ripple-tide-search/composables/useTideSearch.ts +++ b/packages/ripple-tide-search/composables/useTideSearch.ts @@ -18,6 +18,7 @@ export default ( ) => { const { public: config } = useRuntimeConfig() const route: RouteLocation = useRoute() + const appConfig = useAppConfig() const index = customIndex || config.tide.appSearch.engineName @@ -120,8 +121,10 @@ export default ( const userFilters = computed(() => { return Object.keys(filterForm.value).map((key: string) => { const itm = userFilterConfig.find((itm) => itm.id === key) + const filterVal = filterForm.value[key] && Array.from(filterForm.value[key]) + // Need to work out if form has value - will be different for different controls const hasValue = (v: unknown) => { if (itm.component === 'TideSearchFilterDropdown') { @@ -129,17 +132,22 @@ export default ( } return v } + if (itm.filter && hasValue(filterVal)) { - // Raw ES Filter clause from Tide config, replaces {{value}} with user value + /** + * Raw ES Filter clause from Tide config, replaces {{value}} with user value + */ if (itm.filter.type === 'raw') { const re = new RegExp('{{value}}', 'g') const result = itm.filter.value.replace(re, JSON.stringify(filterVal)) return JSON.parse(result) } - // Term and Terms querys - To simplify things we transform all term queries into terms queries with a single value array - // - Term query: https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-term-query.html - // - Terms query: https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-terms-query.html + /** + * Term and Terms querys - To simplify things we transform all term queries into terms queries with a single value array + * - Term query: https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-term-query.html + * - Terms query: https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-terms-query.html + */ if (itm.filter.type === 'term' || itm.filter.type === 'terms') { return { terms: { @@ -150,10 +158,35 @@ export default ( } } - // Call a function passed from app.config to add filters + /** + * Call a function passed from app.config to to allow extending and overriding. The function should + * return a valid DSL query. + * When called the function is passed the filter config and the value of the filter from the user + * + * In the nuxt app.config, the function is provided like this: + * { + * ripple: { + * search: { + exampleFunction: (filterConfig, values) => { + return { + ... some DSL query + } + } + * } + * } + * } + */ if (itm.filter.type === 'function') { - // TODO: this should allow calling a custom function that returns a valid query clause - // function should be passed through from app.config to allow extending and overriding + const filterFuncs = appConfig?.ripple?.search?.filterFunctions || {} + const fn = filterFuncs[itm.filter.value] + + if (typeof fn !== 'function') { + throw new Error( + `Search listing: No matching filter function called "${itm.filter.value}"` + ) + } + + return fn(itm, filterVal) } } })