Skip to content

Commit

Permalink
feature/mx-1702 resolve types in edit view (#158)
Browse files Browse the repository at this point in the history
# PR Context
- this PR only resolves Links, Texts and Enums, but does not resolve
Identifiers yet
- additionally cleans up the user interface and code structure a bit
- needs robert-koch-institut/mex-backend#187 and
robert-koch-institut/mex-backend#189

# Changes
- upgrade mex-common and model dependencies to v3
- overhaul and simplify margins and spaces
- move transform_models_to_fields from State to transform module
- use dedicated backend connector methods for edit and search
- use same rendering components for edit and search pages
- pop toasts when backend connector encounters errors
- scroll to top when pagination is triggered

# Fixed
- fix routing issues by moving the refresh handlers section.from
on_mount to page.on_load

---------

Signed-off-by: Nicolas Drebenstedt <[email protected]>
Co-authored-by: Franziska Diehr <[email protected]>
Co-authored-by: RKI | Metadata Exchange <[email protected]>
Co-authored-by: rababerladuseladim <[email protected]>
  • Loading branch information
4 people authored Nov 14, 2024
1 parent 7ea1271 commit b025472
Show file tree
Hide file tree
Showing 30 changed files with 1,202 additions and 475 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/testing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ jobs:
ports:
- 7687:7687
backend:
image: ghcr.io/robert-koch-institut/mex-backend:0.20.0
image: ghcr.io/robert-koch-institut/mex-backend:0.22.0
env:
MEX_BACKEND_API_USER_DATABASE: ${{ secrets.MEX_BACKEND_API_USER_DATABASE }}
MEX_BACKEND_API_KEY_DATABASE: ${{ secrets.MEX_BACKEND_API_KEY_DATABASE }}
Expand Down Expand Up @@ -91,6 +91,6 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: playwright-screenshots-${{ github.run_id }}
path: tests_*.jpeg
path: test*.png
if-no-files-found: error
retention-days: 10
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added

### Changes

- upgrade mex-common and model dependencies to v3
- overhaul and simplify margins and spaces
- move transform_models_to_fields from State to transform module
- use dedicated backend connector methods for edit and search
- use same rendering components for edit and search pages
- pop toasts when backend connector encounters errors
- scroll to top when pagination is triggered
- update dependency versions

### Deprecated
Expand All @@ -18,6 +26,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- fix routing issues by moving the refresh handlers section from `on_mount` to `page.on_load`

### Security

## [0.5.0] - 2024-09-19
Expand Down
67 changes: 67 additions & 0 deletions mex/editor/components.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import reflex as rx

from mex.editor.edit.models import FixedValue


def fixed_internal_link(value: FixedValue) -> rx.Component:
"""Render a fixed value as a clickable internal link that reloads the edit page."""
return rx.link(
value.text,
href=value.href,
high_contrast=True,
role="link",
)


def fixed_external_link(value: FixedValue) -> rx.Component:
"""Render a fixed value as a clickable external link that opens in a new window."""
return rx.link(
value.text,
href=value.href,
high_contrast=True,
is_external=True,
role="link",
)


def fixed_link(value: FixedValue) -> rx.Component:
"""Render a fixed value as a clickable link that reloads the edit page."""
return rx.cond(
value.external,
fixed_external_link(value),
fixed_internal_link(value),
)


def fixed_text(value: FixedValue) -> rx.Component:
"""Render a fixed value as a text span."""
return rx.text(
value.text,
as_="span",
)


def postfix_badge(value: FixedValue) -> rx.Component:
"""Render a generic badge after the fixed value."""
return rx.badge(
value.badge,
radius="full",
variant="surface",
style={"margin": "auto 0"},
)


def fixed_value(value: FixedValue) -> rx.Component:
"""Return a single fixed value."""
return rx.hstack(
rx.cond(
value.href,
fixed_link(value),
fixed_text(value),
),
rx.cond(
value.badge,
postfix_badge(value),
),
spacing="1",
)
62 changes: 46 additions & 16 deletions mex/editor/edit/main.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,43 @@
import reflex as rx

from mex.editor.edit.state import EditableField, EditablePrimarySource, EditState
from mex.editor.components import fixed_value
from mex.editor.edit.models import EditableField, EditablePrimarySource, FixedValue
from mex.editor.edit.state import EditState
from mex.editor.layout import page


def editable_value(value: str) -> rx.Component:
"""Return a single card for editing one value."""
def fixed_value_card(
field_name: str, primary_source: str | None, index: int, value: FixedValue
) -> rx.Component:
"""Return a card containing a single fixed value."""
return rx.card(
value,
fixed_value(value),
style={"width": "30vw"},
custom_attrs={"data-testid": f"value-{field_name}_{primary_source}_{index}"},
)


def editable_primary_source(model: EditablePrimarySource) -> rx.Component:
def editable_primary_source(
field_name: str, model: EditablePrimarySource
) -> rx.Component:
"""Return a horizontal grid of cards for editing one primary source."""
return rx.hstack(
rx.card(
model.name,
style={"width": "15vw"},
fixed_value(model.name),
style={"width": "20vw"},
custom_attrs={
"data-testid": f"primary-source-{field_name}_{model.name.text}"
},
),
rx.vstack(
rx.foreach(
model.editable_values,
editable_value,
model.editor_values,
lambda value, index: fixed_value_card(
field_name,
model.name.text,
index,
value,
),
)
),
)
Expand All @@ -32,33 +47,48 @@ def editable_field(model: EditableField) -> rx.Component:
"""Return a horizontal grid of cards for editing one field."""
return rx.hstack(
rx.card(
model.name,
rx.text(model.name),
style={"width": "15vw"},
custom_attrs={"data-testid": f"field-{model.name}"},
),
rx.foreach(
model.primary_sources,
editable_primary_source,
lambda primary_source: editable_primary_source(
model.name,
primary_source,
),
),
role="row",
)


def index() -> rx.Component:
"""Return the index for the edit component."""
return page(
rx.section(
rx.box(
rx.heading(
EditState.item_title,
rx.hstack(
rx.foreach(
EditState.item_title,
fixed_value,
)
),
custom_attrs={"data-testid": "edit-heading"},
style={"margin": "1em 0"},
style={
"margin": "1em 0",
"whiteSpace": "nowrap",
"overflow": "hidden",
"textOverflow": "ellipsis",
"maxWidth": "80%",
},
),
rx.vstack(
rx.foreach(
EditState.fields,
editable_field,
),
),
on_mount=EditState.refresh,
style={"width": "100%"},
style={"width": "100%", "margin": "0 2em 1em"},
custom_attrs={"data-testid": "edit-section"},
),
)
6 changes: 4 additions & 2 deletions mex/editor/edit/models.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import reflex as rx

from mex.editor.models import FixedValue


class EditablePrimarySource(rx.Base):
"""Model for describing the editor state for one primary source."""

name: str
editable_values: list[str]
name: FixedValue
editor_values: list[FixedValue]


class EditableField(rx.Base):
Expand Down
81 changes: 36 additions & 45 deletions mex/editor/edit/state.py
Original file line number Diff line number Diff line change
@@ -1,57 +1,48 @@
from pydantic import BaseModel
import reflex as rx
from reflex.event import EventSpec
from requests import HTTPError

from mex.common.backend_api.connector import BackendApiConnector
from mex.common.models import AnyExtractedModel
from mex.editor.edit.models import EditableField, EditablePrimarySource
from mex.common.logging import logger
from mex.editor.edit.models import EditableField
from mex.editor.edit.transform import transform_models_to_fields
from mex.editor.models import FixedValue
from mex.editor.state import State
from mex.editor.transform import render_any_value, render_model_title


class _BackendSearchResponse(BaseModel):
total: int
items: list[AnyExtractedModel]
from mex.editor.transform import transform_models_to_title


class EditState(State):
"""State for the edit component."""

fields: list[EditableField] = []
item_title: str = ""
item_title: list[FixedValue] = []

def refresh(self) -> None:
"""Refresh the search results."""
def refresh(self) -> EventSpec | None:
"""Refresh the edit page."""
self.reset()
# TODO(ND): use the user auth for backend requests (stop-gap MX-1616)
connector = BackendApiConnector.get()

# TODO(ND): use a specialized extracted-item search method
response = connector.request(
"GET", f"extracted-item?stableTargetId={self.item_id}"
)
items = _BackendSearchResponse.model_validate(response).items
self.fields = self.extracted_to_fields(items)

# TODO(ND): use title of merged item instead of title of first item
self.item_title = render_model_title(items[0])

@staticmethod
def extracted_to_fields(models: list[AnyExtractedModel]) -> list[EditableField]:
"""Convert a list of extracted models into editable field models."""
fields_by_name: dict[str, EditableField] = {}
for model in models:
model_dump = model.model_dump()
for field_name in model.model_fields:
editable_field = fields_by_name.setdefault(
field_name, EditableField(name=field_name, primary_sources=[])
)
value = model_dump[field_name]
if not value:
continue
if not isinstance(value, list):
value = [value]
editable_field.primary_sources.append(
EditablePrimarySource(
name=model.hadPrimarySource,
editable_values=[render_any_value(v) for v in value],
)
)
return list(fields_by_name.values())
try:
response = connector.fetch_extracted_items(
None,
self.item_id,
None,
0,
100,
)
except HTTPError as exc:
self.reset()
logger.error(
"backend error fetching extracted items: %s",
exc.response.text,
exc_info=False,
)
return rx.toast.error(
exc.response.text,
duration=5000,
close_button=True,
dismissible=True,
)
self.item_title = transform_models_to_title(response.items)
self.fields = transform_models_to_fields(response.items)
return None
39 changes: 39 additions & 0 deletions mex/editor/edit/transform.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
from collections.abc import Iterable

from mex.common.exceptions import MExError
from mex.common.models import (
MEX_PRIMARY_SOURCE_STABLE_TARGET_ID,
AnyExtractedModel,
AnyRuleModel,
)
from mex.common.types import Identifier
from mex.editor.edit.models import EditableField, EditablePrimarySource
from mex.editor.transform import transform_value, transform_values


def transform_models_to_fields(
models: Iterable[AnyExtractedModel | AnyRuleModel],
) -> list[EditableField]:
"""Convert a list of extracted models into editable field models."""
fields_by_name: dict[str, EditableField] = {}
for model in models:
if isinstance(model, AnyExtractedModel):
primary_source_name = transform_value(Identifier(model.hadPrimarySource))
elif isinstance(model, AnyRuleModel):
primary_source_name = transform_value(MEX_PRIMARY_SOURCE_STABLE_TARGET_ID)
else:
msg = (
"cannot transform model, expected extracted ExtractedData or "
f"RuleItem, got {type(model).__name__}"
)
raise MExError(msg)
for field_name in model.model_fields:
editable_field = EditableField(name=field_name, primary_sources=[])
fields_by_name.setdefault(field_name, editable_field)
if values := transform_values(getattr(model, field_name)):
editable_field.primary_sources.append(
EditablePrimarySource(
name=primary_source_name, editor_values=values
)
)
return list(fields_by_name.values())
5 changes: 1 addition & 4 deletions mex/editor/layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@ def nav_bar() -> rx.Component:
user_menu(),
rx.spacer(),
rx.color_mode.button(),
id="navbar",
padding="1em",
position="fixed",
style={"background": "var(--accent-4)"},
Expand All @@ -91,11 +90,9 @@ def page(*children: str | rx.Component) -> rx.Component:
rx.hstack(
*children,
min_height="85vh",
margin="2em 1em 1em",
margin="4em 0 0",
spacing="5",
),
on_mount=State.load_page,
on_unmount=State.unload_page,
),
rx.center(
rx.spinner(size="3"),
Expand Down
Loading

0 comments on commit b025472

Please sign in to comment.