diff --git a/Makefile b/Makefile index 42603ec5..256f79cd 100644 --- a/Makefile +++ b/Makefile @@ -102,6 +102,10 @@ lint_front: @echo "🔎 Running linters for frontend code..." ${DOCKER_COMPOSE} run --rm search_nodejs npm run format +tsc_watch: + @echo "🔎 Running front-end tsc in watch mode..." + ${DOCKER_COMPOSE} run --rm search_nodejs npm run build:watch + #-------# # Tests # #-------# diff --git a/frontend/public/off.html b/frontend/public/off.html index 0f20c5b6..7c5d9161 100644 --- a/frontend/public/off.html +++ b/frontend/public/off.html @@ -327,278 +327,47 @@
- - + + + +
- - - -

Elasticsearch query

-
{
-    "query": {
-        "bool": {
-            "should": [
-                {
-                    "bool": {
-                        "should": [
-                            {
-                                "match_phrase": {
-                                    "product_name.en": {
-                                        "query": "test",
-                                        "boost": 2.0
-                                    }
-                                }
-                            },
-                            {
-                                "match_phrase": {
-                                    "product_name.fr": {
-                                        "query": "test",
-                                        "boost": 2.0
-                                    }
-                                }
-                            }
-                        ]
-                    }
-                },
-                {
-                    "bool": {
-                        "should": [
-                            {
-                                "match_phrase": {
-                                    "generic_name.en": {
-                                        "query": "test",
-                                        "boost": 2.0
-                                    }
-                                }
-                            },
-                            {
-                                "match_phrase": {
-                                    "generic_name.fr": {
-                                        "query": "test",
-                                        "boost": 2.0
-                                    }
-                                }
-                            }
-                        ]
-                    }
-                },
-                {
-                    "bool": {
-                        "should": [
-                            {
-                                "match_phrase": {
-                                    "categories.en": {
-                                        "query": "test",
-                                        "boost": 2.0
-                                    }
-                                }
-                            },
-                            {
-                                "match_phrase": {
-                                    "categories.fr": {
-                                        "query": "test",
-                                        "boost": 2.0
-                                    }
-                                }
-                            }
-                        ]
-                    }
-                },
-                {
-                    "bool": {
-                        "should": [
-                            {
-                                "match_phrase": {
-                                    "labels.en": {
-                                        "query": "test",
-                                        "boost": 2.0
-                                    }
-                                }
-                            },
-                            {
-                                "match_phrase": {
-                                    "labels.fr": {
-                                        "query": "test",
-                                        "boost": 2.0
-                                    }
-                                }
-                            }
-                        ]
-                    }
-                },
-                {
-                    "match_phrase": {
-                        "brands": {
-                            "query": "test",
-                            "boost": 2.0
-                        }
-                    }
-                },
-                {
-                    "multi_match": {
-                        "query": "test",
-                        "fields": [
-                            "product_name.en",
-                            "product_name.fr",
-                            "generic_name.en",
-                            "generic_name.fr",
-                            "categories.en",
-                            "categories.fr",
-                            "labels.en",
-                            "labels.fr",
-                            "brands"
-                        ]
-                    }
-                }
-            ]
-        }
-    },
-    "aggs": {
-        "brands_tags": {
-            "terms": {
-                "field": "brands_tags"
-            }
-        },
-        "lang": {
-            "terms": {
-                "field": "lang"
-            }
-        },
-        "owner": {
-            "terms": {
-                "field": "owner"
-            }
-        },
-        "categories_tags": {
-            "terms": {
-                "field": "categories_tags"
-            }
-        },
-        "labels_tags": {
-            "terms": {
-                "field": "labels_tags"
-            }
-        },
-        "countries_tags": {
-            "terms": {
-                "field": "countries_tags"
-            }
-        },
-        "states_tags": {
-            "terms": {
-                "field": "states_tags"
-            }
-        },
-        "nutrition_grades": {
-            "terms": {
-                "field": "nutrition_grades"
-            }
-        },
-        "ecoscore_grade": {
-            "terms": {
-                "field": "ecoscore_grade"
-            }
-        },
-        "nova_groups": {
-            "terms": {
-                "field": "nova_groups"
-            }
-        }
-    },
-    "size": 24,
-    "from": 0
-}
+

FIXME: pagination

diff --git a/frontend/src/errors.ts b/frontend/src/errors.ts new file mode 100644 index 00000000..845d1b86 --- /dev/null +++ b/frontend/src/errors.ts @@ -0,0 +1,19 @@ +/** + * An error thrown if we don't have a template in results component + */ +export class MissingResultTemplateError extends Error { + constructor(message: string) { + super(message); + this.name = 'MissingResultTemplateError'; + } +} + +/** + * An error thrown if we have multiple templates in results component + */ +export class MultipleResultTemplateError extends Error { + constructor(message: string) { + super(message); + this.name = 'MissingResultTemplateError'; + } +} diff --git a/frontend/src/search-ctl.ts b/frontend/src/search-ctl.ts index e7d37493..d2c51a53 100644 --- a/frontend/src/search-ctl.ts +++ b/frontend/src/search-ctl.ts @@ -57,16 +57,27 @@ export const SearchaliciousSearchMixin = >( @property() index?: string; - // TODO: should be on results element instead + /** + * Number of result per page + */ @property({type: Number, attribute: 'page-size'}) pageSize = 10; + /** + * Last search page count + */ @state() _pageCount?: number; + /** + * Last search results for current page + */ @state() _results?: {}[]; + /** + * Last search total number of results + */ @state() _count?: number; diff --git a/frontend/src/search-results.ts b/frontend/src/search-results.ts index 8a5ae562..57642ed8 100644 --- a/frontend/src/search-results.ts +++ b/frontend/src/search-results.ts @@ -5,12 +5,21 @@ import {repeat} from 'lit/directives/repeat.js'; import {EventRegistrationMixin} from './event-listener-setup'; import {SearchResultEvent} from './events'; import {SearchaliciousEvents} from './enums'; +import { + MissingResultTemplateError, + MultipleResultTemplateError, +} from './errors'; + +// we need it to declare functions +type htmlType = typeof html; + /** * The search results element * - * It will display results based upon the InnerHTML considered as a template. + * It will display results based upon the slot with name `result`, + * considered as a template, with variable interpolation (using tag-params library). * - * It reacts to the `searchalicious-result` event fired by the search controller + * It reacts to the `searchalicious-result` event fired by the search controller. */ @customElement('searchalicious-results') export class SearchaliciousResults extends EventRegistrationMixin(LitElement) { @@ -31,24 +40,77 @@ export class SearchaliciousResults extends EventRegistrationMixin(LitElement) { resultId = 'id'; /** - * Get the slot contents and interpret it as a JS template. + * A function rendering a single result. We define this just to get it's prototype right. + * + * It will be replaced by a dynamic function created + * from the content of the slot named result. + * + * Note that we need to pass along html, because at rendering time, it will not be available as a global */ - getResultTemplate() { - return ( - this.renderRoot.querySelector('slot')?.toString() || - 'Please provide a template' - ); + resultRenderer = function (html: htmlType, result: Object, index: number) { + const data = html`Please provide a template`; + if (!result && !index) { + // just to make TS happy that we use the variables + // eslint-disable-next-line no-empty + } + return data; + }; + + // override constructor to generate the result renderer function + constructor() { + super(); + this.resultRenderer = this._buildResultRenderer(); + } + + /** + * Build a result renderer from the template provided by user. + * It creates dynamically a function that renders the template with the given result and index. + * This is the best way I could find ! + * It is faster as using eval as the function is built only once at component creation time. + * @returns Function (htmlType, Object, string) => TemplateResult + */ + _buildResultRenderer() { + const resultTemplate = this._getTemplate(); + return Function( + 'html', + 'result', + 'index', + 'return html`' + resultTemplate + '`;' + ) as typeof this.resultRenderer; + } + + /** + * Get the template for one result, using `` + * This must be run in constructor ! (to be able to grab this.innerHTML) + */ + _getTemplate() { + // const fragment = new DocumentFragment(); + // fragment.replaceChildren(document.createElement("template")); + // (fragment.firstChild! as HTMLElement).append(this.innerHTML); + const fragment = document.createElement('div'); + fragment.innerHTML = this.innerHTML; + + //const element = new DOMParser().parseFromString(``, "text/html"); + const slots = fragment.querySelectorAll('slot[name="result"]'); + // we only need one ! + if (!slots || slots.length === 0) { + throw new MissingResultTemplateError('No slot found with name="result"'); + } else if (slots.length > 1) { + throw new MultipleResultTemplateError( + 'Multiple slots found with name="result"' + ); + } + return slots[0].innerHTML; } override render() { - const resultTemplate = this.getResultTemplate(); - console.log('Result template'); - console.dir(resultTemplate); + const renderResult = this.resultRenderer; + console.dir(this.results[0]); return html`
    ${repeat( this.results, - (item) => item[this.resultId] as string, - (item, index) => html`
  • Result ${index} ${item}
  • ` + (result) => result[this.resultId] as string, + (result, index) => renderResult(html, result, index) )}
`; }