From dbd54b2527e6d0032ab503dc5bf5829d17dbd6f8 Mon Sep 17 00:00:00 2001 From: Alex Garel Date: Fri, 25 Oct 2024 10:27:19 +0200 Subject: [PATCH 01/23] feat: enhanced completion * add the option of a preprocessor for taxonomies * add a preprocessor for Open Food Facts to handle: * brands taxonomy * add main language * add xx entries to all languages * fix to have names in synonyms * add a score to completion to first match shortest entries * enable querying multiple languages at once * added tests --- Makefile | 4 ++ app/_import.py | 42 +++++++++++++++----- app/api.py | 9 +++-- app/config.py | 20 ++++++++++ app/indexing.py | 40 +++++++++++++++++-- app/openfoodfacts.py | 34 +++++++++++++++- app/postprocessing.py | 29 +++++++++----- app/query.py | 37 ++++++++++-------- app/taxonomy.py | 46 +++++++++++++++------- app/taxonomy_es.py | 6 +-- data/config/openfoodfacts.yml | 1 + tests/int/data/test_off.yml | 1 + tests/int/helpers.py | 9 +++++ tests/int/test_completion.py | 73 +++++++++++++++++++++++++++++++++++ 14 files changed, 291 insertions(+), 60 deletions(-) create mode 100644 tests/int/test_completion.py diff --git a/Makefile b/Makefile index 670e422f..f0f0c7be 100644 --- a/Makefile +++ b/Makefile @@ -176,6 +176,10 @@ build-translations: @echo "🔎 Building translations …" ${DOCKER_COMPOSE} run --rm search_nodejs npm run translations:build +cleanup-indexes: + @echo "🔎 Cleaning indexes …" + ${DOCKER_COMPOSE} run --rm api python3 -m app cleanup-indexes ${args} + generate-openapi: _ensure_network @echo "🔎 Generating OpenAPI spec …" ${DOCKER_COMPOSE} run --rm api python3 -m app export-openapi /opt/search/data/searchalicious-openapi.yml diff --git a/app/_import.py b/app/_import.py index 7789af50..51fc49fd 100644 --- a/app/_import.py +++ b/app/_import.py @@ -12,8 +12,9 @@ from redis import Redis from app._types import FetcherResult, FetcherStatus, JSONType -from app.config import Config, IndexConfig, TaxonomyConfig, settings +from app.config import Config, IndexConfig, settings from app.indexing import ( + BaseTaxonomyPreprocessor, DocumentProcessor, generate_index_object, generate_taxonomy_index_object, @@ -252,7 +253,7 @@ def gen_documents( def gen_taxonomy_documents( - taxonomy_config: TaxonomyConfig, next_index: str, supported_langs: set[str] + config: IndexConfig, next_index: str, supported_langs: set[str] ): """Generator for taxonomy documents in Elasticsearch. @@ -261,26 +262,49 @@ def gen_taxonomy_documents( :param supported_langs: a set of supported languages :yield: a dict with the document to index, compatible with ES bulk API """ - for taxonomy_name, taxonomy in tqdm.tqdm(iter_taxonomies(taxonomy_config)): + taxonomy_config = config.taxonomy + preprocessor: BaseTaxonomyPreprocessor | None = None + if taxonomy_config.preprocessor: + preprocessor_cls = load_class_object_from_string(taxonomy_config.preprocessor) + preprocessor = preprocessor_cls(config) + for taxonomy in tqdm.tqdm(iter_taxonomies(taxonomy_config)): for node in taxonomy.iter_nodes(): + if preprocessor: + result = preprocessor.preprocess(taxonomy, node) + if result.status != FetcherStatus.FOUND or result.node is None: + continue # skip this entry + node = result.node names = { lang: lang_names for lang, lang_names in node.names.items() if lang in supported_langs } - synonyms = { - lang: lang_names - for lang, lang_names in node.synonyms.items() + synonyms: dict[str, set[str]] = { + lang: set(node.synonyms.get(lang) or []) + for lang in node.synonyms if lang in supported_langs } + for lang, lang_names in names.items(): + if lang_names: + if not isinstance(lang_names, str): + import pdb + + pdb.set_trace() + synonyms.setdefault(lang, set()).add(lang_names) yield { "_index": next_index, "_source": { "id": node.id, - "taxonomy_name": taxonomy_name, + "taxonomy_name": taxonomy.name, "name": names, - "synonyms": synonyms, + "synonyms": { + lang: { + "input": list(lang_synonyms), + "weight": max(100 - len(node.id), 0), + } + for lang, lang_synonyms in synonyms.items() + }, }, } @@ -370,7 +394,7 @@ def import_taxonomies(config: IndexConfig, next_index: str): success, errors = bulk( es, gen_taxonomy_documents( - config.taxonomy, next_index, supported_langs=set(config.supported_langs) + config, next_index, supported_langs=set(config.supported_langs) ), raise_on_error=False, ) diff --git a/app/api.py b/app/api.py index 6f6fcc47..ed09c7ef 100644 --- a/app/api.py +++ b/app/api.py @@ -149,8 +149,11 @@ def taxonomy_autocomplete( description="Name(s) of the taxonomy to search in, as a comma-separated value." ), ], - lang: Annotated[ - str, Query(description="Language to search in, defaults to 'en'.") + langs: Annotated[ + str, + Query( + description="Languages to search in (as a comma separated list), defaults to 'en'." + ), ] = "en", size: Annotated[int, Query(description="Number of results to return.")] = 10, fuzziness: Annotated[ @@ -167,7 +170,7 @@ def taxonomy_autocomplete( query = build_completion_query( q=q, taxonomy_names=taxonomy_names_list, - lang=lang, + langs=langs.split(","), size=size, config=index_config, fuzziness=fuzziness, diff --git a/app/config.py b/app/config.py index f820099b..c381d87b 100644 --- a/app/config.py +++ b/app/config.py @@ -510,6 +510,26 @@ class TaxonomyConfig(BaseModel): TaxonomyIndexConfig, Field(description=TaxonomyIndexConfig.__doc__), ] + preprocessor: ( + Annotated[ + str, + Field( + description=cd_( + """The full qualified reference to the preprocessor + to use before taxonomy entry import. + + This class must inherit `app.indexing.BaseTaxonomyPreprocessor` + and specialize the `preprocess` method. + + This is used to adapt the taxonomy schema + or to add specific fields for example. + """ + ), + examples=["app.openfoodfacts.TaxonomyPreprocessor"], + ), + ] + | None + ) = None class ScriptConfig(BaseModel): diff --git a/app/indexing.py b/app/indexing.py index 9d09f776..6a3050fd 100644 --- a/app/indexing.py +++ b/app/indexing.py @@ -9,12 +9,12 @@ from app._types import FetcherResult, FetcherStatus, JSONType from app.config import ( ANALYZER_LANG_MAPPING, - Config, FieldConfig, FieldType, IndexConfig, TaxonomyConfig, ) +from app.taxonomy import Taxonomy, TaxonomyNode, TaxonomyNodeResult from app.utils import load_class_object_from_string from app.utils.analyzers import ( get_autocomplete_analyzer, @@ -104,8 +104,41 @@ def preprocess_field_value( return input_value +class BaseTaxonomyPreprocessor(abc.ABC): + """Base class for taxonomy entries preprocessors. + + Classes referenced in index configuration `preprocess` field, + has to be derived from it. + """ + + def __init__(self, config: IndexConfig) -> None: + self.config = config + + @abc.abstractmethod + def preprocess(self, taxonomy: Taxonomy, node: TaxonomyNode) -> TaxonomyNodeResult: + """Preprocess the taxonomy entry before ingestion in Elasticsearch, + and before synonyms generation + + This can be used to make document schema compatible with the project + schema or to add custom fields. + + :return: a TaxonomyNodeResult object: + + * the status can be used to pilot wether + to index or not the entry (even delete it) + * the entry is the transformed entry + """ + pass + + class BaseDocumentPreprocessor(abc.ABC): - def __init__(self, config: Config) -> None: + """Base class for document preprocessors. + + Classes referenced in index configuration `preprocess` field, + has to be derived from it. + """ + + def __init__(self, config: IndexConfig) -> None: self.config = config @abc.abstractmethod @@ -119,7 +152,7 @@ def preprocess(self, document: JSONType) -> FetcherResult: * the status can be used to pilot wether to index or not the document (even delete it) - * the document is the document transformed document + * the document is the transformed document """ pass @@ -379,6 +412,7 @@ def generate_taxonomy_mapping_object(config: IndexConfig) -> Mapping: "type": "category", } ], + preserve_separators=False, # help match plurals ) for lang in supported_langs }, diff --git a/app/openfoodfacts.py b/app/openfoodfacts.py index 1a4adc6e..8b3e1fd1 100644 --- a/app/openfoodfacts.py +++ b/app/openfoodfacts.py @@ -7,8 +7,9 @@ from app._import import BaseDocumentFetcher from app._types import FetcherResult, FetcherStatus, JSONType -from app.indexing import BaseDocumentPreprocessor +from app.indexing import BaseDocumentPreprocessor, BaseTaxonomyPreprocessor from app.postprocessing import BaseResultProcessor +from app.taxonomy import Taxonomy, TaxonomyNode, TaxonomyNodeResult from app.utils.download import http_session from app.utils.log import get_logger @@ -87,6 +88,37 @@ def generate_image_url(code: str, image_id: str) -> str: OFF_API_URL = os.environ.get("OFF_API_URL", "https://world.openfoodfacts.org") +class TaxonomyPreprocessor(BaseTaxonomyPreprocessor): + """Preprocessor for Open Food Facts taxonomies.""" + + def preprocess(self, taxonomy: Taxonomy, node: TaxonomyNode) -> TaxonomyNodeResult: + """Preprocess a taxonomy node, + + We add the main language, and we also have specificities for some taxonomies + """ + if taxonomy.name == "brands": + # brands are english only, put them in "main lang" + node.names.update(main=node.names["en"]) + if node.synonyms and (synonyms_en := list(node.synonyms.get("en", []))): + node.synonyms.update(main=synonyms_en) + else: + # main language is entry id prefix + eventual xx entries + id_lang = node.id.split(":")[0] + if node_names := node.names.get(id_lang): + node.names.update(main=node_names) + node.synonyms.update(main=list(node.synonyms.get(id_lang, []))) + # add eventual xx entries as synonyms to all languages + xx_name = node.names.get("xx") + xx_names = [xx_name] if xx_name else [] + xx_names += node.synonyms.get("xx", []) + if xx_names: + for lang in self.config.supported_langs: + node.names.setdefault(lang, xx_names[0]) + lang_synonyms = node.synonyms.setdefault(lang, []) + lang_synonyms += xx_names + return TaxonomyNodeResult(status=FetcherStatus.FOUND, node=node) + + class DocumentFetcher(BaseDocumentFetcher): def fetch_document(self, stream_name: str, item: JSONType) -> FetcherResult: if item.get("action") == "deleted": diff --git a/app/postprocessing.py b/app/postprocessing.py index 40d0f87a..35c00ff1 100644 --- a/app/postprocessing.py +++ b/app/postprocessing.py @@ -65,13 +65,24 @@ def load_result_processor(config: IndexConfig) -> BaseResultProcessor | None: def process_taxonomy_completion_response(response: Response) -> JSONType: output = {"took": response.took, "timed_out": response.timed_out} options = [] - suggestion = response.suggest["taxonomy_suggest"][0] - for option in suggestion.options: - result = { - "id": option._source["id"], - "text": option.text, - "taxonomy_name": option._source["taxonomy_name"], - } - options.append(result) - output["options"] = options + ids = set() + for suggestion_id in dir(response.suggest): + if not suggestion_id.startswith("taxonomy_suggest_"): + continue + for suggestion in getattr(response.suggest, suggestion_id): + for option in suggestion.options: + if option._source["id"] in ids: + continue + ids.add(option._source["id"]) + result = { + "id": option._source["id"], + "text": option.text, + "score": option._score, + "taxonomy_name": option._source["taxonomy_name"], + } + options.append(result) + # highest score first + output["options"] = sorted( + options, key=lambda option: option["score"], reverse=True + ) return output diff --git a/app/query.py b/app/query.py index b5299902..d4555d2f 100644 --- a/app/query.py +++ b/app/query.py @@ -322,7 +322,7 @@ def build_es_query( def build_completion_query( q: str, taxonomy_names: list[str], - lang: str, + langs: list[str], size: int, config: IndexConfig, fuzziness: int | None = 2, @@ -331,28 +331,31 @@ def build_completion_query( :param q: the user autocomplete query :param taxonomy_names: a list of taxonomies we want to search in - :param lang: the language we want search in + :param langs: the language we want search in :param size: number of results to return :param config: the index configuration to use :param fuzziness: fuzziness parameter for completion query :return: the built Query """ - - completion_clause = { - "field": f"synonyms.{lang}", - "size": size, - "contexts": {"taxonomy_name": taxonomy_names}, - } - - if fuzziness is not None: - completion_clause["fuzzy"] = {"fuzziness": fuzziness} - query = Search(index=config.taxonomy.index.name) - query = query.suggest( - "taxonomy_suggest", - q, - completion=completion_clause, - ) + # import pdb;pdb.set_trace(); + for lang in langs: + completion_clause = { + "field": f"synonyms.{lang}", + "size": size, + "contexts": {"taxonomy_name": taxonomy_names}, + "skip_duplicates": True, + } + if fuzziness is not None: + completion_clause["fuzzy"] = {"fuzziness": fuzziness} + + query = query.suggest( + f"taxonomy_suggest_{lang}", + q, + completion=completion_clause, + ) + # limit returned fields + # query.source(fields=["id", "taxonomy_name"]) return query diff --git a/app/taxonomy.py b/app/taxonomy.py index e1face38..46f7222b 100644 --- a/app/taxonomy.py +++ b/app/taxonomy.py @@ -9,8 +9,9 @@ import cachetools import requests +from pydantic import BaseModel, ConfigDict -from app._types import JSONType +from app._types import FetcherStatus, JSONType from app.config import TaxonomyConfig, settings from app.utils import get_logger from app.utils.download import download_file, http_session, should_download_file @@ -157,8 +158,9 @@ class Taxonomy: node identifier to a `TaxonomyNode`. """ - def __init__(self) -> None: + def __init__(self, name: str) -> None: self.nodes: Dict[str, TaxonomyNode] = {} + self.name = name def add(self, key: str, node: TaxonomyNode) -> None: """Add a node to the taxonomy under the id `key`. @@ -263,13 +265,13 @@ def to_dict(self) -> JSONType: return export @classmethod - def from_dict(cls, data: JSONType) -> "Taxonomy": + def from_dict(cls, name: str, data: JSONType) -> "Taxonomy": """Create a Taxonomy from `data`. :param data: the taxonomy as a dict :return: a Taxonomy """ - taxonomy = Taxonomy() + taxonomy = Taxonomy(name) for key, key_data in data.items(): if key not in taxonomy: @@ -293,17 +295,21 @@ def from_dict(cls, data: JSONType) -> "Taxonomy": return taxonomy @classmethod - def from_path(cls, file_path: Union[str, Path]) -> "Taxonomy": + def from_path(cls, name: str, file_path: Union[str, Path]) -> "Taxonomy": """Create a Taxonomy from a JSON file. :param file_path: a JSON file, gzipped (.json.gz) files are supported :return: a Taxonomy """ - return cls.from_dict(load_json(file_path)) # type: ignore + return cls.from_dict(name, load_json(file_path)) # type: ignore @classmethod def from_url( - cls, url: str, session: Optional[requests.Session] = None, timeout: int = 120 + cls, + name: str, + url: str, + session: Optional[requests.Session] = None, + timeout: int = 120, ) -> "Taxonomy": """Create a Taxonomy from a taxonomy file hosted at `url`. @@ -315,7 +321,7 @@ def from_url( session = http_session if session is None else session r = session.get(url, timeout=timeout) data = r.json() - return cls.from_dict(data) + return cls.from_dict(name, data) @cachetools.cached(cachetools.TTLCache(maxsize=100, ttl=3600)) @@ -345,7 +351,7 @@ def get_taxonomy( fpath = taxonomy_url[len("file://") :] if not fpath.startswith("/"): raise RuntimeError("Relative path (not yet) supported for taxonomy url") - return Taxonomy.from_path(fpath.rstrip("/")) + return Taxonomy.from_path(taxonomy_name, fpath.rstrip("/")) filename = f"{taxonomy_name}.json" cache_dir = DEFAULT_CACHE_DIR if cache_dir is None else cache_dir @@ -354,16 +360,26 @@ def get_taxonomy( if not should_download_file( taxonomy_url, taxonomy_path, force_download, download_newer ): - return Taxonomy.from_path(taxonomy_path) + return Taxonomy.from_path(taxonomy_name, taxonomy_path) cache_dir.mkdir(parents=True, exist_ok=True) logger.info("Downloading taxonomy, saving it in %s", taxonomy_path) download_file(taxonomy_url, taxonomy_path) - return Taxonomy.from_path(taxonomy_path) + return Taxonomy.from_path(taxonomy_name, taxonomy_path) -def iter_taxonomies(taxonomy_config: TaxonomyConfig) -> Iterator[tuple[str, Taxonomy]]: +def iter_taxonomies(taxonomy_config: TaxonomyConfig) -> Iterator[Taxonomy]: for taxonomy_source_config in taxonomy_config.sources: - yield taxonomy_source_config.name, get_taxonomy( - taxonomy_source_config.name, str(taxonomy_source_config.url) - ) + yield get_taxonomy(taxonomy_source_config.name, str(taxonomy_source_config.url)) + + +class TaxonomyNodeResult(BaseModel): + """Result for a taxonomy node transformation. + + This is used to eventually skip entry after preprocessing + """ + + model_config = ConfigDict(arbitrary_types_allowed=True) + + status: FetcherStatus + node: TaxonomyNode | None diff --git a/app/taxonomy_es.py b/app/taxonomy_es.py index a8f713e4..89e293a5 100644 --- a/app/taxonomy_es.py +++ b/app/taxonomy_es.py @@ -94,10 +94,10 @@ def create_synonyms_files(taxonomy: Taxonomy, langs: list[str], target_dir: Path def create_synonyms(index_config: IndexConfig, target_dir: Path): - for name, taxonomy in iter_taxonomies(index_config.taxonomy): - target = target_dir / name + for taxonomy in iter_taxonomies(index_config.taxonomy): + target = target_dir / taxonomy.name # a temporary directory, we move at the end - target_tmp = target_dir / f"{name}.tmp" + target_tmp = target_dir / f"{taxonomy.name}.tmp" shutil.rmtree(target_tmp, ignore_errors=True) # ensure directory os.makedirs(target_tmp, mode=0o775, exist_ok=True) diff --git a/data/config/openfoodfacts.yml b/data/config/openfoodfacts.yml index a12d3b73..13d39535 100644 --- a/data/config/openfoodfacts.yml +++ b/data/config/openfoodfacts.yml @@ -160,6 +160,7 @@ indices: primary_color: "#341100" accent_color: "#ff8714" taxonomy: + preprocessor: app.openfoodfacts.TaxonomyPreprocessor sources: - name: categories url: https://static.openfoodfacts.org/data/taxonomies/categories.full.json diff --git a/tests/int/data/test_off.yml b/tests/int/data/test_off.yml index 3a17f8ef..9800e470 100644 --- a/tests/int/data/test_off.yml +++ b/tests/int/data/test_off.yml @@ -50,6 +50,7 @@ indices: primary_color: "#341100" accent_color: "#ff8714" taxonomy: + preprocessor: tests.int.helpers.TestTaxonomyPreprocessor sources: - name: categories url: file:///opt/search/tests/int/data/test_categories.full.json diff --git a/tests/int/helpers.py b/tests/int/helpers.py index 983811c6..35573e48 100644 --- a/tests/int/helpers.py +++ b/tests/int/helpers.py @@ -5,7 +5,9 @@ from app._import import BaseDocumentFetcher from app._types import FetcherResult, FetcherStatus, JSONType from app.indexing import BaseDocumentPreprocessor +from app.openfoodfacts import TaxonomyPreprocessor from app.postprocessing import BaseResultProcessor +from app.taxonomy import Taxonomy, TaxonomyNode, TaxonomyNodeResult class CallRegistration: @@ -41,6 +43,13 @@ def get_calls(cls): return calls +class TestTaxonomyPreprocessor(TaxonomyPreprocessor, CallRegistration): + + def preprocess(self, taxonomy: Taxonomy, node: TaxonomyNode) -> TaxonomyNodeResult: + self.register_call((taxonomy.name, node.id)) + return super().preprocess(taxonomy, node) + + class TestDocumentFetcher(BaseDocumentFetcher, CallRegistration): def fetch_document(self, stream_name: str, item: JSONType) -> FetcherResult: diff --git a/tests/int/test_completion.py b/tests/int/test_completion.py new file mode 100644 index 00000000..75eb022e --- /dev/null +++ b/tests/int/test_completion.py @@ -0,0 +1,73 @@ +import pytest + + +@pytest.mark.parametrize( + "q,taxonomies,langs,results", + [ + # simple + ("organ", "labels", "en", [("en:organic", "Organic", 90)]), + # no case match + ("ORGAN", "labels", "en", [("en:organic", "Organic", 90)]), + # french + ("biol", "labels", "fr", [("en:organic", "biologique", 90)]), + # multiple languages + ("biol", "labels", "en,fr", [("en:organic", "biologique", 90)]), + # xx added to french + ("Max H", "labels", "fr", [("en:max-havelaar", "Max Havelaar", 85)]), + # main for an entry without french + ( + "Fairtrade/Max H", + "labels", + "fr,main", + [("en:max-havelaar", "Fairtrade/Max Havelaar", 85)], + ), + # multiple taxonomies + ( + "fr", + "labels,categories", + "en", + [ + ("en:organic", "From Organic Agriculture", 90), + ("en:fr-bio-01", "FR-BIO-01", 88), + ("en:no-artificial-flavors", "free of artificial flavor", 76), + ( + "en:fruits-and-vegetables-based-foods", + "Fruits and vegetables based foods", + 64, + ), + ], + ), + # different answers + ( + "b", + "categories", + "en", + [ + ("en:biscuits", "biscuit", 89), + ("en:beverages", "Beverages", 88), + ("en:chocolate-biscuits", "Biscuit with chocolate", 79), + ("en:biscuits-and-cakes", "Biscuits and cakes", 79), + ("en:sweetened-beverages", "Beverages with added sugar", 78), + ], + ), + ], +) +def test_completion(q, taxonomies, langs, results, test_client, synonyms_created): + response = test_client.get( + f"/autocomplete?q={q}&langs={langs}&taxonomy_names={taxonomies}&size=5" + ) + assert response.status_code == 200 + options = response.json()["options"] + assert len(options) == len(results) + # only requested taxonomies + result_taxonomies = set([option["taxonomy_name"] for option in options]) + assert result_taxonomies <= set(taxonomies.split(",")) + # well sorted + assert sorted([option["score"] for option in options], reverse=True) == [ + option["score"] for option in options + ] + # expected results + completions = [ + (option["id"], option["text"], int(option["score"])) for option in options + ] + assert completions == results From 2b8ef15aeda96c958be200a74fe4e297825a7fa7 Mon Sep 17 00:00:00 2001 From: Alex Garel Date: Fri, 25 Oct 2024 18:38:18 +0200 Subject: [PATCH 02/23] wip: on updating suggestion web-component --- frontend/public/off.html | 4 +- frontend/src/mixins/search-ctl.ts | 18 --- frontend/src/mixins/suggestion-selection.ts | 118 +++++++++------ frontend/src/mixins/suggestions-ctl.ts | 2 +- frontend/src/search-a-licious.ts | 1 + frontend/src/search-autocomplete.ts | 21 ++- frontend/src/search-bar.ts | 89 ++++++----- frontend/src/search-facets.ts | 2 + frontend/src/search-suggester.ts | 154 ++++++++++++++++++++ frontend/src/search-suggestion-entry.ts | 31 +--- 10 files changed, 306 insertions(+), 134 deletions(-) create mode 100644 frontend/src/search-suggester.ts diff --git a/frontend/public/off.html b/frontend/public/off.html index cda33377..895209fc 100644 --- a/frontend/public/off.html +++ b/frontend/public/off.html @@ -286,7 +286,9 @@
  • - + + +
  • diff --git a/frontend/src/mixins/search-ctl.ts b/frontend/src/mixins/search-ctl.ts index 437bc3ea..33b22fde 100644 --- a/frontend/src/mixins/search-ctl.ts +++ b/frontend/src/mixins/search-ctl.ts @@ -67,7 +67,6 @@ export interface SearchaliciousSearchInterface _facetsNodes(): SearchaliciousFacets[]; _facetsFilters(): string; resetFacets(launchSearch?: boolean): void; - selectTermByTaxonomy(taxonomy: string, term: string): void; } export const SearchaliciousSearchMixin = >( @@ -208,23 +207,6 @@ export const SearchaliciousSearchMixin = >( ); } - /** - * Select a term by taxonomy in all facets - * It will update the selected terms in facets - * @param taxonomy - * @param term - */ - selectTermByTaxonomy(taxonomy: string, term: string) { - for (const facets of this._facetsParentNode()) { - // if true, the facets has been updated - if ( - (facets as SearchaliciousFacets).selectTermByTaxonomy(taxonomy, term) - ) { - return; - } - } - } - /** * @returns the sort element linked to this search ctl */ diff --git a/frontend/src/mixins/suggestion-selection.ts b/frontend/src/mixins/suggestion-selection.ts index a7c81095..152b014c 100644 --- a/frontend/src/mixins/suggestion-selection.ts +++ b/frontend/src/mixins/suggestion-selection.ts @@ -1,6 +1,6 @@ import {LitElement} from 'lit'; import {Constructor} from './utils'; -import {property} from 'lit/decorators.js'; +import {property, state} from 'lit/decorators.js'; import {DebounceMixin, DebounceMixinInterface} from './debounce'; /** @@ -10,39 +10,58 @@ export interface SuggestionSelectionMixinInterface extends DebounceMixinInterface { inputName: string; options: SuggestionSelectionOption[]; - value: string; - currentIndex: number; - getOptionIndex: number; + selectedOption: SuggestionSelectionOption | undefined; visible: boolean; isLoading: boolean; - currentOption: SuggestionSelectionOption | undefined; + currentIndex: number; onInput(event: InputEvent): void; handleInput(value: string): void; blurInput(): void; - resetInput(): void; - submit(isSuggestion?: boolean): void; + resetInput(selectedOption?: SuggestionSelectionOption): void; + submitSuggestion(isSuggestion?: boolean): void; handleArrowKey(direction: 'up' | 'down'): void; handleEnter(event: KeyboardEvent): void; handleEscape(): void; onKeyDown(event: KeyboardEvent): void; - onClick(index: number): () => void; + onClick(option: SuggestionSelectionOption): () => void; onFocus(): void; onBlur(): void; } + /** * Type for suggestion option. */ export type SuggestionSelectionOption = { + /** + * value assigned to this suggestion + */ value: string; + /** + * Label to display this suggestion + */ label: string; + /** + * Unique id for this suggestion + * + * It is important when we have multiple suggestions sources + */ + id: string; + /** + * text that gave the suggestion + * + * It is important because we might do a suggestion on part of the searched terms + */ + input: string; }; + /** * Type for suggestion result. */ export type SuggestionSelectionResult = { value: string; label?: string; + id: string; }; /** @@ -65,51 +84,59 @@ export const SuggestionSelectionMixin = >( @property({attribute: false, type: Array}) options: SuggestionSelectionOption[] = []; - // selected values - @property() - value = ''; - - @property({attribute: false}) + /** + * index of suggestion about to be selected + * + * It mainly tracks current focused suggestion + * when navigating with arrow keys + */ + @state() currentIndex = 0; + // track current input value + @state() + inputValue = ''; + + /** + * The option that was selected + * + * Note that it might be from outside the options list (for specific inputs) + */ + selectedOption: SuggestionSelectionOption | undefined; + @property({attribute: false}) visible = false; @property({attribute: false}) isLoading = false; - /** - * This method is used to get the current index. - * It remove the offset of 1 because the currentIndex is 1-based. - * @returns {number} The current index. - */ - get getOptionIndex() { - return this.currentIndex - 1; - } - - get currentOption() { - return this.options[this.getOptionIndex]; - } - getInput() { return this.shadowRoot!.querySelector('input'); } /** - * Handles the input event on the suggestion and dispatch custom event : "suggestion-input". + * Handles the input event on the suggestion, that is when the option is suggested + * and dispatch to handleInput * @param {InputEvent} event - The input event. */ onInput(event: InputEvent) { const value = (event.target as HTMLInputElement).value; - this.value = value; + this.inputValue = value; this.handleInput(value); } + /** + * React on changing input + * + * This method is to be implemented by the component to ask for new suggestions based on the input value. + * @param value the current input value + */ handleInput(value: string) { throw new Error( `handleInput method must be implemented for ${this} with ${value}` ); } + /** * This method is used to remove focus from the input element. * It is used to quit after selecting an option. @@ -125,14 +152,18 @@ export const SuggestionSelectionMixin = >( * This method is used to reset the input value and blur it. * It is used to reset the input after a search. */ - resetInput() { - this.value = ''; + resetInput(selectedOption?: SuggestionSelectionOption) { this.currentIndex = 0; const input = this.getInput(); if (!input) { return; } - input.value = ''; + // remove part of the text that generates the selection + if (selectedOption) { + input.value = input.value.replace(selectedOption?.input || '', ''); + } else { + input.value = ''; + } this.blurInput(); } @@ -141,7 +172,7 @@ export const SuggestionSelectionMixin = >( * It is used to submit the input value after selecting an option. * @param {boolean} isSuggestion - A boolean value to check if the value is a suggestion. */ - submit(isSuggestion = false) { + submitSuggestion(isSuggestion = false) { throw new Error( `submit method must be implemented for ${this} with ${isSuggestion}` ); @@ -168,12 +199,18 @@ export const SuggestionSelectionMixin = >( let isSuggestion = false; if (this.currentIndex) { isSuggestion = true; - this.value = this.currentOption!.value; + this.selectedOption = this.options[this.currentIndex - 1]; } else { + // direct suggestion const value = (event.target as HTMLInputElement).value; - this.value = value; + this.selectedOption = { + value: value, + label: value, + id: '--direct-suggestion--' + value, + input: value, + }; } - this.submit(isSuggestion); + this.submitSuggestion(isSuggestion); } /** @@ -206,14 +243,13 @@ export const SuggestionSelectionMixin = >( /** * On a click on the suggestion option, we select it as value and submit it. - * @param index + * @param option - chosen option */ - onClick(index: number) { + onClick(option: SuggestionSelectionOption) { return () => { - this.value = this.options[index].value; - // we need to increment the index because currentIndex is 1-based - this.currentIndex = index + 1; - this.submit(true); + this.selectedOption = option; + this.currentIndex = 0; + this.submitSuggestion(true); }; } diff --git a/frontend/src/mixins/suggestions-ctl.ts b/frontend/src/mixins/suggestions-ctl.ts index e74079e6..7f32acfb 100644 --- a/frontend/src/mixins/suggestions-ctl.ts +++ b/frontend/src/mixins/suggestions-ctl.ts @@ -72,7 +72,7 @@ export const SearchaliciousTermsMixin = >( */ _termsUrl(q: string, taxonomyNames: string[]) { const baseUrl = this.taxonomiesBaseUrl.replace(/\/+$/, ''); - return `${baseUrl}/autocomplete?q=${q}&lang=${this.langs}&taxonomy_names=${taxonomyNames}&size=5`; + return `${baseUrl}/autocomplete?q=${q}&langs=${this.langs}&taxonomy_names=${taxonomyNames}&size=5`; } /** diff --git a/frontend/src/search-a-licious.ts b/frontend/src/search-a-licious.ts index ac6d0fa8..3d21d500 100644 --- a/frontend/src/search-a-licious.ts +++ b/frontend/src/search-a-licious.ts @@ -4,6 +4,7 @@ export {SearchaliciousCheckbox} from './search-checkbox'; export {SearchaliciousRadio} from './search-radio'; export {SearchaliciousToggle} from './search-toggle'; export {SearchaliciousBar} from './search-bar'; +export {SearchaliciousTaxonomySuggester} from './search-suggester'; export {SearchaliciousButton} from './search-button'; export {SearchaliciousPages} from './search-pages'; export {SearchaliciousFacets} from './search-facets'; diff --git a/frontend/src/search-autocomplete.ts b/frontend/src/search-autocomplete.ts index f380baba..08dba06f 100644 --- a/frontend/src/search-autocomplete.ts +++ b/frontend/src/search-autocomplete.ts @@ -87,7 +87,7 @@ export class SearchaliciousAutocomplete extends SuggestionSelectionMixin( ? this.options.map( (option, index) => html`
  • ${option.label}
  • ` @@ -118,23 +118,24 @@ export class SearchaliciousAutocomplete extends SuggestionSelectionMixin( * It is used to submit the input value after selecting an option. * @param {boolean} isSuggestion - A boolean value to check if the value is a suggestion or a free input from the user. */ - override submit(isSuggestion = false) { - if (!this.value) return; + override submitSuggestion(isSuggestion = false) { + const selectedOption = this.selectedOption; + if (!selectedOption) return; const inputEvent = new CustomEvent( SearchaliciousEvents.AUTOCOMPLETE_SUBMIT, { // we send both value and label detail: { - value: this.value, - label: isSuggestion ? this.currentOption!.label : undefined, + value: selectedOption.value, + label: isSuggestion ? selectedOption!.label : undefined, } as SuggestionSelectionResult, bubbles: true, composed: true, } ); this.dispatchEvent(inputEvent); - this.resetInput(); + this.resetInput(selectedOption); } /** @@ -148,14 +149,18 @@ export class SearchaliciousAutocomplete extends SuggestionSelectionMixin( type="text" name="${this.inputName}" id="${this.inputName}" - .value=${this.value} + .value=${this.selectedOption?.value || ''} @input=${this.onInput} @keydown=${this.onKeyDown} autocomplete="off" @focus=${this.onFocus} @blur=${this.onBlur} /> -
      +
        ${this.isLoading ? html`
      • ${msg('Loading...')}
      • ` : this._renderSuggestions()} diff --git a/frontend/src/search-bar.ts b/frontend/src/search-bar.ts index c5321094..c640d6fc 100644 --- a/frontend/src/search-bar.ts +++ b/frontend/src/search-bar.ts @@ -3,10 +3,13 @@ import {customElement, property} from 'lit/decorators.js'; import {SearchaliciousSearchMixin} from './mixins/search-ctl'; import {localized, msg} from '@lit/localize'; import {setLocale} from './localization/main'; -import {SearchaliciousTermsMixin} from './mixins/suggestions-ctl'; import {SuggestionSelectionMixin} from './mixins/suggestion-selection'; +import { + SearchaliciousSuggester, + SuggestOption, + SuggestersSelector, +} from './search-suggester'; import {classMap} from 'lit/directives/class-map.js'; -import {removeLangFromTermId} from './utils/taxonomies'; import {searchBarInputAndButtonStyle} from './css/header'; import {SearchaliciousEvents} from './utils/enums'; import {isTheSameSearchName} from './utils/search'; @@ -20,7 +23,7 @@ import {isTheSameSearchName} from './utils/search'; @customElement('searchalicious-bar') @localized() export class SearchaliciousBar extends SuggestionSelectionMixin( - SearchaliciousTermsMixin(SearchaliciousSearchMixin(LitElement)) + SearchaliciousSearchMixin(LitElement) ) { static override styles = [ searchBarInputAndButtonStyle, @@ -83,6 +86,13 @@ export class SearchaliciousBar extends SuggestionSelectionMixin( `, ]; + /** + * The options for the suggestion. + * + * We redefine them to use SuggestOption + */ + @property({attribute: false, type: Array}) + override options: SuggestOption[] = []; /** * Placeholder attribute is stored in a private variable to be able to use the msg() function * it stores the placeholder attribute value if it is set @@ -90,12 +100,6 @@ export class SearchaliciousBar extends SuggestionSelectionMixin( */ private _placeholder?: string; - /** - * Taxonomies we want to use for suggestions - */ - @property({type: String, attribute: 'suggestions'}) - suggestions = ''; - /** * Place holder in search bar */ @@ -117,13 +121,6 @@ export class SearchaliciousBar extends SuggestionSelectionMixin( return this.isQueryChanged || this.isFacetsChanged; } - /** - * It parses the string suggestions attribute and returns an array - */ - get parsedSuggestions() { - return this.suggestions.split(','); - } - constructor() { super(); @@ -132,22 +129,44 @@ export class SearchaliciousBar extends SuggestionSelectionMixin( window.setLocale = setLocale; } + /** Return the list of suggesters contained in the search bar */ + get suggesters() { + const suggesters: SearchaliciousSuggester[] = []; + this.querySelectorAll(SuggestersSelector).forEach((element) => { + suggesters.push(element as SearchaliciousSuggester); + }); + return suggesters; + } + + /** Ask suggesters for suggested options */ + async getSuggestions(value: string) { + return Promise.allSettled( + this.suggesters.map((suggester) => { + return suggester.getSuggestions(value); + }) + ).then((optionsLists) => { + const options: SuggestOption[] = []; + optionsLists.forEach((result) => { + if (result.status === 'fulfilled' && result.value != null) { + options.push(...(result.value as SuggestOption[])); + } + }); + return options; + }); + } + /** * Handle the input event * It will update the query and call the getTaxonomiesTerms method to show suggestions * @param value */ override handleInput(value: string) { - this.value = value; this.query = value; this.updateSearchSignals(); this.debounce(() => { - this.getTaxonomiesTerms(value, this.parsedSuggestions).then(() => { - this.options = this.terms.map((term) => ({ - value: term.text, - label: term.text, - })); + this.getSuggestions(value).then((options) => { + this.options = options; }); }); } @@ -155,17 +174,15 @@ export class SearchaliciousBar extends SuggestionSelectionMixin( /** * Submit - might either be selecting a suggestion or submitting a search expression */ - override submit(isSuggestion?: boolean) { + override submitSuggestion(isSuggestion?: boolean) { // If the value is a suggestion, select the term and reset the input otherwise search if (isSuggestion) { - this.selectTermByTaxonomy( - this.terms[this.getOptionIndex].taxonomy_name, - removeLangFromTermId(this.terms[this.getOptionIndex].id) - ); - this.resetInput(); - this.query = ''; + const selectedOption = this.selectedOption as SuggestOption; + selectedOption!.source.selectSuggestion(selectedOption); + this.resetInput(this.selectedOption); + this.query = ''; // not sure if we should instead put the value of remaining input } else { - this.query = this.value; + this.query = this.selectedOption?.value || ''; this.blurInput(); } this.search(); @@ -176,21 +193,19 @@ export class SearchaliciousBar extends SuggestionSelectionMixin( */ renderSuggestions() { // Don't show suggestions if the input is not focused or the value is empty or there are no suggestions - if (!this.visible || !this.value || this.terms.length === 0) { + if (!this.visible || !this.selectedOption || this.options.length === 0) { return html``; } return html`
          - ${this.terms.map( - (term, index) => html` + ${this.options.map( + (option, index) => html`
        • - + ${option.source.renderSuggestion(option, index)}
        • ` )} diff --git a/frontend/src/search-facets.ts b/frontend/src/search-facets.ts index cd6a2c86..fd00bb61 100644 --- a/frontend/src/search-facets.ts +++ b/frontend/src/search-facets.ts @@ -382,6 +382,8 @@ export class SearchaliciousTermsFacet extends SearchActionMixin( return { value: removeLangFromTermId(term.id), label: term.text, + id: term.id, + input: term.text, }; }); diff --git a/frontend/src/search-suggester.ts b/frontend/src/search-suggester.ts new file mode 100644 index 00000000..e0a24088 --- /dev/null +++ b/frontend/src/search-suggester.ts @@ -0,0 +1,154 @@ +/** + * Search suggester is an element that should be placed inside a search-bar component. + * It indicates some suggestion that the search bar should apply + * + */ +import {LitElement, TemplateResult, html} from 'lit'; +import {customElement, property} from 'lit/decorators.js'; +import {SearchaliciousTermsMixin} from './mixins/suggestions-ctl'; +import {SearchaliciousSearchInterface} from './mixins/search-ctl'; +import {SuggestionSelectionOption} from './mixins/suggestion-selection'; +import {SearchaliciousFacets} from './search-facets'; +import {removeLangFromTermId} from './utils/taxonomies'; + +/** + * Type for a suggested option + */ +export type SuggestOption = SuggestionSelectionOption & { + /** + * source of this suggestion + */ + source: SearchaliciousSuggester; +}; + +export class SearchaliciousSuggester extends LitElement { + /** + * Query for options to suggest for value and return them + */ + async getSuggestions(_value: string): Promise { + throw new Error('Not implemented, implement in child class'); + } + + /** + * Select a suggestion + */ + async selectSuggestion(_selected: SuggestionSelectionOption) { + throw new Error('Not implemented, implement in child class'); + } + + /** + * Render a suggestion + */ + renderSuggestion(_suggestion: SuggestOption, _index: number): TemplateResult { + throw new Error('Not implemented, implement in child class'); + } +} + +/** + * An element to be used inside a searchalicious-bar component. + * It enables giving suggestions that are based upon taxonomies + */ +@customElement('searchalicious-taxonomy-suggester') +export class SearchaliciousTaxonomySuggester extends SearchaliciousTermsMixin( + SearchaliciousSuggester +) { + // TODO: suggestion should be by type + // for that we need for example to use slot by taxonomy where we put the value + // this would enable adding beautiful html selections + + /** + * Taxonomies we want to use for suggestions + */ + @property({type: String, attribute: 'taxonomies'}) + taxonomies = ''; + + /** + * Fuzziness level for suggestions + */ + @property({type: Number}) + fuzziness = 2; + + /** + * taxonomies attribute but as an array of String + */ + get taxonomiesList() { + return this.taxonomies.split(','); + } + + /** + * Return the search bar this element is contained in + */ + get searchBar(): SearchaliciousSearchInterface { + const searchBar = this.closest('searchalicious-bar'); + if (searchBar == null) { + throw new Error( + 'Taxonomy suggester must be contained in a searchalicious-bar' + ); + } + return searchBar; + } + + /** list of facets containers */ + _facetsParentNode() { + return document.querySelectorAll( + `searchalicious-facets[search-name=${this.searchBar.name}]` + ); + } + + /** + * Select a term by taxonomy in all facets + * It will update the selected terms in facets + * @param taxonomy + * @param term + */ + selectTermByTaxonomy(taxonomy: string, term: string) { + for (const facets of this._facetsParentNode()) { + // if true, the facets has been updated + if ( + (facets as SearchaliciousFacets).selectTermByTaxonomy(taxonomy, term) + ) { + return; + } + } + // TODO: handle the case of no facet found: replace expression with a condition on specific field + } + + /** + * Query for options to suggest + */ + override async getSuggestions(value: string): Promise { + return this.getTaxonomiesTerms(value, this.taxonomiesList).then(() => { + return this.terms.map((term) => ({ + value: removeLangFromTermId(term.id), + label: term.text, + id: term.taxonomy_name + '-' + term.id, + source: this, + input: value, + })); + }); + } + + override async selectSuggestion(selected: SuggestionSelectionOption) { + this.selectTermByTaxonomy(this.taxonomies, selected.value); + } + + override renderSuggestion( + suggestion: SuggestOption, + _index: number + ): TemplateResult { + return html``; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'searchalicious-taxonomy-suggest': SearchaliciousTaxonomySuggester; + } +} + +/** + * An expression to be able to get all suggesters using a querySelectorAll + */ +export const SuggestersSelector = 'searchalicious-taxonomy-suggest'; diff --git a/frontend/src/search-suggestion-entry.ts b/frontend/src/search-suggestion-entry.ts index f1fdf25e..0a169299 100644 --- a/frontend/src/search-suggestion-entry.ts +++ b/frontend/src/search-suggestion-entry.ts @@ -1,5 +1,6 @@ import {css, html, LitElement} from 'lit'; import {customElement, property} from 'lit/decorators.js'; +import {SuggestionSelectionOption} from './mixins/suggestion-selection'; /** * This component represent a suggestion to the user as he types his search. @@ -28,26 +29,12 @@ export class SearchaliciousSuggestionEntry extends LitElement { --img-size: var(--searchalicious-suggestion-entry-img-size, 2rem); } - .suggestion-entry .suggestion-entry-img-wrapper { - width: var(--img-size); - height: var(--img-size); - overflow: hidden; - } - .suggestion-entry .suggestion-entry-text-wrapper { --margin-left: 1rem; margin-left: var(--margin-left); width: calc(100% - var(--img-size) - var(--margin-left)); } - .suggestion-entry-img-wrapper > * { - width: 100%; - height: 100%; - object-fit: cover; - border-radius: 50%; - background-color: var(--img-background-color, #d9d9d9); - } - .suggestion-entry-text { font-weight: bold; text-overflow: ellipsis; @@ -58,12 +45,7 @@ export class SearchaliciousSuggestionEntry extends LitElement { `; @property({type: Object, attribute: 'term'}) - term?: { - imageUrl?: string; - id: string; - text: string; - taxonomy_name: string; - }; + term?: SuggestionSelectionOption; /** * We display the taxonomy term and corresponding filter name @@ -71,15 +53,8 @@ export class SearchaliciousSuggestionEntry extends LitElement { override render() { return html`
          -
          - - ${this.term?.imageUrl - ? html`` - : html`
          `} -
          -
          ${this.term?.text}
          -
          ${this.term?.taxonomy_name}
          +
          ${this.term?.label}
          `; From 9d47ab654d023a568856d8d92548322928e84ee9 Mon Sep 17 00:00:00 2001 From: Alex Garel Date: Mon, 28 Oct 2024 15:32:05 +0100 Subject: [PATCH 03/23] chore: add lit-element-context library --- frontend/package-lock.json | 310 +++++++++++++++++++++++++++++++------ frontend/package.json | 3 +- 2 files changed, 261 insertions(+), 52 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 0b14143a..4e89e1a7 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -12,7 +12,8 @@ "@lit-labs/preact-signals": "^1.0.2", "@lit/context": "^1.1.2", "@lit/localize": "^0.12.1", - "lit": "^3.0.0" + "lit": "^3.0.0", + "lit-element-context": "^0.2.3" }, "devDependencies": { "@custom-elements-manifest/analyzer": "^0.6.3", @@ -42,12 +43,13 @@ } }, "node_modules/@75lb/deep-merge": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@75lb/deep-merge/-/deep-merge-1.1.1.tgz", - "integrity": "sha512-xvgv6pkMGBA6GwdyJbNAnDmfAIR/DfWhrj9jgWh3TY7gRm3KO46x/GPjRg6wJ0nOepwqrNxFfojebh0Df4h4Tw==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@75lb/deep-merge/-/deep-merge-1.1.2.tgz", + "integrity": "sha512-08K9ou5VNbheZFxM5tDWoqjA3ImC50DiuuJ2tj1yEPRfkp8lLLg6XAaJ4On+a0yAXor/8ay5gHnAIshRM44Kpw==", "dev": true, + "license": "MIT", "dependencies": { - "lodash.assignwith": "^4.2.0", + "lodash": "^4.17.21", "typical": "^7.1.1" }, "engines": { @@ -2286,8 +2288,7 @@ "node_modules/@open-wc/dedupe-mixin": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@open-wc/dedupe-mixin/-/dedupe-mixin-1.4.0.tgz", - "integrity": "sha512-Sj7gKl1TLcDbF7B6KUhtvr+1UCxdhMbNY5KxdU5IfMFWqL8oy1ZeAcCANjoB1TL0AJTcPmcCFsCbHf8X2jGDUA==", - "dev": true + "integrity": "sha512-Sj7gKl1TLcDbF7B6KUhtvr+1UCxdhMbNY5KxdU5IfMFWqL8oy1ZeAcCANjoB1TL0AJTcPmcCFsCbHf8X2jGDUA==" }, "node_modules/@open-wc/dev-server-hmr": { "version": "0.1.2-next.0", @@ -3426,15 +3427,16 @@ } }, "node_modules/@web/test-runner-commands/node_modules/@web/dev-server-core": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/@web/dev-server-core/-/dev-server-core-0.7.1.tgz", - "integrity": "sha512-alHd2j0f4e1ekqYDR8lWScrzR7D5gfsUZq3BP3De9bkFWM3AELINCmqqlVKmCtlkAdEc9VyQvNiEqrxraOdc2A==", + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@web/dev-server-core/-/dev-server-core-0.7.3.tgz", + "integrity": "sha512-GS+Ok6HiqNZOsw2oEv5V2OISZ2s/6icJodyGjUuD3RChr0G5HiESbKf2K8mZV4shTz9sRC9KSQf8qvno2gPKrQ==", "dev": true, + "license": "MIT", "dependencies": { "@types/koa": "^2.11.6", "@types/ws": "^7.4.0", "@web/parse5-utils": "^2.1.0", - "chokidar": "^3.4.3", + "chokidar": "^4.0.1", "clone": "^2.1.2", "es-module-lexer": "^1.0.0", "get-stream": "^6.0.0", @@ -3448,7 +3450,7 @@ "mime-types": "^2.1.27", "parse5": "^6.0.1", "picomatch": "^2.2.2", - "ws": "^7.4.2" + "ws": "^7.5.10" }, "engines": { "node": ">=18.0.0" @@ -3459,6 +3461,7 @@ "resolved": "https://registry.npmjs.org/@web/parse5-utils/-/parse5-utils-2.1.0.tgz", "integrity": "sha512-GzfK5disEJ6wEjoPwx8AVNwUe9gYIiwc+x//QYxYDAFKUp4Xb1OJAGLc2l2gVrSQmtPGLKrTRcW90Hv4pEq1qA==", "dev": true, + "license": "MIT", "dependencies": { "@types/parse5": "^6.0.1", "parse5": "^6.0.1" @@ -3468,10 +3471,11 @@ } }, "node_modules/@web/test-runner-commands/node_modules/@web/test-runner-core": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/@web/test-runner-core/-/test-runner-core-0.13.1.tgz", - "integrity": "sha512-2hESALx/UFsAzK+ApWXAkFp2eCmwcs2yj1v4YjwV8F38sQumJ40P3px3BMjFwkOYDORtQOicW0RUeSw1g3BMLA==", + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/@web/test-runner-core/-/test-runner-core-0.13.4.tgz", + "integrity": "sha512-84E1025aUSjvZU1j17eCTwV7m5Zg3cZHErV3+CaJM9JPCesZwLraIa0ONIQ9w4KLgcDgJFw9UnJ0LbFf42h6tg==", "dev": true, + "license": "MIT", "dependencies": { "@babel/code-frame": "^7.12.11", "@types/babel__code-frame": "^7.0.2", @@ -3481,15 +3485,15 @@ "@types/istanbul-lib-coverage": "^2.0.3", "@types/istanbul-reports": "^3.0.0", "@web/browser-logs": "^0.4.0", - "@web/dev-server-core": "^0.7.0", - "chokidar": "^3.4.3", + "@web/dev-server-core": "^0.7.3", + "chokidar": "^4.0.1", "cli-cursor": "^3.1.0", "co-body": "^6.1.0", "convert-source-map": "^2.0.0", "debounce": "^1.2.0", "dependency-graph": "^0.11.0", "globby": "^11.0.1", - "ip": "^2.0.1", + "internal-ip": "^6.2.0", "istanbul-lib-coverage": "^3.0.0", "istanbul-lib-report": "^3.0.1", "istanbul-reports": "^3.0.2", @@ -3504,27 +3508,53 @@ "node": ">=18.0.0" } }, - "node_modules/@web/test-runner-commands/node_modules/es-module-lexer": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.0.tgz", - "integrity": "sha512-pqrTKmwEIgafsYZAGw9kszYzmagcE/n4dbgwGWLEXg7J4QFJVQRBld8j3Q3GNez79jzxZshq0bcT962QHOghjw==", - "dev": true + "node_modules/@web/test-runner-commands/node_modules/chokidar": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz", + "integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } }, - "node_modules/@web/test-runner-commands/node_modules/ip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.1.tgz", - "integrity": "sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ==", - "dev": true + "node_modules/@web/test-runner-commands/node_modules/es-module-lexer": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz", + "integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==", + "dev": true, + "license": "MIT" }, "node_modules/@web/test-runner-commands/node_modules/lru-cache": { "version": "8.0.5", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-8.0.5.tgz", "integrity": "sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA==", "dev": true, + "license": "ISC", "engines": { "node": ">=16.14" } }, + "node_modules/@web/test-runner-commands/node_modules/readdirp": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz", + "integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@web/test-runner-core": { "version": "0.10.29", "resolved": "https://registry.npmjs.org/@web/test-runner-core/-/test-runner-core-0.10.29.tgz", @@ -3905,12 +3935,13 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, + "license": "MIT", "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -4724,6 +4755,19 @@ "node": ">=0.10.0" } }, + "node_modules/default-gateway": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz", + "integrity": "sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "execa": "^5.0.0" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", @@ -5303,6 +5347,37 @@ "node": ">= 0.6" } }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, "node_modules/extract-zip": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", @@ -5412,10 +5487,11 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, + "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -5894,6 +5970,16 @@ "node": ">= 6" } }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -5985,6 +6071,25 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true }, + "node_modules/internal-ip": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/internal-ip/-/internal-ip-6.2.0.tgz", + "integrity": "sha512-D8WGsR6yDt8uq7vDMu7mjcR+yRMm3dW8yufyChmszWRjcSHuxLBkR3GdS2HZAjodsaGuCvXeEJpueisXJULghg==", + "dev": true, + "license": "MIT", + "dependencies": { + "default-gateway": "^6.0.0", + "ipaddr.js": "^1.9.1", + "is-ip": "^3.1.0", + "p-event": "^4.2.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/internal-ip?sponsor=1" + } + }, "node_modules/intersection-observer": { "version": "0.12.2", "resolved": "https://registry.npmjs.org/intersection-observer/-/intersection-observer-0.12.2.tgz", @@ -5997,6 +6102,26 @@ "integrity": "sha512-cyRxvOEpNHNtchU3Ln9KC/auJgup87llfQpQ+t5ghoC/UhL16SWzbueiCsdTnWmqAWl7LadfuwhlqmtOaqMHdQ==", "dev": true }, + "node_modules/ip-regex": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-4.3.0.tgz", + "integrity": "sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -6096,6 +6221,19 @@ "node": ">=0.10.0" } }, + "node_modules/is-ip": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-ip/-/is-ip-3.1.0.tgz", + "integrity": "sha512-35vd5necO7IitFPjd/YBeqwWnyDWbuLH9ZXQdMfDA8TEo7pv5X8yfrvVO3xbJbLUlERCMvf6X0hTUamQxCYJ9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-regex": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/is-module": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", @@ -6107,6 +6245,7 @@ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.12.0" } @@ -6781,6 +6920,15 @@ "lit-html": "^3.1.2" } }, + "node_modules/lit-element-context": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/lit-element-context/-/lit-element-context-0.2.3.tgz", + "integrity": "sha512-x2qACI9d4t0M2elWjQ8e9xt7F6fxwx0iHzzpt5pod8CU4kvsdrEwUNtXYDX6DjJXKbpzSGgDJkRAdHyWgMfc4Q==", + "license": "MIT", + "dependencies": { + "@open-wc/dedupe-mixin": "^1.3.1" + } + }, "node_modules/lit-html": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.1.2.tgz", @@ -6810,12 +6958,6 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true }, - "node_modules/lodash.assignwith": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.assignwith/-/lodash.assignwith-4.2.0.tgz", - "integrity": "sha512-ZznplvbvtjK2gMvnQ1BR/zqPFZmS6jbK4p+6Up4xcRYA7yMIwxHCfbTcrYxXKzzqLsQ05eJPVznEW3tuwV7k1g==", - "dev": true - }, "node_modules/lodash.camelcase": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", @@ -6955,12 +7097,13 @@ } }, "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, + "license": "MIT", "dependencies": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" }, "engines": { @@ -7169,6 +7312,19 @@ "node": ">=0.10.0" } }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/object-inspect": { "version": "1.13.1", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", @@ -7254,6 +7410,32 @@ "node": ">= 0.8.0" } }, + "node_modules/p-event": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/p-event/-/p-event-4.2.0.tgz", + "integrity": "sha512-KXatOjCRXXkSePPb1Nbi0p0m+gQAwdlbhi4wQKJPI1HsMQS9g+Sqp2o+QHziPr7eYJyOZet836KoHEVM1mwOrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-timeout": "^3.1.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -7284,6 +7466,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-timeout": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", + "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-finally": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/param-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", @@ -7985,10 +8180,11 @@ } }, "node_modules/rollup": { - "version": "2.79.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.1.tgz", - "integrity": "sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==", + "version": "2.79.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz", + "integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==", "dev": true, + "license": "MIT", "bin": { "rollup": "dist/bin/rollup" }, @@ -8382,6 +8578,16 @@ "node": ">=8" } }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -8565,6 +8771,7 @@ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, + "license": "MIT", "dependencies": { "is-number": "^7.0.0" }, @@ -9142,10 +9349,11 @@ "dev": true }, "node_modules/ws": { - "version": "7.5.9", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", - "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8.3.0" }, diff --git a/frontend/package.json b/frontend/package.json index 99aecb0b..4d57427f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -47,7 +47,8 @@ "@lit-labs/preact-signals": "^1.0.2", "@lit/context": "^1.1.2", "@lit/localize": "^0.12.1", - "lit": "^3.0.0" + "lit": "^3.0.0", + "lit-element-context": "^0.2.3" }, "devDependencies": { "@custom-elements-manifest/analyzer": "^0.6.3", From c9be73178dd6d8c7e83ccaf4a5158ad28f9d7e2e Mon Sep 17 00:00:00 2001 From: Alex Garel Date: Tue, 29 Oct 2024 14:29:39 +0100 Subject: [PATCH 04/23] refactor: splet interfaces + wip on suggesters --- frontend/src/event-listener-setup.ts | 26 +----- frontend/src/interfaces/chart-interfaces.ts | 8 ++ frontend/src/interfaces/events-interfaces.ts | 20 +++++ frontend/src/interfaces/facets-interfaces.ts | 6 ++ frontend/src/interfaces/history-interfaces.ts | 43 ++++++++++ .../src/interfaces/search-bar-interfaces.ts | 13 +++ .../src/interfaces/search-ctl-interfaces.ts | 27 ++++++ .../interfaces/search-params-interfaces.ts | 14 +++ frontend/src/interfaces/sort-interfaces.ts | 10 +++ .../src/interfaces/suggestion-interfaces.ts | 43 ++++++++++ frontend/src/mixins/history.ts | 52 +++-------- frontend/src/mixins/search-ctl.ts | 78 ++++------------- frontend/src/mixins/search-results-ctl.ts | 6 +- frontend/src/mixins/suggestion-selection.ts | 27 +----- frontend/src/search-bar.ts | 42 +++++---- frontend/src/search-chart.ts | 10 +-- frontend/src/search-facets.ts | 8 +- frontend/src/search-sort.ts | 16 ++-- frontend/src/search-suggester.ts | 86 +++++++++++-------- frontend/src/search-suggestion-entry.ts | 2 +- 20 files changed, 304 insertions(+), 233 deletions(-) create mode 100644 frontend/src/interfaces/chart-interfaces.ts create mode 100644 frontend/src/interfaces/events-interfaces.ts create mode 100644 frontend/src/interfaces/facets-interfaces.ts create mode 100644 frontend/src/interfaces/history-interfaces.ts create mode 100644 frontend/src/interfaces/search-bar-interfaces.ts create mode 100644 frontend/src/interfaces/search-ctl-interfaces.ts create mode 100644 frontend/src/interfaces/search-params-interfaces.ts create mode 100644 frontend/src/interfaces/sort-interfaces.ts create mode 100644 frontend/src/interfaces/suggestion-interfaces.ts diff --git a/frontend/src/event-listener-setup.ts b/frontend/src/event-listener-setup.ts index 5a160f4d..24923844 100644 --- a/frontend/src/event-listener-setup.ts +++ b/frontend/src/event-listener-setup.ts @@ -2,30 +2,8 @@ * Event registration mixin to take into account AnimationFrame and avoid race conditions on register / unregister events */ //import {LitElement} from 'lit'; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type Constructor = new (...args: any[]) => T; - -export interface EventRegistrationInterface { - /** - * Calls window.addEventListener if not remove before next AnimationFrame - * @param event - event name - * @param handler - function handling event. Beware of using () => this.method to have method bind to this. - */ - addEventHandler( - event: string, - handler: EventListenerOrEventListenerObject - ): void; - /** - * Removes window.removeEventListener but only if really needed - * @param event - event name - * @param handler - function handling event. - */ - removeEventHandler( - event: string, - handler: EventListenerOrEventListenerObject - ): void; -} +import {Constructor} from './mixins/utils'; +import {EventRegistrationInterface} from './interfaces/events-interfaces'; export const EventRegistrationMixin = >( superClass: T diff --git a/frontend/src/interfaces/chart-interfaces.ts b/frontend/src/interfaces/chart-interfaces.ts new file mode 100644 index 00000000..c5aa847d --- /dev/null +++ b/frontend/src/interfaces/chart-interfaces.ts @@ -0,0 +1,8 @@ +interface ChartSearchParamPOST { + chart_type: string; + field?: string; + x?: string; + y?: string; +} + +export type ChartSearchParam = ChartSearchParamPOST | string; diff --git a/frontend/src/interfaces/events-interfaces.ts b/frontend/src/interfaces/events-interfaces.ts new file mode 100644 index 00000000..043a30c7 --- /dev/null +++ b/frontend/src/interfaces/events-interfaces.ts @@ -0,0 +1,20 @@ +export interface EventRegistrationInterface { + /** + * Calls window.addEventListener if not remove before next AnimationFrame + * @param event - event name + * @param handler - function handling event. Beware of using () => this.method to have method bind to this. + */ + addEventHandler( + event: string, + handler: EventListenerOrEventListenerObject + ): void; + /** + * Removes window.removeEventListener but only if really needed + * @param event - event name + * @param handler - function handling event. + */ + removeEventHandler( + event: string, + handler: EventListenerOrEventListenerObject + ): void; +} diff --git a/frontend/src/interfaces/facets-interfaces.ts b/frontend/src/interfaces/facets-interfaces.ts new file mode 100644 index 00000000..96624120 --- /dev/null +++ b/frontend/src/interfaces/facets-interfaces.ts @@ -0,0 +1,6 @@ +import {LitElement} from 'lit'; + +export interface SearchaliciousFacetsInterface extends LitElement { + setSelectedTermsByFacet(value: Record): void; + selectTermByTaxonomy(taxonomy: string, term: string): boolean; +} diff --git a/frontend/src/interfaces/history-interfaces.ts b/frontend/src/interfaces/history-interfaces.ts new file mode 100644 index 00000000..25c29db1 --- /dev/null +++ b/frontend/src/interfaces/history-interfaces.ts @@ -0,0 +1,43 @@ +import {SearchaliciousFacetsInterface} from './facets-interfaces'; +import {SearchaliciousSortInterface} from './sort-interfaces'; +import {SearchParameters} from './search-params-interfaces'; + +/** + * A set of values that can be deduced from parameters, + * and are easy to use to set search components to corresponding values + */ +export type HistoryOutput = { + query?: string; + page?: number; + sortOptionId?: string; + selectedTermsByFacet?: Record; + history: HistoryParams; +}; + +// type of object containing search parameters +export type HistoryParams = { + [key in HistorySearchParams]?: string; +}; + +/** + * Parameters we need to put in URL to be able to deep link the search + */ +export enum HistorySearchParams { + QUERY = 'q', + FACETS_FILTERS = 'facetsFilters', + PAGE = 'page', + SORT_BY = 'sort_by', +} + +export type SearchaliciousHistoryInterface = { + query: string; + name: string; + _currentPage?: number; + relatedFacets: () => SearchaliciousFacetsInterface[]; + _facetsFilters: () => string; + _sortElement: () => SearchaliciousSortInterface | null; + convertHistoryParamsToValues: (params: URLSearchParams) => HistoryOutput; + setValuesFromHistory: (values: HistoryOutput) => void; + buildHistoryParams: (params: SearchParameters) => HistoryParams; + setParamFromUrl: () => {launchSearch: boolean; values: HistoryOutput}; +}; diff --git a/frontend/src/interfaces/search-bar-interfaces.ts b/frontend/src/interfaces/search-bar-interfaces.ts new file mode 100644 index 00000000..1ed63786 --- /dev/null +++ b/frontend/src/interfaces/search-bar-interfaces.ts @@ -0,0 +1,13 @@ +import {createContext} from '@lit/context'; +import {SearchaliciousSuggesterInterface} from './suggestion-interfaces'; +import {SearchaliciousSearchInterface} from './search-ctl-interfaces'; + +export interface SuggesterRegistryInterface { + searchCtl: SearchaliciousSearchInterface; + registerSuggester(suggester: SearchaliciousSuggesterInterface): void; +} + +export const suggesterRegistryContext = + createContext( + 'searchalicious-suggester-registry' + ); diff --git a/frontend/src/interfaces/search-ctl-interfaces.ts b/frontend/src/interfaces/search-ctl-interfaces.ts new file mode 100644 index 00000000..89f1db75 --- /dev/null +++ b/frontend/src/interfaces/search-ctl-interfaces.ts @@ -0,0 +1,27 @@ +import {EventRegistrationInterface} from './events-interfaces'; +import {SearchaliciousHistoryInterface} from '../interfaces/history-interfaces'; +import {SearchaliciousFacetsInterface} from './facets-interfaces'; + +export interface SearchaliciousSearchInterface + extends EventRegistrationInterface, + SearchaliciousHistoryInterface { + query: string; + name: string; + baseUrl: string; + langs: string; + index: string; + pageSize: number; + lastQuery?: string; + lastFacetsFilters?: string; + isQueryChanged: boolean; + isFacetsChanged: boolean; + isSearchChanged: boolean; + canReset: boolean; + + updateSearchSignals(): void; + search(): Promise; + relatedFacets(): SearchaliciousFacetsInterface[]; + relatedFacets(): SearchaliciousFacetsInterface[]; + _facetsFilters(): string; + resetFacets(launchSearch?: boolean): void; +} diff --git a/frontend/src/interfaces/search-params-interfaces.ts b/frontend/src/interfaces/search-params-interfaces.ts new file mode 100644 index 00000000..f4c56064 --- /dev/null +++ b/frontend/src/interfaces/search-params-interfaces.ts @@ -0,0 +1,14 @@ +import {SortParameters} from './sort-interfaces'; +import {ChartSearchParam} from './chart-interfaces'; + +export interface SearchParameters extends SortParameters { + q: string; + boost_phrase: Boolean; + langs: string[]; + page_size: string; + page?: string; + index_id?: string; + facets?: string[]; + params?: string[]; + charts?: string | ChartSearchParam[]; +} diff --git a/frontend/src/interfaces/sort-interfaces.ts b/frontend/src/interfaces/sort-interfaces.ts new file mode 100644 index 00000000..47bc6e05 --- /dev/null +++ b/frontend/src/interfaces/sort-interfaces.ts @@ -0,0 +1,10 @@ +export interface SortParameters { + sort_by?: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + sort_params?: Record; +} + +export interface SearchaliciousSortInterface { + getSortParameters(): SortParameters | null; + setSortOptionById(optionId: string | undefined): void; +} diff --git a/frontend/src/interfaces/suggestion-interfaces.ts b/frontend/src/interfaces/suggestion-interfaces.ts new file mode 100644 index 00000000..daaef3fe --- /dev/null +++ b/frontend/src/interfaces/suggestion-interfaces.ts @@ -0,0 +1,43 @@ +import {TemplateResult} from 'lit'; + +/** + * Type for suggestion option. + */ +export type SuggestionSelectionOption = { + /** + * value assigned to this suggestion + */ + value: string; + /** + * Label to display this suggestion + */ + label: string; + /** + * Unique id for this suggestion + * + * It is important when we have multiple suggestions sources + */ + id: string; + /** + * text that gave the suggestion + * + * It is important because we might do a suggestion on part of the searched terms + */ + input: string; +}; + +/** + * Type for a suggested option + */ +export type SuggestOption = SuggestionSelectionOption & { + /** + * source of this suggestion + */ + source: SearchaliciousSuggesterInterface; +}; + +export interface SearchaliciousSuggesterInterface { + getSuggestions(_value: string): Promise; + selectSuggestion(_selected: SuggestionSelectionOption): void; + renderSuggestion(_suggestion: SuggestOption, _index: number): TemplateResult; +} diff --git a/frontend/src/mixins/history.ts b/frontend/src/mixins/history.ts index 7df15b79..65060767 100644 --- a/frontend/src/mixins/history.ts +++ b/frontend/src/mixins/history.ts @@ -5,55 +5,23 @@ import { removeParenthesis, } from '../utils/url'; import {isNullOrUndefined} from '../utils'; -import {SearchParameters} from './search-ctl'; +import {SearchParameters} from '../interfaces/search-params-interfaces'; import {property} from 'lit/decorators.js'; import {QueryOperator} from '../utils/enums'; +import { + HistoryOutput, + HistoryParams, + HistorySearchParams, + SearchaliciousHistoryInterface, +} from '../interfaces/history-interfaces'; import {SearchaliciousSort} from '../search-sort'; import {SearchaliciousFacets} from '../search-facets'; import {Constructor} from './utils'; import {DEFAULT_SEARCH_NAME} from '../utils/constants'; -export type SearchaliciousHistoryInterface = { - query: string; - name: string; - _currentPage?: number; - _facetsNodes: () => SearchaliciousFacets[]; - _facetsFilters: () => string; - _sortElement: () => SearchaliciousSort | null; - convertHistoryParamsToValues: (params: URLSearchParams) => HistoryOutput; - setValuesFromHistory: (values: HistoryOutput) => void; - buildHistoryParams: (params: SearchParameters) => HistoryParams; - setParamFromUrl: () => {launchSearch: boolean; values: HistoryOutput}; -}; - -/** - * A set of values that can be deduced from parameters, - * and are easy to use to set search components to corresponding values - */ -export type HistoryOutput = { - query?: string; - page?: number; - sortOptionId?: string; - selectedTermsByFacet?: Record; - history: HistoryParams; -}; -/** - * Parameters we need to put in URL to be able to deep link the search - */ -export enum HistorySearchParams { - QUERY = 'q', - FACETS_FILTERS = 'facetsFilters', - PAGE = 'page', - SORT_BY = 'sort_by', -} - // name of search params as an array (to ease iteration) export const SEARCH_PARAMS = Object.values(HistorySearchParams); -// type of object containing search parameters -export type HistoryParams = { - [key in HistorySearchParams]?: string; -}; /** * Object to convert the URL params to the original values * @@ -107,7 +75,7 @@ const HISTORY_VALUES: Record< /** * Mixin to handle the history of the search * It exists to have the logic of the history in a single place - * It has to be inherited by SearchaliciousSearchMixin to implement _facetsNodes and _facetsFilters functionss + * It has to be inherited by SearchaliciousSearchMixin to implement relatedFacets and _facetsFilters functionss * @param superClass * @constructor */ @@ -125,7 +93,7 @@ export const SearchaliciousHistoryMixin = >( _sortElement = (): SearchaliciousSort | null => { throw new Error('Method not implemented.'); }; - _facetsNodes = (): SearchaliciousFacets[] => { + relatedFacets = (): SearchaliciousFacets[] => { throw new Error('Method not implemented.'); }; _facetsFilters = (): string => { @@ -161,7 +129,7 @@ export const SearchaliciousHistoryMixin = >( this._sortElement()?.setSortOptionById(values.sortOptionId); // set facets terms using linked facets nodes if (values.selectedTermsByFacet) { - this._facetsNodes().forEach((facets) => + this.relatedFacets().forEach((facets) => facets.setSelectedTermsByFacet(values.selectedTermsByFacet!) ); } diff --git a/frontend/src/mixins/search-ctl.ts b/frontend/src/mixins/search-ctl.ts index 33b22fde..60593530 100644 --- a/frontend/src/mixins/search-ctl.ts +++ b/frontend/src/mixins/search-ctl.ts @@ -1,13 +1,10 @@ import {LitElement} from 'lit'; import {property, state} from 'lit/decorators.js'; -import { - EventRegistrationInterface, - EventRegistrationMixin, -} from '../event-listener-setup'; +import {EventRegistrationMixin} from '../event-listener-setup'; import {QueryOperator, SearchaliciousEvents} from '../utils/enums'; import {ChangePageEvent} from '../events'; import {Constructor} from './utils'; -import {SearchaliciousSort, SortParameters} from '../search-sort'; +import {SearchaliciousSort} from '../search-sort'; import {SearchaliciousFacets} from '../search-facets'; import {setCurrentURLHistory} from '../utils/url'; import {isNullOrUndefined} from '../utils'; @@ -16,15 +13,14 @@ import { DEFAULT_SEARCH_NAME, PROPERTY_LIST_DIVIDER, } from '../utils/constants'; -import { - HistorySearchParams, - SearchaliciousHistoryInterface, - SearchaliciousHistoryMixin, -} from './history'; +import {SearchaliciousSearchInterface} from '../interfaces/search-ctl-interfaces'; +import {HistorySearchParams} from '../interfaces/history-interfaces'; +import {SearchParameters} from '../interfaces/search-params-interfaces'; +import {ChartSearchParam} from '../interfaces/chart-interfaces'; +import {SearchaliciousHistoryMixin} from './history'; import { SearchaliciousDistributionChart, SearchaliciousScatterChart, - ChartSearchParam, } from '../search-chart'; import { canResetSearch, @@ -35,40 +31,6 @@ import { import {SignalWatcher} from '@lit-labs/preact-signals'; import {isTheSameSearchName} from '../utils/search'; -export interface SearchParameters extends SortParameters { - q: string; - boost_phrase: Boolean; - langs: string[]; - page_size: string; - page?: string; - index_id?: string; - facets?: string[]; - params?: string[]; - charts?: string | ChartSearchParam[]; -} -export interface SearchaliciousSearchInterface - extends EventRegistrationInterface, - SearchaliciousHistoryInterface { - query: string; - name: string; - baseUrl: string; - langs: string; - index: string; - pageSize: number; - lastQuery?: string; - lastFacetsFilters?: string; - isQueryChanged: boolean; - isFacetsChanged: boolean; - isSearchChanged: boolean; - canReset: boolean; - - updateSearchSignals(): void; - search(): Promise; - _facetsNodes(): SearchaliciousFacets[]; - _facetsFilters(): string; - resetFacets(launchSearch?: boolean): void; -} - export const SearchaliciousSearchMixin = >( superClass: T ) => { @@ -200,13 +162,6 @@ export const SearchaliciousSearchMixin = >( isSearchChanged(this.name).value = this.isSearchChanged; } - /** list of facets containers */ - _facetsParentNode() { - return document.querySelectorAll( - `searchalicious-facets[search-name=${this.name}]` - ); - } - /** * @returns the sort element linked to this search ctl */ @@ -259,10 +214,11 @@ export const SearchaliciousSearchMixin = >( /** * @returns all searchalicious-facets elements linked to this search ctl */ - override _facetsNodes = (): SearchaliciousFacets[] => { + override relatedFacets = (): SearchaliciousFacets[] => { const allNodes: SearchaliciousFacets[] = []; - // search facets elements, we can't filter on search-name because of default value… - this._facetsParentNode()?.forEach((item) => { + // search facets elements, + // we can't directly filter on search-name in selector because of default value + document.querySelectorAll(`searchalicious-facets`).forEach((item) => { const facetElement = item as SearchaliciousFacets; if (facetElement.searchName == this.name) { allNodes.push(facetElement); @@ -274,8 +230,8 @@ export const SearchaliciousSearchMixin = >( /** * Get the list of facets we want to request */ - _facets(): string[] { - const names = this._facetsNodes() + _facetsNames(): string[] { + const names = this.relatedFacets() .map((facets) => facets.getFacetsNames()) .flat(); return [...new Set(names)]; @@ -319,14 +275,14 @@ export const SearchaliciousSearchMixin = >( * @returns an expression to be added to query */ override _facetsFilters = (): string => { - const allFilters: string[] = this._facetsNodes() + const allFilters: string[] = this.relatedFacets() .map((facets) => facets.getSearchFilters()) .flat(); return allFilters.join(QueryOperator.AND); }; resetFacets(launchSearch = true) { - this._facetsNodes().forEach((facets) => facets.reset(launchSearch)); + this.relatedFacets().forEach((facets) => facets.reset(launchSearch)); } /* @@ -488,8 +444,8 @@ export const SearchaliciousSearchMixin = >( params.page = page.toString(); } // facets - if (this._facets().length > 0) { - params.facets = this._facets(); + if (this._facetsNames().length > 0) { + params.facets = this._facetsNames(); } const charts = this._chartParams(!needsPOST); diff --git a/frontend/src/mixins/search-results-ctl.ts b/frontend/src/mixins/search-results-ctl.ts index 1deba718..735d5f18 100644 --- a/frontend/src/mixins/search-results-ctl.ts +++ b/frontend/src/mixins/search-results-ctl.ts @@ -5,10 +5,8 @@ import {Constructor} from './utils'; import {DEFAULT_SEARCH_NAME} from '../utils/constants'; import {SearchResultDetail, searchResultDetail} from '../signals'; import {Signal, SignalWatcher} from '@lit-labs/preact-signals'; -import { - EventRegistrationInterface, - EventRegistrationMixin, -} from '../event-listener-setup'; +import {EventRegistrationInterface} from '../interfaces/events-interfaces'; +import {EventRegistrationMixin} from '../event-listener-setup'; export interface SearchaliciousResultsCtlInterface extends EventRegistrationInterface { diff --git a/frontend/src/mixins/suggestion-selection.ts b/frontend/src/mixins/suggestion-selection.ts index 152b014c..8ac6fa80 100644 --- a/frontend/src/mixins/suggestion-selection.ts +++ b/frontend/src/mixins/suggestion-selection.ts @@ -2,6 +2,7 @@ import {LitElement} from 'lit'; import {Constructor} from './utils'; import {property, state} from 'lit/decorators.js'; import {DebounceMixin, DebounceMixinInterface} from './debounce'; +import {SuggestionSelectionOption} from '../interfaces/suggestion-interfaces'; /** * Interface for the Suggestion Selection mixin. @@ -29,32 +30,6 @@ export interface SuggestionSelectionMixinInterface onBlur(): void; } -/** - * Type for suggestion option. - */ -export type SuggestionSelectionOption = { - /** - * value assigned to this suggestion - */ - value: string; - /** - * Label to display this suggestion - */ - label: string; - /** - * Unique id for this suggestion - * - * It is important when we have multiple suggestions sources - */ - id: string; - /** - * text that gave the suggestion - * - * It is important because we might do a suggestion on part of the searched terms - */ - input: string; -}; - /** * Type for suggestion result. */ diff --git a/frontend/src/search-bar.ts b/frontend/src/search-bar.ts index c640d6fc..25aef94c 100644 --- a/frontend/src/search-bar.ts +++ b/frontend/src/search-bar.ts @@ -2,17 +2,16 @@ import {LitElement, html, css} from 'lit'; import {customElement, property} from 'lit/decorators.js'; import {SearchaliciousSearchMixin} from './mixins/search-ctl'; import {localized, msg} from '@lit/localize'; +import {provide} from '@lit/context'; import {setLocale} from './localization/main'; import {SuggestionSelectionMixin} from './mixins/suggestion-selection'; -import { - SearchaliciousSuggester, - SuggestOption, - SuggestersSelector, -} from './search-suggester'; +import {SuggestOption} from './interfaces/suggestion-interfaces'; +import {SuggesterRegistry, SuggestersToClass} from './search-suggester'; import {classMap} from 'lit/directives/class-map.js'; import {searchBarInputAndButtonStyle} from './css/header'; import {SearchaliciousEvents} from './utils/enums'; import {isTheSameSearchName} from './utils/search'; +import {suggesterRegistryContext} from './interfaces/search-bar-interfaces'; /** * The search bar element @@ -100,6 +99,9 @@ export class SearchaliciousBar extends SuggestionSelectionMixin( */ private _placeholder?: string; + @provide({context: suggesterRegistryContext}) + public suggesterRegistry: SuggesterRegistry; + /** * Place holder in search bar */ @@ -123,25 +125,20 @@ export class SearchaliciousBar extends SuggestionSelectionMixin( constructor() { super(); - - // allow to set the locale from the browser + // create registry + this.suggesterRegistry = new SuggesterRegistry(this); // @ts-ignore - window.setLocale = setLocale; - } - - /** Return the list of suggesters contained in the search bar */ - get suggesters() { - const suggesters: SearchaliciousSuggester[] = []; - this.querySelectorAll(SuggestersSelector).forEach((element) => { - suggesters.push(element as SearchaliciousSuggester); - }); - return suggesters; + if (!window.setLocale) { + // allow to set the locale from the browser + // @ts-ignore + window.setLocale = setLocale; + } } /** Ask suggesters for suggested options */ async getSuggestions(value: string) { return Promise.allSettled( - this.suggesters.map((suggester) => { + (this.suggesterRegistry.suggesters || []).map((suggester) => { return suggester.getSuggestions(value); }) ).then((optionsLists) => { @@ -239,6 +236,15 @@ export class SearchaliciousBar extends SuggestionSelectionMixin( this.onReset(); } }); + + // instanciate the suggesters + const selector = Object.keys(SuggestersToClass).join(','); + this.querySelectorAll(selector).forEach((suggesterElement) => { + const suggesterClass = + SuggestersToClass[suggesterElement.tagName.toLowerCase()]; + const suggester = new suggesterClass(this.suggesterRegistry); + this.suggesterRegistry.registerSuggester(suggester); + }); } override disconnectedCallback() { diff --git a/frontend/src/search-chart.ts b/frontend/src/search-chart.ts index 791f77cc..36dfa5e1 100644 --- a/frontend/src/search-chart.ts +++ b/frontend/src/search-chart.ts @@ -4,17 +4,9 @@ import {customElement, property} from 'lit/decorators.js'; import {WHITE_PANEL_STYLE} from './styles'; import {SearchResultDetail} from './signals'; +import {ChartSearchParam} from './interfaces/chart-interfaces'; import {SearchaliciousResultCtlMixin} from './mixins/search-results-ctl'; -interface ChartSearchParamPOST { - chart_type: string; - field?: string; - x?: string; - y?: string; -} - -export type ChartSearchParam = ChartSearchParamPOST | string; - // eslint raises error due to :any // eslint-disable-next-line declare const vega: any; diff --git a/frontend/src/search-facets.ts b/frontend/src/search-facets.ts index fd00bb61..6ccdc760 100644 --- a/frontend/src/search-facets.ts +++ b/frontend/src/search-facets.ts @@ -10,6 +10,7 @@ import {QueryOperator, SearchaliciousEvents} from './utils/enums'; import {getPluralTranslation} from './localization/translations'; import {msg, localized} from '@lit/localize'; import {WHITE_PANEL_STYLE} from './styles'; +import {SearchaliciousFacetsInterface} from './interfaces/facets-interfaces'; import {SearchaliciousResultCtlMixin} from './mixins/search-results-ctl'; interface FacetsInfos { @@ -46,9 +47,10 @@ function stringGuard(s: string | undefined): s is string { */ @customElement('searchalicious-facets') @localized() -export class SearchaliciousFacets extends SearchaliciousResultCtlMixin( - SearchActionMixin(LitElement) -) { +export class SearchaliciousFacets + extends SearchaliciousResultCtlMixin(SearchActionMixin(LitElement)) + implements SearchaliciousFacetsInterface +{ static override styles = css` .reset-button-wrapper { display: flex; diff --git a/frontend/src/search-sort.ts b/frontend/src/search-sort.ts index d5dfa2f6..d1ddedf5 100644 --- a/frontend/src/search-sort.ts +++ b/frontend/src/search-sort.ts @@ -1,15 +1,14 @@ import {css, LitElement, html, nothing} from 'lit'; import {customElement, property, state} from 'lit/decorators.js'; +import { + SortParameters, + SearchaliciousSortInterface, +} from './interfaces/sort-interfaces'; import {SearchActionMixin} from './mixins/search-action'; import {EventRegistrationMixin} from './event-listener-setup'; import {SearchaliciousEvents} from './utils/enums'; -export interface SortParameters { - sort_by?: string; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - sort_params?: Record; -} /** * A component to enable user to choose a search order. * @@ -21,9 +20,10 @@ export interface SortParameters { * @cssproperty --sort-options-background-color - The background color of the options.t */ @customElement('searchalicious-sort') -export class SearchaliciousSort extends SearchActionMixin( - EventRegistrationMixin(LitElement) -) { +export class SearchaliciousSort + extends SearchActionMixin(EventRegistrationMixin(LitElement)) + implements SearchaliciousSortInterface +{ static override styles = css` .options { list-style: none; diff --git a/frontend/src/search-suggester.ts b/frontend/src/search-suggester.ts index e0a24088..0806e846 100644 --- a/frontend/src/search-suggester.ts +++ b/frontend/src/search-suggester.ts @@ -6,22 +6,24 @@ import {LitElement, TemplateResult, html} from 'lit'; import {customElement, property} from 'lit/decorators.js'; import {SearchaliciousTermsMixin} from './mixins/suggestions-ctl'; -import {SearchaliciousSearchInterface} from './mixins/search-ctl'; -import {SuggestionSelectionOption} from './mixins/suggestion-selection'; -import {SearchaliciousFacets} from './search-facets'; +import {SearchaliciousSearchInterface} from './interfaces/search-ctl-interfaces'; +import { + SuggestionSelectionOption, + SuggestOption, +} from './interfaces/suggestion-interfaces'; +import {SuggesterRegistryInterface} from './interfaces/search-bar-interfaces'; +import {SearchaliciousFacetsInterface} from './interfaces/facets-interfaces'; import {removeLangFromTermId} from './utils/taxonomies'; - -/** - * Type for a suggested option - */ -export type SuggestOption = SuggestionSelectionOption & { - /** - * source of this suggestion - */ - source: SearchaliciousSuggester; -}; +import {Constructor} from './mixins/utils'; export class SearchaliciousSuggester extends LitElement { + suggesterRegistry: SuggesterRegistryInterface; + + constructor(suggesterRegistry: SuggesterRegistryInterface) { + super(); + this.suggesterRegistry = suggesterRegistry; + } + /** * Query for options to suggest for value and return them */ @@ -48,7 +50,7 @@ export class SearchaliciousSuggester extends LitElement { * An element to be used inside a searchalicious-bar component. * It enables giving suggestions that are based upon taxonomies */ -@customElement('searchalicious-taxonomy-suggester') +@customElement('searchalicious-taxonomy-suggest') export class SearchaliciousTaxonomySuggester extends SearchaliciousTermsMixin( SearchaliciousSuggester ) { @@ -69,30 +71,16 @@ export class SearchaliciousTaxonomySuggester extends SearchaliciousTermsMixin( fuzziness = 2; /** - * taxonomies attribute but as an array of String + * Search bar associated to this suggester */ - get taxonomiesList() { - return this.taxonomies.split(','); - } + @property({attribute: false}) + searchBar?: SearchaliciousSearchInterface; /** - * Return the search bar this element is contained in + * taxonomies attribute but as an array of String */ - get searchBar(): SearchaliciousSearchInterface { - const searchBar = this.closest('searchalicious-bar'); - if (searchBar == null) { - throw new Error( - 'Taxonomy suggester must be contained in a searchalicious-bar' - ); - } - return searchBar; - } - - /** list of facets containers */ - _facetsParentNode() { - return document.querySelectorAll( - `searchalicious-facets[search-name=${this.searchBar.name}]` - ); + get taxonomiesList() { + return this.taxonomies.split(','); } /** @@ -102,10 +90,13 @@ export class SearchaliciousTaxonomySuggester extends SearchaliciousTermsMixin( * @param term */ selectTermByTaxonomy(taxonomy: string, term: string) { - for (const facets of this._facetsParentNode()) { + for (const facets of this.searchBar!.relatedFacets()) { // if true, the facets has been updated if ( - (facets as SearchaliciousFacets).selectTermByTaxonomy(taxonomy, term) + (facets as SearchaliciousFacetsInterface).selectTermByTaxonomy( + taxonomy, + term + ) ) { return; } @@ -151,4 +142,25 @@ declare global { /** * An expression to be able to get all suggesters using a querySelectorAll */ -export const SuggestersSelector = 'searchalicious-taxonomy-suggest'; +export const SuggestersToClass: Record< + string, + Constructor +> = { + 'searchalicious-taxonomy-suggest': SearchaliciousTaxonomySuggester, +}; + +export class SuggesterRegistry implements SuggesterRegistryInterface { + suggesters: SearchaliciousSuggester[]; + searchCtl: SearchaliciousSearchInterface; + + constructor(searchCtl: SearchaliciousSearchInterface) { + this.searchCtl = searchCtl; + this.suggesters = []; + } + + registerSuggester(suggester: SearchaliciousSuggester): void { + if (!this.suggesters!.includes(suggester)) { + this.suggesters!.push(suggester); + } + } +} diff --git a/frontend/src/search-suggestion-entry.ts b/frontend/src/search-suggestion-entry.ts index 0a169299..64acdafb 100644 --- a/frontend/src/search-suggestion-entry.ts +++ b/frontend/src/search-suggestion-entry.ts @@ -1,6 +1,6 @@ import {css, html, LitElement} from 'lit'; import {customElement, property} from 'lit/decorators.js'; -import {SuggestionSelectionOption} from './mixins/suggestion-selection'; +import {SuggestionSelectionOption} from './interfaces/suggestion-interfaces'; /** * This component represent a suggestion to the user as he types his search. From 73c159957ced112d8291771a9adf1bed827138fd Mon Sep 17 00:00:00 2001 From: Alex Garel Date: Tue, 29 Oct 2024 15:40:32 +0100 Subject: [PATCH 05/23] =?UTF-8?q?chore:=C2=A0wip?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/search-bar.ts | 32 ++++++++++------ frontend/src/search-suggester.ts | 64 ++++++++++++++++++++------------ 2 files changed, 61 insertions(+), 35 deletions(-) diff --git a/frontend/src/search-bar.ts b/frontend/src/search-bar.ts index 25aef94c..74c28a57 100644 --- a/frontend/src/search-bar.ts +++ b/frontend/src/search-bar.ts @@ -1,12 +1,12 @@ import {LitElement, html, css} from 'lit'; -import {customElement, property} from 'lit/decorators.js'; +import {customElement, property, state} from 'lit/decorators.js'; import {SearchaliciousSearchMixin} from './mixins/search-ctl'; import {localized, msg} from '@lit/localize'; import {provide} from '@lit/context'; import {setLocale} from './localization/main'; import {SuggestionSelectionMixin} from './mixins/suggestion-selection'; import {SuggestOption} from './interfaces/suggestion-interfaces'; -import {SuggesterRegistry, SuggestersToClass} from './search-suggester'; +import {SearchaliciousSuggester, SuggesterRegistry} from './search-suggester'; import {classMap} from 'lit/directives/class-map.js'; import {searchBarInputAndButtonStyle} from './css/header'; import {SearchaliciousEvents} from './utils/enums'; @@ -100,6 +100,7 @@ export class SearchaliciousBar extends SuggestionSelectionMixin( private _placeholder?: string; @provide({context: suggesterRegistryContext}) + @state() public suggesterRegistry: SuggesterRegistry; /** @@ -137,10 +138,17 @@ export class SearchaliciousBar extends SuggestionSelectionMixin( /** Ask suggesters for suggested options */ async getSuggestions(value: string) { + const suggesters = [ + ...this.querySelectorAll('searchalicious-taxonomy-suggest'), + ]; return Promise.allSettled( - (this.suggesterRegistry.suggesters || []).map((suggester) => { - return suggester.getSuggestions(value); + // FIXME + suggesters.map((suggester) => { + return (suggester as SearchaliciousSuggester).getSuggestions(value); }) + //(this.suggesterRegistry.suggesters || []).map((suggester) => { + // return suggester.getSuggestions(value); + //}) ).then((optionsLists) => { const options: SuggestOption[] = []; optionsLists.forEach((result) => { @@ -190,7 +198,7 @@ export class SearchaliciousBar extends SuggestionSelectionMixin( */ renderSuggestions() { // Don't show suggestions if the input is not focused or the value is empty or there are no suggestions - if (!this.visible || !this.selectedOption || this.options.length === 0) { + if (!this.visible || !this.query || this.options.length === 0) { return html``; } @@ -238,13 +246,13 @@ export class SearchaliciousBar extends SuggestionSelectionMixin( }); // instanciate the suggesters - const selector = Object.keys(SuggestersToClass).join(','); - this.querySelectorAll(selector).forEach((suggesterElement) => { - const suggesterClass = - SuggestersToClass[suggesterElement.tagName.toLowerCase()]; - const suggester = new suggesterClass(this.suggesterRegistry); - this.suggesterRegistry.registerSuggester(suggester); - }); + //const selector = Object.keys(SuggestersToClass).join(','); + //this.querySelectorAll(selector).forEach((suggesterElement) => { + // const suggesterClass = + // SuggestersToClass[suggesterElement.tagName.toLowerCase()]; + // const suggester = new suggesterClass(this.suggesterRegistry); + // this.suggesterRegistry.registerSuggester(suggester); + //}); } override disconnectedCallback() { diff --git a/frontend/src/search-suggester.ts b/frontend/src/search-suggester.ts index 0806e846..b55fc4ef 100644 --- a/frontend/src/search-suggester.ts +++ b/frontend/src/search-suggester.ts @@ -3,27 +3,51 @@ * It indicates some suggestion that the search bar should apply * */ -import {LitElement, TemplateResult, html} from 'lit'; +import {LitElement, PropertyValues, TemplateResult, html} from 'lit'; import {customElement, property} from 'lit/decorators.js'; +import {consume} from '@lit/context'; import {SearchaliciousTermsMixin} from './mixins/suggestions-ctl'; import {SearchaliciousSearchInterface} from './interfaces/search-ctl-interfaces'; import { SuggestionSelectionOption, SuggestOption, } from './interfaces/suggestion-interfaces'; -import {SuggesterRegistryInterface} from './interfaces/search-bar-interfaces'; +import { + SuggesterRegistryInterface, + suggesterRegistryContext, +} from './interfaces/search-bar-interfaces'; import {SearchaliciousFacetsInterface} from './interfaces/facets-interfaces'; import {removeLangFromTermId} from './utils/taxonomies'; -import {Constructor} from './mixins/utils'; +//import {Constructor} from './mixins/utils'; export class SearchaliciousSuggester extends LitElement { - suggesterRegistry: SuggesterRegistryInterface; - - constructor(suggesterRegistry: SuggesterRegistryInterface) { - super(); - this.suggesterRegistry = suggesterRegistry; + //suggesterRegistry: SuggesterRegistryInterface; + + //constructor(suggesterRegistry: SuggesterRegistryInterface) { + // super(); + // this.suggesterRegistry = suggesterRegistry; + //} + + @consume({context: suggesterRegistryContext}) + suggesterRegistry!: SuggesterRegistryInterface; + + //override connectedCallback() { + // super.connectedCallback(); + // // this is the good time to register + // this.suggesterRegistry?.registerSuggester(this); + //} + + override firstUpdated(changedProperties: PropertyValues) { + super.firstUpdated(changedProperties); + // this is the good time to register + this.suggesterRegistry?.registerSuggester(this); } + // override render() { + // this.suggesterRegistry?.registerSuggester(this); + // return html``; + // } + /** * Query for options to suggest for value and return them */ @@ -70,12 +94,6 @@ export class SearchaliciousTaxonomySuggester extends SearchaliciousTermsMixin( @property({type: Number}) fuzziness = 2; - /** - * Search bar associated to this suggester - */ - @property({attribute: false}) - searchBar?: SearchaliciousSearchInterface; - /** * taxonomies attribute but as an array of String */ @@ -90,7 +108,7 @@ export class SearchaliciousTaxonomySuggester extends SearchaliciousTermsMixin( * @param term */ selectTermByTaxonomy(taxonomy: string, term: string) { - for (const facets of this.searchBar!.relatedFacets()) { + for (const facets of this.suggesterRegistry.searchCtl!.relatedFacets()) { // if true, the facets has been updated if ( (facets as SearchaliciousFacetsInterface).selectTermByTaxonomy( @@ -142,12 +160,12 @@ declare global { /** * An expression to be able to get all suggesters using a querySelectorAll */ -export const SuggestersToClass: Record< - string, - Constructor -> = { - 'searchalicious-taxonomy-suggest': SearchaliciousTaxonomySuggester, -}; +//export const SuggestersToClass: Record< +// string, +// Constructor +//> = { +// 'searchalicious-taxonomy-suggest': SearchaliciousTaxonomySuggester, +//}; export class SuggesterRegistry implements SuggesterRegistryInterface { suggesters: SearchaliciousSuggester[]; @@ -159,8 +177,8 @@ export class SuggesterRegistry implements SuggesterRegistryInterface { } registerSuggester(suggester: SearchaliciousSuggester): void { - if (!this.suggesters!.includes(suggester)) { - this.suggesters!.push(suggester); + if (!this.suggesters.includes(suggester)) { + this.suggesters.push(suggester); } } } From f6d0fe72cf1c5b82e46b1a88869ef58c7e9579b8 Mon Sep 17 00:00:00 2001 From: Alex Garel Date: Tue, 29 Oct 2024 16:04:39 +0100 Subject: [PATCH 06/23] =?UTF-8?q?feat:=C2=A0suggester=20component=20workin?= =?UTF-8?q?g?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/interfaces/search-bar-interfaces.ts | 13 ---- frontend/src/search-bar.ts | 43 +++++------ frontend/src/search-suggester.ts | 77 ++++--------------- 3 files changed, 34 insertions(+), 99 deletions(-) diff --git a/frontend/src/interfaces/search-bar-interfaces.ts b/frontend/src/interfaces/search-bar-interfaces.ts index 1ed63786..e69de29b 100644 --- a/frontend/src/interfaces/search-bar-interfaces.ts +++ b/frontend/src/interfaces/search-bar-interfaces.ts @@ -1,13 +0,0 @@ -import {createContext} from '@lit/context'; -import {SearchaliciousSuggesterInterface} from './suggestion-interfaces'; -import {SearchaliciousSearchInterface} from './search-ctl-interfaces'; - -export interface SuggesterRegistryInterface { - searchCtl: SearchaliciousSearchInterface; - registerSuggester(suggester: SearchaliciousSuggesterInterface): void; -} - -export const suggesterRegistryContext = - createContext( - 'searchalicious-suggester-registry' - ); diff --git a/frontend/src/search-bar.ts b/frontend/src/search-bar.ts index 74c28a57..c3c77c91 100644 --- a/frontend/src/search-bar.ts +++ b/frontend/src/search-bar.ts @@ -1,17 +1,17 @@ -import {LitElement, html, css} from 'lit'; +import {LitElement, html, css, PropertyValues} from 'lit'; import {customElement, property, state} from 'lit/decorators.js'; import {SearchaliciousSearchMixin} from './mixins/search-ctl'; import {localized, msg} from '@lit/localize'; -import {provide} from '@lit/context'; import {setLocale} from './localization/main'; import {SuggestionSelectionMixin} from './mixins/suggestion-selection'; import {SuggestOption} from './interfaces/suggestion-interfaces'; -import {SearchaliciousSuggester, SuggesterRegistry} from './search-suggester'; +import { + SearchaliciousSuggester /*SuggesterRegistry*/, +} from './search-suggester'; import {classMap} from 'lit/directives/class-map.js'; import {searchBarInputAndButtonStyle} from './css/header'; import {SearchaliciousEvents} from './utils/enums'; import {isTheSameSearchName} from './utils/search'; -import {suggesterRegistryContext} from './interfaces/search-bar-interfaces'; /** * The search bar element @@ -99,9 +99,8 @@ export class SearchaliciousBar extends SuggestionSelectionMixin( */ private _placeholder?: string; - @provide({context: suggesterRegistryContext}) @state() - public suggesterRegistry: SuggesterRegistry; + suggesters?: SearchaliciousSuggester[] = []; /** * Place holder in search bar @@ -126,8 +125,6 @@ export class SearchaliciousBar extends SuggestionSelectionMixin( constructor() { super(); - // create registry - this.suggesterRegistry = new SuggesterRegistry(this); // @ts-ignore if (!window.setLocale) { // allow to set the locale from the browser @@ -136,19 +133,24 @@ export class SearchaliciousBar extends SuggestionSelectionMixin( } } - /** Ask suggesters for suggested options */ - async getSuggestions(value: string) { - const suggesters = [ + override firstUpdated(changedProperties: PropertyValues) { + super.firstUpdated(changedProperties); + // this is the good time to register + this.suggesters = [ ...this.querySelectorAll('searchalicious-taxonomy-suggest'), ]; + this.suggesters.forEach((suggester) => (suggester.searchCtl = this)); + } + + /** Ask suggesters for suggested options */ + async getSuggestions(value: string) { + if (this.suggesters === undefined) { + throw new Error('No suggesters registered'); + } return Promise.allSettled( - // FIXME - suggesters.map((suggester) => { + this.suggesters.map((suggester) => { return (suggester as SearchaliciousSuggester).getSuggestions(value); }) - //(this.suggesterRegistry.suggesters || []).map((suggester) => { - // return suggester.getSuggestions(value); - //}) ).then((optionsLists) => { const options: SuggestOption[] = []; optionsLists.forEach((result) => { @@ -244,15 +246,6 @@ export class SearchaliciousBar extends SuggestionSelectionMixin( this.onReset(); } }); - - // instanciate the suggesters - //const selector = Object.keys(SuggestersToClass).join(','); - //this.querySelectorAll(selector).forEach((suggesterElement) => { - // const suggesterClass = - // SuggestersToClass[suggesterElement.tagName.toLowerCase()]; - // const suggester = new suggesterClass(this.suggesterRegistry); - // this.suggesterRegistry.registerSuggester(suggester); - //}); } override disconnectedCallback() { diff --git a/frontend/src/search-suggester.ts b/frontend/src/search-suggester.ts index b55fc4ef..5b650993 100644 --- a/frontend/src/search-suggester.ts +++ b/frontend/src/search-suggester.ts @@ -3,50 +3,20 @@ * It indicates some suggestion that the search bar should apply * */ -import {LitElement, PropertyValues, TemplateResult, html} from 'lit'; +import {LitElement, TemplateResult, html} from 'lit'; import {customElement, property} from 'lit/decorators.js'; -import {consume} from '@lit/context'; import {SearchaliciousTermsMixin} from './mixins/suggestions-ctl'; import {SearchaliciousSearchInterface} from './interfaces/search-ctl-interfaces'; import { SuggestionSelectionOption, SuggestOption, } from './interfaces/suggestion-interfaces'; -import { - SuggesterRegistryInterface, - suggesterRegistryContext, -} from './interfaces/search-bar-interfaces'; import {SearchaliciousFacetsInterface} from './interfaces/facets-interfaces'; import {removeLangFromTermId} from './utils/taxonomies'; -//import {Constructor} from './mixins/utils'; export class SearchaliciousSuggester extends LitElement { - //suggesterRegistry: SuggesterRegistryInterface; - - //constructor(suggesterRegistry: SuggesterRegistryInterface) { - // super(); - // this.suggesterRegistry = suggesterRegistry; - //} - - @consume({context: suggesterRegistryContext}) - suggesterRegistry!: SuggesterRegistryInterface; - - //override connectedCallback() { - // super.connectedCallback(); - // // this is the good time to register - // this.suggesterRegistry?.registerSuggester(this); - //} - - override firstUpdated(changedProperties: PropertyValues) { - super.firstUpdated(changedProperties); - // this is the good time to register - this.suggesterRegistry?.registerSuggester(this); - } - - // override render() { - // this.suggesterRegistry?.registerSuggester(this); - // return html``; - // } + @property({attribute: false}) + searchCtl: SearchaliciousSearchInterface | undefined; /** * Query for options to suggest for value and return them @@ -70,6 +40,13 @@ export class SearchaliciousSuggester extends LitElement { } } +type taxonomySelectionOption = SuggestionSelectionOption & { + /** + * taxonomy related to this suggestion + */ + taxonomy: string; +}; + /** * An element to be used inside a searchalicious-bar component. * It enables giving suggestions that are based upon taxonomies @@ -108,7 +85,7 @@ export class SearchaliciousTaxonomySuggester extends SearchaliciousTermsMixin( * @param term */ selectTermByTaxonomy(taxonomy: string, term: string) { - for (const facets of this.suggesterRegistry.searchCtl!.relatedFacets()) { + for (const facets of this.searchCtl!.relatedFacets()) { // if true, the facets has been updated if ( (facets as SearchaliciousFacetsInterface).selectTermByTaxonomy( @@ -133,12 +110,16 @@ export class SearchaliciousTaxonomySuggester extends SearchaliciousTermsMixin( id: term.taxonomy_name + '-' + term.id, source: this, input: value, + taxonomy: term.taxonomy_name, })); }); } override async selectSuggestion(selected: SuggestionSelectionOption) { - this.selectTermByTaxonomy(this.taxonomies, selected.value); + this.selectTermByTaxonomy( + (selected as taxonomySelectionOption).taxonomy, + selected.value + ); } override renderSuggestion( @@ -156,29 +137,3 @@ declare global { 'searchalicious-taxonomy-suggest': SearchaliciousTaxonomySuggester; } } - -/** - * An expression to be able to get all suggesters using a querySelectorAll - */ -//export const SuggestersToClass: Record< -// string, -// Constructor -//> = { -// 'searchalicious-taxonomy-suggest': SearchaliciousTaxonomySuggester, -//}; - -export class SuggesterRegistry implements SuggesterRegistryInterface { - suggesters: SearchaliciousSuggester[]; - searchCtl: SearchaliciousSearchInterface; - - constructor(searchCtl: SearchaliciousSearchInterface) { - this.searchCtl = searchCtl; - this.suggesters = []; - } - - registerSuggester(suggester: SearchaliciousSuggester): void { - if (!this.suggesters.includes(suggester)) { - this.suggesters.push(suggester); - } - } -} From fc064829ee78e737be268c2dd071e758792e2f34 Mon Sep 17 00:00:00 2001 From: Alex Garel Date: Tue, 29 Oct 2024 17:42:02 +0100 Subject: [PATCH 07/23] =?UTF-8?q?fix:=C2=A0fix=20bugs=20on=20facets=20&=20?= =?UTF-8?q?history?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/mixins/history.ts | 14 +++++++++++-- frontend/src/search-facets.ts | 36 +++++++++++++++++++++++++------- frontend/src/search-suggester.ts | 4 ++-- frontend/src/utils/taxonomies.ts | 5 +++++ 4 files changed, 48 insertions(+), 11 deletions(-) diff --git a/frontend/src/mixins/history.ts b/frontend/src/mixins/history.ts index 65060767..3daa4613 100644 --- a/frontend/src/mixins/history.ts +++ b/frontend/src/mixins/history.ts @@ -53,17 +53,27 @@ const HISTORY_VALUES: Record< sortOptionId: history.sort_by, }; }, + // NOTE: this approach is a bit brittle, + // shan't we instead store selected facets in the URL directly? [HistorySearchParams.FACETS_FILTERS]: (history) => { if (!history.facetsFilters) { return {}; } // we split back the facetsFilters expression to its sub components // parameter value is facet1:(value1 OR value2) AND facet2:(value3 OR value4) + // but beware quotes: facet1:"en:value1" const selectedTermsByFacet = history.facetsFilters .split(QueryOperator.AND) .reduce((acc, filter) => { - const [key, value] = filter.split(':'); - acc[key] = removeParenthesis(value).split(QueryOperator.OR); + const [key, ...values] = filter.split(':'); + // keep last parts + let value = values.join(':'); + // remove parenthesis if any + value = value.replace(/^\((.*)\)$/, '$1'); + acc[key] = acc[key] || []; + value.split(QueryOperator.OR).forEach((value) => { + acc[key].push(removeParenthesis(value)); + }); return acc; }, {} as Record); diff --git a/frontend/src/search-facets.ts b/frontend/src/search-facets.ts index 6ccdc760..75709e1d 100644 --- a/frontend/src/search-facets.ts +++ b/frontend/src/search-facets.ts @@ -1,9 +1,13 @@ -import {LitElement, html, nothing, css} from 'lit'; +import {LitElement, html, nothing, css, PropertyValues} from 'lit'; import {customElement, property, queryAssignedNodes} from 'lit/decorators.js'; import {repeat} from 'lit/directives/repeat.js'; import {DebounceMixin} from './mixins/debounce'; import {SearchaliciousTermsMixin} from './mixins/suggestions-ctl'; -import {getTaxonomyName, removeLangFromTermId} from './utils/taxonomies'; +import { + getTaxonomyName, + removeLangFromTermId, + unquoteTerm, +} from './utils/taxonomies'; import {SearchActionMixin} from './mixins/search-action'; import {FACET_TERM_OTHER} from './utils/constants'; import {QueryOperator, SearchaliciousEvents} from './utils/enums'; @@ -30,6 +34,7 @@ interface FacetItem { interface FacetTerm extends FacetItem { count: number; + selected: boolean; } interface PresenceInfo { @@ -185,7 +190,7 @@ export class SearchaliciousFacet extends LitElement { @property() name = ''; - // the last search infor for my facet + // the last search infos for my facet @property({attribute: false}) // eslint-disable-next-line @typescript-eslint/no-explicit-any infos?: FacetInfo; @@ -294,6 +299,8 @@ export class SearchaliciousTermsFacet extends SearchActionMixin( * Set wether a term is selected or not */ override setTermSelected(checked: boolean, name: string) { + // remove quotes if needed + name = unquoteTerm(name); this.selectedTerms = { ...this.selectedTerms, ...{[name]: checked}, @@ -326,7 +333,7 @@ export class SearchaliciousTermsFacet extends SearchActionMixin( override setSelectedTerms(terms?: string[]) { this.selectedTerms = {}; for (const term of terms ?? []) { - this.selectedTerms[term] = true; + this.selectedTerms[unquoteTerm(term)] = true; } } @@ -337,9 +344,9 @@ export class SearchaliciousTermsFacet extends SearchActionMixin( let values = Object.keys(this.selectedTerms).filter( (key) => this.selectedTerms[key] ); - // add quotes if we have ":" in values + // add quotes if we have ":" or "-" in values values = values.map((value) => - value.includes(':') ? `"${value}"` : value + !value.match(/^\w+$/) ? `"${value}"` : value ); if (values.length === 0) { return undefined; @@ -427,7 +434,7 @@ export class SearchaliciousTermsFacet extends SearchActionMixin(
          @@ -455,6 +462,21 @@ export class SearchaliciousTermsFacet extends SearchActionMixin( search && this._launchSearchWithDebounce(); }; + protected override willUpdate(_changedProperties: PropertyValues): void { + super.willUpdate(_changedProperties); + if (_changedProperties.has('infos')) { + // recompute selectedTerms + this.selectedTerms = {}; + if (this.infos) { + this.infos.items.forEach((item) => { + if ((item as FacetTerm).selected) { + this.selectedTerms[item.key] = true; + } + }); + } + } + } + /** * Renders the facet content */ diff --git a/frontend/src/search-suggester.ts b/frontend/src/search-suggester.ts index 5b650993..a3ee1985 100644 --- a/frontend/src/search-suggester.ts +++ b/frontend/src/search-suggester.ts @@ -107,7 +107,7 @@ export class SearchaliciousTaxonomySuggester extends SearchaliciousTermsMixin( return this.terms.map((term) => ({ value: removeLangFromTermId(term.id), label: term.text, - id: term.taxonomy_name + '-' + term.id, + id: term.id, source: this, input: value, taxonomy: term.taxonomy_name, @@ -118,7 +118,7 @@ export class SearchaliciousTaxonomySuggester extends SearchaliciousTermsMixin( override async selectSuggestion(selected: SuggestionSelectionOption) { this.selectTermByTaxonomy( (selected as taxonomySelectionOption).taxonomy, - selected.value + selected.id ); } diff --git a/frontend/src/utils/taxonomies.ts b/frontend/src/utils/taxonomies.ts index 7baa7d44..2faecadc 100644 --- a/frontend/src/utils/taxonomies.ts +++ b/frontend/src/utils/taxonomies.ts @@ -14,3 +14,8 @@ export const getTaxonomyName = (taxonomy: string): string => { export const removeLangFromTermId = (termId: string): string => { return termId.replace(/^[a-z]{2}:/, ''); }; + +/** unquote a term */ +export const unquoteTerm = (term: string): string => { + return term.replace(/^"(.*)"$/, '$1'); +}; From 084bb62de0a4438b8d97b209553a6d2d2c97e446 Mon Sep 17 00:00:00 2001 From: Alex Garel Date: Tue, 29 Oct 2024 17:45:43 +0100 Subject: [PATCH 08/23] Revert "chore: add lit-element-context library" This reverts commit 9d47ab654d023a568856d8d92548322928e84ee9. --- frontend/package-lock.json | 310 ++++++------------------------------- frontend/package.json | 3 +- 2 files changed, 52 insertions(+), 261 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 4e89e1a7..0b14143a 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -12,8 +12,7 @@ "@lit-labs/preact-signals": "^1.0.2", "@lit/context": "^1.1.2", "@lit/localize": "^0.12.1", - "lit": "^3.0.0", - "lit-element-context": "^0.2.3" + "lit": "^3.0.0" }, "devDependencies": { "@custom-elements-manifest/analyzer": "^0.6.3", @@ -43,13 +42,12 @@ } }, "node_modules/@75lb/deep-merge": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@75lb/deep-merge/-/deep-merge-1.1.2.tgz", - "integrity": "sha512-08K9ou5VNbheZFxM5tDWoqjA3ImC50DiuuJ2tj1yEPRfkp8lLLg6XAaJ4On+a0yAXor/8ay5gHnAIshRM44Kpw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@75lb/deep-merge/-/deep-merge-1.1.1.tgz", + "integrity": "sha512-xvgv6pkMGBA6GwdyJbNAnDmfAIR/DfWhrj9jgWh3TY7gRm3KO46x/GPjRg6wJ0nOepwqrNxFfojebh0Df4h4Tw==", "dev": true, - "license": "MIT", "dependencies": { - "lodash": "^4.17.21", + "lodash.assignwith": "^4.2.0", "typical": "^7.1.1" }, "engines": { @@ -2288,7 +2286,8 @@ "node_modules/@open-wc/dedupe-mixin": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@open-wc/dedupe-mixin/-/dedupe-mixin-1.4.0.tgz", - "integrity": "sha512-Sj7gKl1TLcDbF7B6KUhtvr+1UCxdhMbNY5KxdU5IfMFWqL8oy1ZeAcCANjoB1TL0AJTcPmcCFsCbHf8X2jGDUA==" + "integrity": "sha512-Sj7gKl1TLcDbF7B6KUhtvr+1UCxdhMbNY5KxdU5IfMFWqL8oy1ZeAcCANjoB1TL0AJTcPmcCFsCbHf8X2jGDUA==", + "dev": true }, "node_modules/@open-wc/dev-server-hmr": { "version": "0.1.2-next.0", @@ -3427,16 +3426,15 @@ } }, "node_modules/@web/test-runner-commands/node_modules/@web/dev-server-core": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/@web/dev-server-core/-/dev-server-core-0.7.3.tgz", - "integrity": "sha512-GS+Ok6HiqNZOsw2oEv5V2OISZ2s/6icJodyGjUuD3RChr0G5HiESbKf2K8mZV4shTz9sRC9KSQf8qvno2gPKrQ==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@web/dev-server-core/-/dev-server-core-0.7.1.tgz", + "integrity": "sha512-alHd2j0f4e1ekqYDR8lWScrzR7D5gfsUZq3BP3De9bkFWM3AELINCmqqlVKmCtlkAdEc9VyQvNiEqrxraOdc2A==", "dev": true, - "license": "MIT", "dependencies": { "@types/koa": "^2.11.6", "@types/ws": "^7.4.0", "@web/parse5-utils": "^2.1.0", - "chokidar": "^4.0.1", + "chokidar": "^3.4.3", "clone": "^2.1.2", "es-module-lexer": "^1.0.0", "get-stream": "^6.0.0", @@ -3450,7 +3448,7 @@ "mime-types": "^2.1.27", "parse5": "^6.0.1", "picomatch": "^2.2.2", - "ws": "^7.5.10" + "ws": "^7.4.2" }, "engines": { "node": ">=18.0.0" @@ -3461,7 +3459,6 @@ "resolved": "https://registry.npmjs.org/@web/parse5-utils/-/parse5-utils-2.1.0.tgz", "integrity": "sha512-GzfK5disEJ6wEjoPwx8AVNwUe9gYIiwc+x//QYxYDAFKUp4Xb1OJAGLc2l2gVrSQmtPGLKrTRcW90Hv4pEq1qA==", "dev": true, - "license": "MIT", "dependencies": { "@types/parse5": "^6.0.1", "parse5": "^6.0.1" @@ -3471,11 +3468,10 @@ } }, "node_modules/@web/test-runner-commands/node_modules/@web/test-runner-core": { - "version": "0.13.4", - "resolved": "https://registry.npmjs.org/@web/test-runner-core/-/test-runner-core-0.13.4.tgz", - "integrity": "sha512-84E1025aUSjvZU1j17eCTwV7m5Zg3cZHErV3+CaJM9JPCesZwLraIa0ONIQ9w4KLgcDgJFw9UnJ0LbFf42h6tg==", + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/@web/test-runner-core/-/test-runner-core-0.13.1.tgz", + "integrity": "sha512-2hESALx/UFsAzK+ApWXAkFp2eCmwcs2yj1v4YjwV8F38sQumJ40P3px3BMjFwkOYDORtQOicW0RUeSw1g3BMLA==", "dev": true, - "license": "MIT", "dependencies": { "@babel/code-frame": "^7.12.11", "@types/babel__code-frame": "^7.0.2", @@ -3485,15 +3481,15 @@ "@types/istanbul-lib-coverage": "^2.0.3", "@types/istanbul-reports": "^3.0.0", "@web/browser-logs": "^0.4.0", - "@web/dev-server-core": "^0.7.3", - "chokidar": "^4.0.1", + "@web/dev-server-core": "^0.7.0", + "chokidar": "^3.4.3", "cli-cursor": "^3.1.0", "co-body": "^6.1.0", "convert-source-map": "^2.0.0", "debounce": "^1.2.0", "dependency-graph": "^0.11.0", "globby": "^11.0.1", - "internal-ip": "^6.2.0", + "ip": "^2.0.1", "istanbul-lib-coverage": "^3.0.0", "istanbul-lib-report": "^3.0.1", "istanbul-reports": "^3.0.2", @@ -3508,53 +3504,27 @@ "node": ">=18.0.0" } }, - "node_modules/@web/test-runner-commands/node_modules/chokidar": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz", - "integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "readdirp": "^4.0.1" - }, - "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/@web/test-runner-commands/node_modules/es-module-lexer": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz", - "integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==", - "dev": true, - "license": "MIT" + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.0.tgz", + "integrity": "sha512-pqrTKmwEIgafsYZAGw9kszYzmagcE/n4dbgwGWLEXg7J4QFJVQRBld8j3Q3GNez79jzxZshq0bcT962QHOghjw==", + "dev": true + }, + "node_modules/@web/test-runner-commands/node_modules/ip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.1.tgz", + "integrity": "sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ==", + "dev": true }, "node_modules/@web/test-runner-commands/node_modules/lru-cache": { "version": "8.0.5", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-8.0.5.tgz", "integrity": "sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA==", "dev": true, - "license": "ISC", "engines": { "node": ">=16.14" } }, - "node_modules/@web/test-runner-commands/node_modules/readdirp": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz", - "integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/@web/test-runner-core": { "version": "0.10.29", "resolved": "https://registry.npmjs.org/@web/test-runner-core/-/test-runner-core-0.10.29.tgz", @@ -3935,13 +3905,12 @@ } }, "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", "dev": true, - "license": "MIT", "dependencies": { - "fill-range": "^7.1.1" + "fill-range": "^7.0.1" }, "engines": { "node": ">=8" @@ -4755,19 +4724,6 @@ "node": ">=0.10.0" } }, - "node_modules/default-gateway": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz", - "integrity": "sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "execa": "^5.0.0" - }, - "engines": { - "node": ">= 10" - } - }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", @@ -5347,37 +5303,6 @@ "node": ">= 0.6" } }, - "node_modules/execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/execa/node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true, - "license": "ISC" - }, "node_modules/extract-zip": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", @@ -5487,11 +5412,10 @@ } }, "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", "dev": true, - "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -5970,16 +5894,6 @@ "node": ">= 6" } }, - "node_modules/human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10.17.0" - } - }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -6071,25 +5985,6 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true }, - "node_modules/internal-ip": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/internal-ip/-/internal-ip-6.2.0.tgz", - "integrity": "sha512-D8WGsR6yDt8uq7vDMu7mjcR+yRMm3dW8yufyChmszWRjcSHuxLBkR3GdS2HZAjodsaGuCvXeEJpueisXJULghg==", - "dev": true, - "license": "MIT", - "dependencies": { - "default-gateway": "^6.0.0", - "ipaddr.js": "^1.9.1", - "is-ip": "^3.1.0", - "p-event": "^4.2.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/internal-ip?sponsor=1" - } - }, "node_modules/intersection-observer": { "version": "0.12.2", "resolved": "https://registry.npmjs.org/intersection-observer/-/intersection-observer-0.12.2.tgz", @@ -6102,26 +5997,6 @@ "integrity": "sha512-cyRxvOEpNHNtchU3Ln9KC/auJgup87llfQpQ+t5ghoC/UhL16SWzbueiCsdTnWmqAWl7LadfuwhlqmtOaqMHdQ==", "dev": true }, - "node_modules/ip-regex": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-4.3.0.tgz", - "integrity": "sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -6221,19 +6096,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-ip": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-ip/-/is-ip-3.1.0.tgz", - "integrity": "sha512-35vd5necO7IitFPjd/YBeqwWnyDWbuLH9ZXQdMfDA8TEo7pv5X8yfrvVO3xbJbLUlERCMvf6X0hTUamQxCYJ9Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ip-regex": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/is-module": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", @@ -6245,7 +6107,6 @@ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, - "license": "MIT", "engines": { "node": ">=0.12.0" } @@ -6920,15 +6781,6 @@ "lit-html": "^3.1.2" } }, - "node_modules/lit-element-context": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/lit-element-context/-/lit-element-context-0.2.3.tgz", - "integrity": "sha512-x2qACI9d4t0M2elWjQ8e9xt7F6fxwx0iHzzpt5pod8CU4kvsdrEwUNtXYDX6DjJXKbpzSGgDJkRAdHyWgMfc4Q==", - "license": "MIT", - "dependencies": { - "@open-wc/dedupe-mixin": "^1.3.1" - } - }, "node_modules/lit-html": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.1.2.tgz", @@ -6958,6 +6810,12 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true }, + "node_modules/lodash.assignwith": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.assignwith/-/lodash.assignwith-4.2.0.tgz", + "integrity": "sha512-ZznplvbvtjK2gMvnQ1BR/zqPFZmS6jbK4p+6Up4xcRYA7yMIwxHCfbTcrYxXKzzqLsQ05eJPVznEW3tuwV7k1g==", + "dev": true + }, "node_modules/lodash.camelcase": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", @@ -7097,13 +6955,12 @@ } }, "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", "dev": true, - "license": "MIT", "dependencies": { - "braces": "^3.0.3", + "braces": "^3.0.2", "picomatch": "^2.3.1" }, "engines": { @@ -7312,19 +7169,6 @@ "node": ">=0.10.0" } }, - "node_modules/npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/object-inspect": { "version": "1.13.1", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", @@ -7410,32 +7254,6 @@ "node": ">= 0.8.0" } }, - "node_modules/p-event": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/p-event/-/p-event-4.2.0.tgz", - "integrity": "sha512-KXatOjCRXXkSePPb1Nbi0p0m+gQAwdlbhi4wQKJPI1HsMQS9g+Sqp2o+QHziPr7eYJyOZet836KoHEVM1mwOrQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-timeout": "^3.1.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-finally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -7466,19 +7284,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/p-timeout": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", - "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-finally": "^1.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/param-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", @@ -8180,11 +7985,10 @@ } }, "node_modules/rollup": { - "version": "2.79.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz", - "integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==", + "version": "2.79.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.1.tgz", + "integrity": "sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==", "dev": true, - "license": "MIT", "bin": { "rollup": "dist/bin/rollup" }, @@ -8578,16 +8382,6 @@ "node": ">=8" } }, - "node_modules/strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -8771,7 +8565,6 @@ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, - "license": "MIT", "dependencies": { "is-number": "^7.0.0" }, @@ -9349,11 +9142,10 @@ "dev": true }, "node_modules/ws": { - "version": "7.5.10", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", - "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "version": "7.5.9", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", + "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", "dev": true, - "license": "MIT", "engines": { "node": ">=8.3.0" }, diff --git a/frontend/package.json b/frontend/package.json index 4d57427f..99aecb0b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -47,8 +47,7 @@ "@lit-labs/preact-signals": "^1.0.2", "@lit/context": "^1.1.2", "@lit/localize": "^0.12.1", - "lit": "^3.0.0", - "lit-element-context": "^0.2.3" + "lit": "^3.0.0" }, "devDependencies": { "@custom-elements-manifest/analyzer": "^0.6.3", From 69c6600b2cd65135302e77f5d36dc7e02b4e5218 Mon Sep 17 00:00:00 2001 From: Alex Garel Date: Tue, 29 Oct 2024 17:47:34 +0100 Subject: [PATCH 09/23] chore: some js updates --- frontend/package-lock.json | 295 +++++++++++++++++++++++++++++++------ 1 file changed, 247 insertions(+), 48 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 0b14143a..615c4bc8 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -42,12 +42,13 @@ } }, "node_modules/@75lb/deep-merge": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@75lb/deep-merge/-/deep-merge-1.1.1.tgz", - "integrity": "sha512-xvgv6pkMGBA6GwdyJbNAnDmfAIR/DfWhrj9jgWh3TY7gRm3KO46x/GPjRg6wJ0nOepwqrNxFfojebh0Df4h4Tw==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@75lb/deep-merge/-/deep-merge-1.1.2.tgz", + "integrity": "sha512-08K9ou5VNbheZFxM5tDWoqjA3ImC50DiuuJ2tj1yEPRfkp8lLLg6XAaJ4On+a0yAXor/8ay5gHnAIshRM44Kpw==", "dev": true, + "license": "MIT", "dependencies": { - "lodash.assignwith": "^4.2.0", + "lodash": "^4.17.21", "typical": "^7.1.1" }, "engines": { @@ -3426,15 +3427,16 @@ } }, "node_modules/@web/test-runner-commands/node_modules/@web/dev-server-core": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/@web/dev-server-core/-/dev-server-core-0.7.1.tgz", - "integrity": "sha512-alHd2j0f4e1ekqYDR8lWScrzR7D5gfsUZq3BP3De9bkFWM3AELINCmqqlVKmCtlkAdEc9VyQvNiEqrxraOdc2A==", + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@web/dev-server-core/-/dev-server-core-0.7.3.tgz", + "integrity": "sha512-GS+Ok6HiqNZOsw2oEv5V2OISZ2s/6icJodyGjUuD3RChr0G5HiESbKf2K8mZV4shTz9sRC9KSQf8qvno2gPKrQ==", "dev": true, + "license": "MIT", "dependencies": { "@types/koa": "^2.11.6", "@types/ws": "^7.4.0", "@web/parse5-utils": "^2.1.0", - "chokidar": "^3.4.3", + "chokidar": "^4.0.1", "clone": "^2.1.2", "es-module-lexer": "^1.0.0", "get-stream": "^6.0.0", @@ -3448,7 +3450,7 @@ "mime-types": "^2.1.27", "parse5": "^6.0.1", "picomatch": "^2.2.2", - "ws": "^7.4.2" + "ws": "^7.5.10" }, "engines": { "node": ">=18.0.0" @@ -3459,6 +3461,7 @@ "resolved": "https://registry.npmjs.org/@web/parse5-utils/-/parse5-utils-2.1.0.tgz", "integrity": "sha512-GzfK5disEJ6wEjoPwx8AVNwUe9gYIiwc+x//QYxYDAFKUp4Xb1OJAGLc2l2gVrSQmtPGLKrTRcW90Hv4pEq1qA==", "dev": true, + "license": "MIT", "dependencies": { "@types/parse5": "^6.0.1", "parse5": "^6.0.1" @@ -3468,10 +3471,11 @@ } }, "node_modules/@web/test-runner-commands/node_modules/@web/test-runner-core": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/@web/test-runner-core/-/test-runner-core-0.13.1.tgz", - "integrity": "sha512-2hESALx/UFsAzK+ApWXAkFp2eCmwcs2yj1v4YjwV8F38sQumJ40P3px3BMjFwkOYDORtQOicW0RUeSw1g3BMLA==", + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/@web/test-runner-core/-/test-runner-core-0.13.4.tgz", + "integrity": "sha512-84E1025aUSjvZU1j17eCTwV7m5Zg3cZHErV3+CaJM9JPCesZwLraIa0ONIQ9w4KLgcDgJFw9UnJ0LbFf42h6tg==", "dev": true, + "license": "MIT", "dependencies": { "@babel/code-frame": "^7.12.11", "@types/babel__code-frame": "^7.0.2", @@ -3481,15 +3485,15 @@ "@types/istanbul-lib-coverage": "^2.0.3", "@types/istanbul-reports": "^3.0.0", "@web/browser-logs": "^0.4.0", - "@web/dev-server-core": "^0.7.0", - "chokidar": "^3.4.3", + "@web/dev-server-core": "^0.7.3", + "chokidar": "^4.0.1", "cli-cursor": "^3.1.0", "co-body": "^6.1.0", "convert-source-map": "^2.0.0", "debounce": "^1.2.0", "dependency-graph": "^0.11.0", "globby": "^11.0.1", - "ip": "^2.0.1", + "internal-ip": "^6.2.0", "istanbul-lib-coverage": "^3.0.0", "istanbul-lib-report": "^3.0.1", "istanbul-reports": "^3.0.2", @@ -3504,27 +3508,53 @@ "node": ">=18.0.0" } }, - "node_modules/@web/test-runner-commands/node_modules/es-module-lexer": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.0.tgz", - "integrity": "sha512-pqrTKmwEIgafsYZAGw9kszYzmagcE/n4dbgwGWLEXg7J4QFJVQRBld8j3Q3GNez79jzxZshq0bcT962QHOghjw==", - "dev": true + "node_modules/@web/test-runner-commands/node_modules/chokidar": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz", + "integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } }, - "node_modules/@web/test-runner-commands/node_modules/ip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.1.tgz", - "integrity": "sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ==", - "dev": true + "node_modules/@web/test-runner-commands/node_modules/es-module-lexer": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz", + "integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==", + "dev": true, + "license": "MIT" }, "node_modules/@web/test-runner-commands/node_modules/lru-cache": { "version": "8.0.5", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-8.0.5.tgz", "integrity": "sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA==", "dev": true, + "license": "ISC", "engines": { "node": ">=16.14" } }, + "node_modules/@web/test-runner-commands/node_modules/readdirp": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz", + "integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@web/test-runner-core": { "version": "0.10.29", "resolved": "https://registry.npmjs.org/@web/test-runner-core/-/test-runner-core-0.10.29.tgz", @@ -3905,12 +3935,13 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, + "license": "MIT", "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -4724,6 +4755,19 @@ "node": ">=0.10.0" } }, + "node_modules/default-gateway": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz", + "integrity": "sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "execa": "^5.0.0" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", @@ -5303,6 +5347,37 @@ "node": ">= 0.6" } }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, "node_modules/extract-zip": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", @@ -5412,10 +5487,11 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, + "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -5894,6 +5970,16 @@ "node": ">= 6" } }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -5985,6 +6071,25 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true }, + "node_modules/internal-ip": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/internal-ip/-/internal-ip-6.2.0.tgz", + "integrity": "sha512-D8WGsR6yDt8uq7vDMu7mjcR+yRMm3dW8yufyChmszWRjcSHuxLBkR3GdS2HZAjodsaGuCvXeEJpueisXJULghg==", + "dev": true, + "license": "MIT", + "dependencies": { + "default-gateway": "^6.0.0", + "ipaddr.js": "^1.9.1", + "is-ip": "^3.1.0", + "p-event": "^4.2.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/internal-ip?sponsor=1" + } + }, "node_modules/intersection-observer": { "version": "0.12.2", "resolved": "https://registry.npmjs.org/intersection-observer/-/intersection-observer-0.12.2.tgz", @@ -5997,6 +6102,26 @@ "integrity": "sha512-cyRxvOEpNHNtchU3Ln9KC/auJgup87llfQpQ+t5ghoC/UhL16SWzbueiCsdTnWmqAWl7LadfuwhlqmtOaqMHdQ==", "dev": true }, + "node_modules/ip-regex": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-4.3.0.tgz", + "integrity": "sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -6096,6 +6221,19 @@ "node": ">=0.10.0" } }, + "node_modules/is-ip": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-ip/-/is-ip-3.1.0.tgz", + "integrity": "sha512-35vd5necO7IitFPjd/YBeqwWnyDWbuLH9ZXQdMfDA8TEo7pv5X8yfrvVO3xbJbLUlERCMvf6X0hTUamQxCYJ9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-regex": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/is-module": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", @@ -6107,6 +6245,7 @@ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.12.0" } @@ -6810,12 +6949,6 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true }, - "node_modules/lodash.assignwith": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.assignwith/-/lodash.assignwith-4.2.0.tgz", - "integrity": "sha512-ZznplvbvtjK2gMvnQ1BR/zqPFZmS6jbK4p+6Up4xcRYA7yMIwxHCfbTcrYxXKzzqLsQ05eJPVznEW3tuwV7k1g==", - "dev": true - }, "node_modules/lodash.camelcase": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", @@ -6955,12 +7088,13 @@ } }, "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, + "license": "MIT", "dependencies": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" }, "engines": { @@ -7169,6 +7303,19 @@ "node": ">=0.10.0" } }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/object-inspect": { "version": "1.13.1", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", @@ -7254,6 +7401,32 @@ "node": ">= 0.8.0" } }, + "node_modules/p-event": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/p-event/-/p-event-4.2.0.tgz", + "integrity": "sha512-KXatOjCRXXkSePPb1Nbi0p0m+gQAwdlbhi4wQKJPI1HsMQS9g+Sqp2o+QHziPr7eYJyOZet836KoHEVM1mwOrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-timeout": "^3.1.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -7284,6 +7457,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-timeout": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", + "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-finally": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/param-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", @@ -7985,10 +8171,11 @@ } }, "node_modules/rollup": { - "version": "2.79.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.1.tgz", - "integrity": "sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==", + "version": "2.79.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz", + "integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==", "dev": true, + "license": "MIT", "bin": { "rollup": "dist/bin/rollup" }, @@ -8382,6 +8569,16 @@ "node": ">=8" } }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -8565,6 +8762,7 @@ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, + "license": "MIT", "dependencies": { "is-number": "^7.0.0" }, @@ -9142,10 +9340,11 @@ "dev": true }, "node_modules/ws": { - "version": "7.5.9", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", - "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8.3.0" }, From 87e05f3dddd1db2e703e2441582af9affad25557 Mon Sep 17 00:00:00 2001 From: Alex Garel Date: Tue, 29 Oct 2024 17:47:43 +0100 Subject: [PATCH 10/23] =?UTF-8?q?docs:=C2=A0added=20comment?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/search-suggester.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/frontend/src/search-suggester.ts b/frontend/src/search-suggester.ts index a3ee1985..65cf3402 100644 --- a/frontend/src/search-suggester.ts +++ b/frontend/src/search-suggester.ts @@ -40,6 +40,9 @@ export class SearchaliciousSuggester extends LitElement { } } +/** + * Selection Option for taxonomy suggestions + */ type taxonomySelectionOption = SuggestionSelectionOption & { /** * taxonomy related to this suggestion From 02f48900f4ebc6e3bbee5b3b9ab18cef753206f3 Mon Sep 17 00:00:00 2001 From: Alex Garel Date: Thu, 31 Oct 2024 17:27:05 +0100 Subject: [PATCH 11/23] Update app/query.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Stéphane Gigandet --- app/query.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/query.py b/app/query.py index d4555d2f..ed97aa55 100644 --- a/app/query.py +++ b/app/query.py @@ -331,7 +331,7 @@ def build_completion_query( :param q: the user autocomplete query :param taxonomy_names: a list of taxonomies we want to search in - :param langs: the language we want search in + :param langs: the languages we want search in :param size: number of results to return :param config: the index configuration to use :param fuzziness: fuzziness parameter for completion query From 61e242affb5490eaf960dc609ec0bcf20f6eac0a Mon Sep 17 00:00:00 2001 From: Alex Garel Date: Thu, 31 Oct 2024 17:30:53 +0100 Subject: [PATCH 12/23] docs: add searchalicious-suggest to web-components ref --- docs/users/ref-web-components.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/users/ref-web-components.md b/docs/users/ref-web-components.md index 4afe261e..a4f56fb4 100644 --- a/docs/users/ref-web-components.md +++ b/docs/users/ref-web-components.md @@ -39,6 +39,10 @@ You'll need it if you mix multiple search bars in the same page. +### searchalicious-taxonomy-suggest + + + ### searchalicious-facets From 9b04e22b8e8f60c93e49495d4b4f5bcc45a9ee2c Mon Sep 17 00:00:00 2001 From: Alex Garel Date: Thu, 31 Oct 2024 18:15:34 +0100 Subject: [PATCH 13/23] fix: use pydantic for taxonomy to be able to generate docs --- app/taxonomy.py | 29 ++++++++--------------------- 1 file changed, 8 insertions(+), 21 deletions(-) diff --git a/app/taxonomy.py b/app/taxonomy.py index 46f7222b..f7829b79 100644 --- a/app/taxonomy.py +++ b/app/taxonomy.py @@ -23,7 +23,7 @@ logger = get_logger(__name__) -class TaxonomyNode: +class TaxonomyNode(BaseModel): """A taxonomy element. Each node has 0+ parents and 0+ children. Each node has the following @@ -39,25 +39,12 @@ class TaxonomyNode: for this language """ - __slots__ = ("id", "names", "parents", "children", "synonyms", "properties") - - def __init__( - self, - identifier: str, - names: Dict[str, str], - synonyms: Optional[Dict[str, List[str]]], - properties: Optional[Dict[str, Any]] = None, - ): - self.id: str = identifier - self.names: Dict[str, str] = names - self.parents: List["TaxonomyNode"] = [] - self.children: List["TaxonomyNode"] = [] - self.properties = properties or {} - - if synonyms: - self.synonyms = synonyms - else: - self.synonyms = {} + id: str + names: Dict[str, str] + parents: List["TaxonomyNode"] = [] + children: List["TaxonomyNode"] = [] + synonyms: Dict[str, List[str]] = {} + properties: Dict[str, Any] = {} def is_child_of(self, item: "TaxonomyNode") -> bool: """Return True if `item` is a child of `self` in the taxonomy.""" @@ -276,7 +263,7 @@ def from_dict(cls, name: str, data: JSONType) -> "Taxonomy": for key, key_data in data.items(): if key not in taxonomy: node = TaxonomyNode( - identifier=key, + id=key, names=key_data.get("name", {}), synonyms=key_data.get("synonyms", None), properties={ From 24dfd4c3d63213f086b52a0d1bdeac879b27149b Mon Sep 17 00:00:00 2001 From: Alex Garel Date: Wed, 6 Nov 2024 15:36:27 +0100 Subject: [PATCH 14/23] fix: fix suggester langs on facets completion --- .../src/interfaces/search-ctl-interfaces.ts | 7 +++ frontend/src/mixins/search-ctl-getter.ts | 43 +++++++++++++++++++ frontend/src/mixins/suggestion-selection.ts | 1 + frontend/src/mixins/suggestions-ctl.ts | 10 ++++- frontend/src/search-autocomplete.ts | 2 +- frontend/src/search-facets.ts | 14 +++++- frontend/src/search-suggester.ts | 10 +++++ 7 files changed, 83 insertions(+), 4 deletions(-) create mode 100644 frontend/src/mixins/search-ctl-getter.ts diff --git a/frontend/src/interfaces/search-ctl-interfaces.ts b/frontend/src/interfaces/search-ctl-interfaces.ts index 89f1db75..2beb61dc 100644 --- a/frontend/src/interfaces/search-ctl-interfaces.ts +++ b/frontend/src/interfaces/search-ctl-interfaces.ts @@ -25,3 +25,10 @@ export interface SearchaliciousSearchInterface _facetsFilters(): string; resetFacets(launchSearch?: boolean): void; } + +/** + * An interface to be able to get the search controller corresponding to a component + */ +export interface SearchCtlGetInterface { + getSearchCtl(): SearchaliciousSearchInterface; +} diff --git a/frontend/src/mixins/search-ctl-getter.ts b/frontend/src/mixins/search-ctl-getter.ts new file mode 100644 index 00000000..196c2dd3 --- /dev/null +++ b/frontend/src/mixins/search-ctl-getter.ts @@ -0,0 +1,43 @@ +import {Constructor} from './utils'; +import { + SearchaliciousSearchInterface, + SearchCtlGetInterface, +} from '../interfaces/search-ctl-interfaces'; + +/** + * Some component may want to refer to the corresponding search controller instance + * + * This mixin provides a getter to get the corresponding search controller instance + */ +export const SearchCtlGetMixin = >( + superClass: T +) => { + class SearchCtlGetMixinClass extends superClass { + get searchName(): string { + throw Error('searchName attribute must be implemented in base class'); + } + + _searchCtl_cache: SearchaliciousSearchInterface | undefined; + + /** get corresponding search bar instance */ + getSearchCtl(): SearchaliciousSearchInterface { + if (!this._searchCtl_cache) { + let searchCtl: SearchaliciousSearchInterface | undefined = undefined; + document.querySelectorAll(`searchalicious-bar`).forEach((item) => { + const candidate = item as SearchaliciousSearchInterface; + if (candidate.name === this.searchName) { + searchCtl = candidate; + } + }); + if (searchCtl == null) { + throw new Error(`No search bar found for ${this.searchName}`); + } + // optimize + this._searchCtl_cache = searchCtl; + } + return this._searchCtl_cache; + } + } + + return SearchCtlGetMixinClass as Constructor & T; +}; diff --git a/frontend/src/mixins/suggestion-selection.ts b/frontend/src/mixins/suggestion-selection.ts index 8ac6fa80..b66f407a 100644 --- a/frontend/src/mixins/suggestion-selection.ts +++ b/frontend/src/mixins/suggestion-selection.ts @@ -12,6 +12,7 @@ export interface SuggestionSelectionMixinInterface inputName: string; options: SuggestionSelectionOption[]; selectedOption: SuggestionSelectionOption | undefined; + inputValue: string; visible: boolean; isLoading: boolean; currentIndex: number; diff --git a/frontend/src/mixins/suggestions-ctl.ts b/frontend/src/mixins/suggestions-ctl.ts index 7f32acfb..59126747 100644 --- a/frontend/src/mixins/suggestions-ctl.ts +++ b/frontend/src/mixins/suggestions-ctl.ts @@ -61,8 +61,14 @@ export const SearchaliciousTermsMixin = >( @property({attribute: 'base-url'}) taxonomiesBaseUrl = '/'; - @property() - langs = 'en'; + /** + * langs to get suggestion from. + * + * Must be implementetd in child class + */ + get langs(): string { + throw new Error('langs must be defined in child class'); + } /** * build URL to search taxonomies terms from input diff --git a/frontend/src/search-autocomplete.ts b/frontend/src/search-autocomplete.ts index 08dba06f..675889f9 100644 --- a/frontend/src/search-autocomplete.ts +++ b/frontend/src/search-autocomplete.ts @@ -158,7 +158,7 @@ export class SearchaliciousAutocomplete extends SuggestionSelectionMixin( />
            ${this.isLoading diff --git a/frontend/src/search-facets.ts b/frontend/src/search-facets.ts index 75709e1d..8e7d61cf 100644 --- a/frontend/src/search-facets.ts +++ b/frontend/src/search-facets.ts @@ -16,6 +16,7 @@ import {msg, localized} from '@lit/localize'; import {WHITE_PANEL_STYLE} from './styles'; import {SearchaliciousFacetsInterface} from './interfaces/facets-interfaces'; import {SearchaliciousResultCtlMixin} from './mixins/search-results-ctl'; +import {SearchCtlGetMixin} from './mixins/search-ctl-getter'; interface FacetsInfos { [key: string]: FacetInfo; @@ -248,7 +249,9 @@ export class SearchaliciousFacet extends LitElement { @customElement('searchalicious-facet-terms') @localized() export class SearchaliciousTermsFacet extends SearchActionMixin( - SearchaliciousTermsMixin(DebounceMixin(SearchaliciousFacet)) + SearchaliciousTermsMixin( + SearchCtlGetMixin(DebounceMixin(SearchaliciousFacet)) + ) ) { static override styles = [ WHITE_PANEL_STYLE, @@ -291,6 +294,15 @@ export class SearchaliciousTermsFacet extends SearchActionMixin( @property({attribute: 'show-other', type: Boolean}) showOther = false; + /** + * Interrogation language for suggestion + * + * We use the same as the search-bar + */ + override get langs() { + return this.getSearchCtl().langs; + } + _launchSearchWithDebounce = () => this.debounce(() => { this._launchSearch(); diff --git a/frontend/src/search-suggester.ts b/frontend/src/search-suggester.ts index 65cf3402..f4e40b58 100644 --- a/frontend/src/search-suggester.ts +++ b/frontend/src/search-suggester.ts @@ -74,6 +74,16 @@ export class SearchaliciousTaxonomySuggester extends SearchaliciousTermsMixin( @property({type: Number}) fuzziness = 2; + /** + * language in which suggestions are to be found + */ + override get langs(): string { + if (!this.searchCtl) { + throw Error('Asking for langs while searchCtl is not yet registered'); + } + return this.searchCtl.langs; + } + /** * taxonomies attribute but as an array of String */ From 38e242d26d7a25c9b77054ecfd8f89aec34307fb Mon Sep 17 00:00:00 2001 From: Alex Garel Date: Wed, 6 Nov 2024 16:15:50 +0100 Subject: [PATCH 15/23] =?UTF-8?q?feat:=C2=A0add=20input=20to=20suggestion?= =?UTF-8?q?=20and=20fix=20a=20small=20bug=20on=20completion=20text=20that?= =?UTF-8?q?=20was=20not=20removed=20from=20suggestion=20input?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api.py | 2 +- app/postprocessing.py | 3 ++- frontend/src/mixins/suggestions-ctl.ts | 1 + frontend/src/search-autocomplete.ts | 1 + frontend/src/search-bar.ts | 1 + frontend/src/search-facets.ts | 2 +- 6 files changed, 7 insertions(+), 3 deletions(-) diff --git a/app/api.py b/app/api.py index ed09c7ef..079aa7d2 100644 --- a/app/api.py +++ b/app/api.py @@ -183,7 +183,7 @@ def taxonomy_autocomplete( detail="taxonomy index not found, taxonomies need to be imported first", ) - response = process_taxonomy_completion_response(es_response) + response = process_taxonomy_completion_response(es_response, q) return { **response, diff --git a/app/postprocessing.py b/app/postprocessing.py index 35c00ff1..ef8ea583 100644 --- a/app/postprocessing.py +++ b/app/postprocessing.py @@ -62,7 +62,7 @@ def load_result_processor(config: IndexConfig) -> BaseResultProcessor | None: return result_processor_cls(config) -def process_taxonomy_completion_response(response: Response) -> JSONType: +def process_taxonomy_completion_response(response: Response, input: str) -> JSONType: output = {"took": response.took, "timed_out": response.timed_out} options = [] ids = set() @@ -78,6 +78,7 @@ def process_taxonomy_completion_response(response: Response) -> JSONType: "id": option._source["id"], "text": option.text, "score": option._score, + "input": input, "taxonomy_name": option._source["taxonomy_name"], } options.append(result) diff --git a/frontend/src/mixins/suggestions-ctl.ts b/frontend/src/mixins/suggestions-ctl.ts index 59126747..a7cb0ca1 100644 --- a/frontend/src/mixins/suggestions-ctl.ts +++ b/frontend/src/mixins/suggestions-ctl.ts @@ -9,6 +9,7 @@ import {VersioningMixin, VersioningMixinInterface} from './versioning'; export type TermOption = { id: string; text: string; + input: string; taxonomy_name: string; }; diff --git a/frontend/src/search-autocomplete.ts b/frontend/src/search-autocomplete.ts index 675889f9..1143e2eb 100644 --- a/frontend/src/search-autocomplete.ts +++ b/frontend/src/search-autocomplete.ts @@ -136,6 +136,7 @@ export class SearchaliciousAutocomplete extends SuggestionSelectionMixin( ); this.dispatchEvent(inputEvent); this.resetInput(selectedOption); + this.selectedOption = undefined; } /** diff --git a/frontend/src/search-bar.ts b/frontend/src/search-bar.ts index c3c77c91..a02d9704 100644 --- a/frontend/src/search-bar.ts +++ b/frontend/src/search-bar.ts @@ -187,6 +187,7 @@ export class SearchaliciousBar extends SuggestionSelectionMixin( const selectedOption = this.selectedOption as SuggestOption; selectedOption!.source.selectSuggestion(selectedOption); this.resetInput(this.selectedOption); + this.selectedOption = undefined; this.query = ''; // not sure if we should instead put the value of remaining input } else { this.query = this.selectedOption?.value || ''; diff --git a/frontend/src/search-facets.ts b/frontend/src/search-facets.ts index 8e7d61cf..a202940c 100644 --- a/frontend/src/search-facets.ts +++ b/frontend/src/search-facets.ts @@ -404,7 +404,7 @@ export class SearchaliciousTermsFacet extends SearchActionMixin( value: removeLangFromTermId(term.id), label: term.text, id: term.id, - input: term.text, + input: term.input, }; }); From bd63cb90183b3fc88ea4819d47fb38c82c769cae Mon Sep 17 00:00:00 2001 From: Alex Garel Date: Wed, 6 Nov 2024 16:33:20 +0100 Subject: [PATCH 16/23] fix: no language prefix in suggestion because it might not work from some pseudo taxonomized fields --- frontend/src/search-suggester.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/search-suggester.ts b/frontend/src/search-suggester.ts index f4e40b58..9112300e 100644 --- a/frontend/src/search-suggester.ts +++ b/frontend/src/search-suggester.ts @@ -118,6 +118,7 @@ export class SearchaliciousTaxonomySuggester extends SearchaliciousTermsMixin( override async getSuggestions(value: string): Promise { return this.getTaxonomiesTerms(value, this.taxonomiesList).then(() => { return this.terms.map((term) => ({ + // we need to remove lang for some pseudo taxonomy fields don't have them… value: removeLangFromTermId(term.id), label: term.text, id: term.id, @@ -131,7 +132,7 @@ export class SearchaliciousTaxonomySuggester extends SearchaliciousTermsMixin( override async selectSuggestion(selected: SuggestionSelectionOption) { this.selectTermByTaxonomy( (selected as taxonomySelectionOption).taxonomy, - selected.id + selected.value ); } From b398d3c9e0a06341372f2f5a7de30c29661eaa5f Mon Sep 17 00:00:00 2001 From: Alex Garel Date: Wed, 6 Nov 2024 18:38:17 +0100 Subject: [PATCH 17/23] =?UTF-8?q?feat:=C2=A0add=20suggestion=20name,=20not?= =?UTF-8?q?=20only=20matched=20text?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/_import.py | 28 ++++++++++++++------------ app/api.py | 2 +- app/postprocessing.py | 6 +++++- app/query.py | 2 +- app/taxonomy.py | 9 +++++++-- frontend/src/mixins/suggestions-ctl.ts | 10 +++++++++ frontend/src/search-facets.ts | 2 +- frontend/src/search-suggester.ts | 2 +- 8 files changed, 41 insertions(+), 20 deletions(-) diff --git a/app/_import.py b/app/_import.py index 51fc49fd..968ce71a 100644 --- a/app/_import.py +++ b/app/_import.py @@ -275,23 +275,25 @@ def gen_taxonomy_documents( continue # skip this entry node = result.node names = { - lang: lang_names - for lang, lang_names in node.names.items() - if lang in supported_langs + lang: lang_name + for lang, lang_name in node.names.items() + if lang in supported_langs and lang_name } synonyms: dict[str, set[str]] = { lang: set(node.synonyms.get(lang) or []) for lang in node.synonyms if lang in supported_langs } - for lang, lang_names in names.items(): - if lang_names: - if not isinstance(lang_names, str): - import pdb - - pdb.set_trace() - synonyms.setdefault(lang, set()).add(lang_names) - + for lang, lang_name in names.items(): + if lang_name: + synonyms.setdefault(lang, set()).add(lang_name) + # put the name as first synonym and order by length + synonyms_list: dict[str, list[str]] = {} + for lang, lang_synonyms in synonyms.items(): + filtered_synonyms = filter(lambda s: s, lang_synonyms) + synonyms_list[lang] = sorted( + filtered_synonyms, key=lambda s: 0 if s == names[lang] else len(s) + ) yield { "_index": next_index, "_source": { @@ -300,10 +302,10 @@ def gen_taxonomy_documents( "name": names, "synonyms": { lang: { - "input": list(lang_synonyms), + "input": lang_synonyms, "weight": max(100 - len(node.id), 0), } - for lang, lang_synonyms in synonyms.items() + for lang, lang_synonyms in synonyms_list.items() }, }, } diff --git a/app/api.py b/app/api.py index 079aa7d2..de90cb88 100644 --- a/app/api.py +++ b/app/api.py @@ -183,7 +183,7 @@ def taxonomy_autocomplete( detail="taxonomy index not found, taxonomies need to be imported first", ) - response = process_taxonomy_completion_response(es_response, q) + response = process_taxonomy_completion_response(es_response, q, langs.split(",")) return { **response, diff --git a/app/postprocessing.py b/app/postprocessing.py index ef8ea583..69ce897a 100644 --- a/app/postprocessing.py +++ b/app/postprocessing.py @@ -62,10 +62,13 @@ def load_result_processor(config: IndexConfig) -> BaseResultProcessor | None: return result_processor_cls(config) -def process_taxonomy_completion_response(response: Response, input: str) -> JSONType: +def process_taxonomy_completion_response( + response: Response, input: str, langs: list[str] +) -> JSONType: output = {"took": response.took, "timed_out": response.timed_out} options = [] ids = set() + lang = langs[0] for suggestion_id in dir(response.suggest): if not suggestion_id.startswith("taxonomy_suggest_"): continue @@ -77,6 +80,7 @@ def process_taxonomy_completion_response(response: Response, input: str) -> JSON result = { "id": option._source["id"], "text": option.text, + "name": getattr(option._source["name"], lang, ""), "score": option._score, "input": input, "taxonomy_name": option._source["taxonomy_name"], diff --git a/app/query.py b/app/query.py index ed97aa55..6d9a7d87 100644 --- a/app/query.py +++ b/app/query.py @@ -355,7 +355,7 @@ def build_completion_query( completion=completion_clause, ) # limit returned fields - # query.source(fields=["id", "taxonomy_name"]) + query.source(fields=["id", "taxonomy_name", "name"]) return query diff --git a/app/taxonomy.py b/app/taxonomy.py index f7829b79..5a67ce33 100644 --- a/app/taxonomy.py +++ b/app/taxonomy.py @@ -135,6 +135,11 @@ def __repr__(self): return "" % self.id +def purge_none_values(d: Dict[str, str | None]) -> Dict[str, str]: + """Remove None values from a dict.""" + return {k: v for k, v in d.items() if v is not None} + + class Taxonomy: """A class representing a taxonomy. @@ -264,8 +269,8 @@ def from_dict(cls, name: str, data: JSONType) -> "Taxonomy": if key not in taxonomy: node = TaxonomyNode( id=key, - names=key_data.get("name", {}), - synonyms=key_data.get("synonyms", None), + names=purge_none_values(key_data.get("name", {})), + synonyms=key_data.get("synonyms", {}), properties={ k: v for k, v in key_data.items() diff --git a/frontend/src/mixins/suggestions-ctl.ts b/frontend/src/mixins/suggestions-ctl.ts index a7cb0ca1..4367c264 100644 --- a/frontend/src/mixins/suggestions-ctl.ts +++ b/frontend/src/mixins/suggestions-ctl.ts @@ -10,6 +10,7 @@ export type TermOption = { id: string; text: string; input: string; + name: string; taxonomy_name: string; }; @@ -30,6 +31,8 @@ export interface SearchaliciousTaxonomiesInterface taxonomiesBaseUrl: string; langs: string; + termLabel(term: TermOption): string; + /** * Method to get taxonomies terms. * @param {string} q - The query string. @@ -71,6 +74,13 @@ export const SearchaliciousTermsMixin = >( throw new Error('langs must be defined in child class'); } + termLabel(term: TermOption): string { + return ( + term.text + + (term.name && term.name != term.text ? ` (${term.name})` : '') + ); + } + /** * build URL to search taxonomies terms from input * @param {string} q - The query string. diff --git a/frontend/src/search-facets.ts b/frontend/src/search-facets.ts index a202940c..8cf75cdf 100644 --- a/frontend/src/search-facets.ts +++ b/frontend/src/search-facets.ts @@ -402,7 +402,7 @@ export class SearchaliciousTermsFacet extends SearchActionMixin( const options = (this.terms || []).map((term) => { return { value: removeLangFromTermId(term.id), - label: term.text, + label: this.termLabel(term), id: term.id, input: term.input, }; diff --git a/frontend/src/search-suggester.ts b/frontend/src/search-suggester.ts index 9112300e..a9c227d6 100644 --- a/frontend/src/search-suggester.ts +++ b/frontend/src/search-suggester.ts @@ -120,7 +120,7 @@ export class SearchaliciousTaxonomySuggester extends SearchaliciousTermsMixin( return this.terms.map((term) => ({ // we need to remove lang for some pseudo taxonomy fields don't have them… value: removeLangFromTermId(term.id), - label: term.text, + label: this.termLabel(term), id: term.id, source: this, input: value, From dd15ff809458415b795bf8863570497ed6f870fb Mon Sep 17 00:00:00 2001 From: Alex Garel Date: Wed, 6 Nov 2024 19:32:40 +0100 Subject: [PATCH 18/23] fix: translations for facets items, also if we don't have language prefix --- app/taxonomy_es.py | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/app/taxonomy_es.py b/app/taxonomy_es.py index 89e293a5..e8e4e7a2 100644 --- a/app/taxonomy_es.py +++ b/app/taxonomy_es.py @@ -23,18 +23,40 @@ def get_taxonomy_names( ) -> dict[tuple[str, str], dict[str, str]]: """Given a set of terms in different taxonomies, return their names""" filters = [] + no_lang_prefix_ids = {id_ for id_, _ in items if ":" not in id_} for id, taxonomy_name in items: + # we may not have lang prefix, in this case blind match them + id_term = ( + Q("term", id=id) + if id not in no_lang_prefix_ids + else Q("wildcard", id={"value": f"*:{id}"}) + ) # match one term - filters.append(Q("term", id=id) & Q("term", taxonomy_name=taxonomy_name)) + filters.append(id_term & Q("term", taxonomy_name=taxonomy_name)) query = ( Search(index=config.taxonomy.index.name) .filter("bool", should=filters, minimum_should_match=1) .params(size=len(filters)) ) - return { - (result.id, result.taxonomy_name): result.name.to_dict() - for result in query.execute().hits + results = query.execute().hits + # some id needs to be replaced by a value + no_lang_prefix = { + result.id: result.id.split(":", 1)[-1] + for result in results + if result.id.split(":", 1)[-1] in no_lang_prefix_ids } + translations = { + (result.id, result.taxonomy_name): result.name.to_dict() for result in results + } + # add values without prefix, because we may have some + translations.update( + { + (no_lang_prefix[result.id], result.taxonomy_name): result.name.to_dict() + for result in results + if no_lang_prefix[result.id] in no_lang_prefix_ids + } + ) + return translations def _normalize_synonym(token: str) -> str: From 8b34dd3c2f7b7c24085c9d22c09119eaa3f8a135 Mon Sep 17 00:00:00 2001 From: Alex Garel Date: Wed, 6 Nov 2024 19:42:31 +0100 Subject: [PATCH 19/23] =?UTF-8?q?fix:=C2=A0fix=20in=20taxonomy=5Fes=20tran?= =?UTF-8?q?slations?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/taxonomy_es.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/app/taxonomy_es.py b/app/taxonomy_es.py index e8e4e7a2..6d547129 100644 --- a/app/taxonomy_es.py +++ b/app/taxonomy_es.py @@ -40,11 +40,7 @@ def get_taxonomy_names( ) results = query.execute().hits # some id needs to be replaced by a value - no_lang_prefix = { - result.id: result.id.split(":", 1)[-1] - for result in results - if result.id.split(":", 1)[-1] in no_lang_prefix_ids - } + no_lang_prefix = {result.id: result.id.split(":", 1)[-1] for result in results} translations = { (result.id, result.taxonomy_name): result.name.to_dict() for result in results } From eaedc2e5e1e065e34572f5cb15256270f7c4f3d7 Mon Sep 17 00:00:00 2001 From: Alex Garel Date: Wed, 6 Nov 2024 19:42:53 +0100 Subject: [PATCH 20/23] fix: no completion query if input is empty --- frontend/src/mixins/suggestions-ctl.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/frontend/src/mixins/suggestions-ctl.ts b/frontend/src/mixins/suggestions-ctl.ts index 4367c264..46c42db7 100644 --- a/frontend/src/mixins/suggestions-ctl.ts +++ b/frontend/src/mixins/suggestions-ctl.ts @@ -102,6 +102,9 @@ export const SearchaliciousTermsMixin = >( q: string, taxonomyNames: string[] ): Promise { + if (!q) { + return Promise.resolve({options: []}); + } this.isTermsLoading = true; // get the version of the terms for each taxonomy const version = this.incrementVersion(); From 63afe4384f892a4c462e038209817db072f317d0 Mon Sep 17 00:00:00 2001 From: Alex Garel Date: Thu, 7 Nov 2024 17:26:24 +0100 Subject: [PATCH 21/23] ci: grouping logs --- scripts/build_mkdocs.sh | 6 +++++- scripts/build_schema.sh | 4 ++++ scripts/build_sphinx.sh | 4 ++++ scripts/generate_doc.sh | 6 ++++++ 4 files changed, 19 insertions(+), 1 deletion(-) diff --git a/scripts/build_mkdocs.sh b/scripts/build_mkdocs.sh index 27363b55..0b14417f 100755 --- a/scripts/build_mkdocs.sh +++ b/scripts/build_mkdocs.sh @@ -1,4 +1,6 @@ #!/usr/bin/env bash +echo '::group::{build_mkdocs}' + set -e # Renders markdown doc in docs to html in gh_pages @@ -12,4 +14,6 @@ docker build --build-arg "USER_UID=$UID" --build-arg "USER_GID=$GID" --tag 'mkdo docker run --rm \ -e USER_ID=$UID -e GROUP_ID=$GID \ -v $(pwd):/app -w /app \ - mkdocs-builder build --strict \ No newline at end of file + mkdocs-builder build --strict + +echo "::endgroup::" \ No newline at end of file diff --git a/scripts/build_schema.sh b/scripts/build_schema.sh index 87ba91a2..12c99b0e 100755 --- a/scripts/build_schema.sh +++ b/scripts/build_schema.sh @@ -2,6 +2,8 @@ # Build config documentation in markdown # Use it before using mkdocs +echo "::group::{build_schema $1}" + # Parameter is the schema type: config / settings SCHEMA=$1 @@ -32,3 +34,5 @@ docker run --rm --user user \ mv build/ref-$SCHEMA/* gh_pages/users/ref-$SCHEMA/ # also source cp data/searchalicious-$SCHEMA-schema.yml gh_pages/users/ref-$SCHEMA/ + +echo "::endgroup::" \ No newline at end of file diff --git a/scripts/build_sphinx.sh b/scripts/build_sphinx.sh index f87cae26..676760c5 100755 --- a/scripts/build_sphinx.sh +++ b/scripts/build_sphinx.sh @@ -1,6 +1,8 @@ #!/usr/bin/env bash # Build sphinx documentation +echo '::group::{build_sphinx}' + set -e # get group id to use it in the docker @@ -26,3 +28,5 @@ docker run --rm --user user \ rm -rf gh_pages/devs/ref-python || true mv gh_pages/sphinx/html gh_pages/devs/ref-python rm -rf gh_pages/sphinx/ + +echo "::endgroup::" \ No newline at end of file diff --git a/scripts/generate_doc.sh b/scripts/generate_doc.sh index c3a26231..9197f3e8 100755 --- a/scripts/generate_doc.sh +++ b/scripts/generate_doc.sh @@ -16,20 +16,26 @@ scripts/build_schema.sh config scripts/build_schema.sh settings echo "Generate OpenAPI documentation" +echo '::group::{generate_openapi}' make generate-openapi +echo "::endgroup::" echo "Generate openapi html with redocly" +echo '::group::{generate_openapi_html}' docker run --rm \ -v $(pwd)/data:/data -v $(pwd)/gh_pages/:/output \ ghcr.io/redocly/redoc/cli:latest \ build -o /output/users/ref-openapi/index.html searchalicious-openapi.yml sudo chown $UID -R gh_pages/users/ref-openapi +echo "::endgroup::" echo "Generate webcomponents documentation" +echo '::group::{generate_custom_elements}' make generate-custom-elements mkdir -p gh_pages/users/ref-web-components/dist cp frontend/public/dist/custom-elements.json gh_pages/users/ref-web-components/dist/custom-elements.json sudo chown $UID -R gh_pages/users/ref-web-components +echo "::endgroup::" echo "Generate python code documentation using sphinx" scripts/build_sphinx.sh From 5c020a69772d4fdd7f291b967cd53bd6d2c2fde1 Mon Sep 17 00:00:00 2001 From: Alex Garel Date: Fri, 8 Nov 2024 14:57:20 +0100 Subject: [PATCH 22/23] chore: fix doc by avoiding latest sphinx --- scripts/Dockerfile.sphinx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/scripts/Dockerfile.sphinx b/scripts/Dockerfile.sphinx index 1aac9afe..0d5cd825 100644 --- a/scripts/Dockerfile.sphinx +++ b/scripts/Dockerfile.sphinx @@ -1,5 +1,5 @@ # in your Dockerfile -FROM sphinxdoc/sphinx:latest +FROM sphinxdoc/sphinx:7.4.7 ARG USER_UID=1000 ARG USER_GID=1000 @@ -11,14 +11,14 @@ RUN addgroup --gid $USER_GID user && adduser --uid $USER_UID --ingroup user --no RUN mkdir -p /docs/build && mkdir -p /docs/source && chown user:user /docs # install poetry, and export dependencies as a requirement.txt COPY poetry.lock pyproject.toml ./ -RUN apt update && apt install -y curl +RUN apt update && apt install -y curl cargo RUN ( curl -sSL https://install.python-poetry.org | python3 - ) && \ /root/.local/bin/poetry self add poetry-plugin-export && \ /root/.local/bin/poetry export --output requirements.txt # install those dependencies -RUN pip install -r requirements.txt +#RUN pip install -U pip && pip install -r requirements.txt # install some useful plugin for sphinx -RUN pip install autodoc_pydantic sphinxcontrib-typer -USER user +#RUN pip install autodoc_pydantic sphinxcontrib-typer +#USER user From 9beecd137e1cf0fe4d08bc3e80b8ecaa0e62a325 Mon Sep 17 00:00:00 2001 From: Alex Garel Date: Fri, 8 Nov 2024 19:27:20 +0100 Subject: [PATCH 23/23] build: revert non intentional changes to Dockerfile.sphinx --- scripts/Dockerfile.sphinx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/Dockerfile.sphinx b/scripts/Dockerfile.sphinx index 0d5cd825..b6383ed4 100644 --- a/scripts/Dockerfile.sphinx +++ b/scripts/Dockerfile.sphinx @@ -16,9 +16,9 @@ RUN ( curl -sSL https://install.python-poetry.org | python3 - ) && \ /root/.local/bin/poetry self add poetry-plugin-export && \ /root/.local/bin/poetry export --output requirements.txt # install those dependencies -#RUN pip install -U pip && pip install -r requirements.txt +RUN pip install -U pip && pip install -r requirements.txt # install some useful plugin for sphinx -#RUN pip install autodoc_pydantic sphinxcontrib-typer -#USER user +RUN pip install autodoc_pydantic sphinxcontrib-typer +USER user