Skip to content

Commit

Permalink
feat: improve search results display (#484)
Browse files Browse the repository at this point in the history
* feat(frontend): make language selection for translations more intuitive (#461)

The PR modifies the translations section of the edit entry page:

* Changes "All languages" terminology to "Fallback translations" (fixes #458)
* Adds an info alert if "en" (English) or "xx" (Fallback translations) is not the main language for an entry (fixes #457)
/ Fixes the fact that the alert message about changing the display name of a language appears even if the language had no translations and we add the first one (fixes #459)
* Adds a "Show all existing translations" checkbox to see all the languages that currently have translations, with their translations Adds a possibility to "pin" languages to select them (so they stay in local storage and appear at the top for each entry), and a possibility to hide (unselect) these languages easily (with an icon next to their title)
* Modifies the selection of new languages: I removed the "number of languages shown" button that had to be clicked to add a language, and created a "Show another language" button at the bottom of the section. Also, the dialog is now an autocomplete instead of a select, and you just type the languages that you want to add and see languages that are not selected, instead of seeing all current languages and being able to remove them. The autocomplete with the options is also automatically focused when opening the dialog.
* Adds vite-plugin-svgr to easily import svg files in React

---------

Co-authored-by: Charles Perier <[email protected]>

* feat: add property filter to search API (#456)

* feat: add property filter to search API

* chore: generate SDK

* chore: Add info banners on the frontend (#473)

* docs: add info banners

* refactor: delete unnecessary components

* fix: add line break

* improve search results, and few other improvements

* show translations instead of translated languages

---------

Co-authored-by: Charles Perier <[email protected]>
Co-authored-by: Eric Nguyen <[email protected]>
  • Loading branch information
3 people authored Apr 11, 2024
1 parent 1a34e1f commit 764b21e
Show file tree
Hide file tree
Showing 28 changed files with 1,355 additions and 724 deletions.
1 change: 0 additions & 1 deletion backend/editor/models/node_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ class EntryNodeCreate(BaseModel):
class EntryNode(BaseModel):
id: str
preceding_lines: list[str]
src_position: int
main_language: str
tags: dict[str, list[str]]
properties: dict[str, str]
Expand Down
128 changes: 127 additions & 1 deletion backend/editor/models/search_models.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import re
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from typing import Annotated, Literal

from pydantic import Field, StringConstraints, TypeAdapter, computed_field
from pydantic import Field, StringConstraints, TypeAdapter, computed_field, field_validator

from .base_models import BaseModel
from .node_models import EntryNode
Expand Down Expand Up @@ -115,6 +116,130 @@ def build_cypher_query(self, param_name: str) -> CypherQuery:
)


class PropertyFilterSearchTerm(AbstractFilterSearchTerm):
filter_type: Literal["property"]
filter_value: str

@field_validator("filter_value")
@classmethod
def validate_filter_value(cls, filter_value: str):
"""
The filter value is in the format `not:inherited:property_name:property_value`
where `not:` and `inherited:` and `:property_value` are optional.
Note that a property_name is always of format `name:lc`.
"""
parsed_value = filter_value
if parsed_value.startswith("not:"):
parsed_value = parsed_value[4:]
if parsed_value.startswith("inherited:"):
parsed_value = parsed_value[10:]

assert ":" in parsed_value, "A property_name is mandatory and must contain a colon"

terms = parsed_value.split(":")
property_name = terms[0] + ":" + terms[1]

if not re.match(r"^[^:\\]+:[a-z]{2}$", property_name):
raise ValueError("Invalid property_name")

return filter_value

@computed_field
def negated(self) -> bool:
return self.filter_value.startswith("not:")

@computed_field
def inherited(self) -> bool:
filter_value = self.get_parsed_filter_value(self.negated)
return filter_value.startswith("inherited:")

@computed_field
def property_name(self) -> str:
filter_value = self.get_parsed_filter_value(self.negated, self.inherited)
terms = filter_value.split(":")
return terms[0] + "_" + terms[1]

@computed_field
def property_value(self) -> str | None:
filter_value = self.get_parsed_filter_value(self.negated, self.inherited)
terms = filter_value.split(":")
return ":".join(terms[2:]) if len(terms) > 2 else None

def get_parsed_filter_value(self, negated=False, inherited=False):
filter_value = self.filter_value
if negated:
filter_value = filter_value[4:]
if inherited:
filter_value = filter_value[10:]
return filter_value

def build_cypher_query(self, param_name: str) -> CypherQuery:
branches = {
"negated": self.negated,
"inherited": self.inherited,
"with_value": self.property_value is not None,
}
match branches:
case {"negated": False, "inherited": False, "with_value": False}:
return CypherQuery(f"n.prop_{self.property_name} IS NOT NULL")
case {"negated": True, "inherited": False, "with_value": False}:
return CypherQuery(f"n.prop_{self.property_name} IS NULL")
case {"negated": False, "inherited": False, "with_value": True}:
return CypherQuery(
f"n.prop_{self.property_name} = ${param_name}",
{param_name: self.property_value},
)
case {"negated": True, "inherited": False, "with_value": True}:
return CypherQuery(
f"n.prop_{self.property_name} <> ${param_name}",
{param_name: self.property_value},
)
case {"negated": False, "inherited": True, "with_value": False}:
return CypherQuery(
f"""(n.prop_{self.property_name} IS NOT NULL OR
any(
ancestor IN [(n)<-[:is_child_of*]-(p:ENTRY) | p]
WHERE ancestor.prop_{self.property_name} IS NOT NULL)
)""",
)
case {"negated": True, "inherited": True, "with_value": False}:
return CypherQuery(
f"""(n.prop_{self.property_name} IS NULL AND
all(
ancestor IN [(n)<-[:is_child_of*]-(p:ENTRY) | p]
WHERE ancestor.prop_{self.property_name} IS NULL)
)""",
)
case {"negated": False, "inherited": True, "with_value": True}:
return CypherQuery(
f"""
[
property IN
[n.prop_{self.property_name}] +
[(n)<-[:is_child_of*]-(p:ENTRY) | p.prop_{self.property_name}]
WHERE property IS NOT NULL
][0]
= ${param_name}""",
{param_name: self.property_value},
)
case {"negated": True, "inherited": True, "with_value": True}:
return CypherQuery(
f"""((n.prop_{self.property_name} IS NULL AND
all(
ancestor IN [(n)<-[:is_child_of*]-(p:ENTRY) | p]
WHERE ancestor.prop_{self.property_name} IS NULL)
) OR
[
property IN
[n.prop_{self.property_name}] +
[(n)<-[:is_child_of*]-(p:ENTRY) | p.prop_{self.property_name}]
WHERE property IS NOT NULL
][0]
<> ${param_name})""",
{param_name: self.property_value},
)


FilterSearchTerm = Annotated[
(
IsFilterSearchTerm
Expand All @@ -123,6 +248,7 @@ def build_cypher_query(self, param_name: str) -> CypherQuery:
| ChildFilterSearchTerm
| AncestorFilterSearchTerm
| DescendantFilterSearchTerm
| PropertyFilterSearchTerm
),
Field(discriminator="filter_type"),
]
Expand Down
42 changes: 40 additions & 2 deletions backend/openapi/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -1261,7 +1261,8 @@
{ "$ref": "#/components/schemas/ParentFilterSearchTerm" },
{ "$ref": "#/components/schemas/ChildFilterSearchTerm" },
{ "$ref": "#/components/schemas/AncestorFilterSearchTerm" },
{ "$ref": "#/components/schemas/DescendantFilterSearchTerm" }
{ "$ref": "#/components/schemas/DescendantFilterSearchTerm" },
{ "$ref": "#/components/schemas/PropertyFilterSearchTerm" }
],
"discriminator": {
"propertyName": "filterType",
Expand All @@ -1271,7 +1272,8 @@
"descendant": "#/components/schemas/DescendantFilterSearchTerm",
"is": "#/components/schemas/IsFilterSearchTerm",
"language": "#/components/schemas/LanguageFilterSearchTerm",
"parent": "#/components/schemas/ParentFilterSearchTerm"
"parent": "#/components/schemas/ParentFilterSearchTerm",
"property": "#/components/schemas/PropertyFilterSearchTerm"
}
}
},
Expand Down Expand Up @@ -1434,6 +1436,42 @@
"enum": ["OPEN", "EXPORTED", "LOADING", "FAILED"],
"title": "ProjectStatus"
},
"PropertyFilterSearchTerm": {
"properties": {
"filterType": { "const": "property", "title": "Filtertype" },
"filterValue": { "type": "string", "title": "Filtervalue" },
"negated": {
"type": "boolean",
"title": "Negated",
"readOnly": true
},
"inherited": {
"type": "boolean",
"title": "Inherited",
"readOnly": true
},
"propertyName": {
"type": "string",
"title": "Propertyname",
"readOnly": true
},
"propertyValue": {
"anyOf": [{ "type": "string" }, { "type": "null" }],
"title": "Propertyvalue",
"readOnly": true
}
},
"type": "object",
"required": [
"filterType",
"filterValue",
"negated",
"inherited",
"propertyName",
"propertyValue"
],
"title": "PropertyFilterSearchTerm"
},
"ValidationError": {
"properties": {
"loc": {
Expand Down
Loading

0 comments on commit 764b21e

Please sign in to comment.