diff --git a/CHANGELOG.md b/CHANGELOG.md index 6038a89..1890d85 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - update mex-common to version 0.49.3 - BREAKING: you must start the local dev mode simply with `pdm run editor` (no 2nd run) - BREAKING: rename postfix_badge to render_badge (for consistency) -- simplify some styles +- simplify some styles and update look and feel to be more inline with mex-drop +- use more idiomatic variables for styling elements with colors or spacing ### Deprecated @@ -23,6 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - remove BackendIdentityProvider in favor of mex-common version - remove identity_provider from EditorSettings in favor of mex-common setting - remove EditorIdentityProvider enum in favor of mex-common enum +- remove dependency to reflex-chakra, use default checkboxes instead ### Fixed diff --git a/assets/chakra_color_mode_provider.js b/assets/chakra_color_mode_provider.js deleted file mode 100644 index ad41d51..0000000 --- a/assets/chakra_color_mode_provider.js +++ /dev/null @@ -1,36 +0,0 @@ -import { useColorMode as chakraUseColorMode } from "@chakra-ui/react"; -import { useTheme } from "next-themes"; -import { useEffect, useState } from "react"; -import { ColorModeContext, defaultColorMode } from "/utils/context.js"; - -export default function ChakraColorModeProvider({ children }) { - const { theme, resolvedTheme, setTheme } = useTheme(); - const { colorMode, toggleColorMode } = chakraUseColorMode(); - const [resolvedColorMode, setResolvedColorMode] = useState(colorMode); - - useEffect(() => { - if (colorMode != resolvedTheme) { - toggleColorMode(); - } - setResolvedColorMode(resolvedTheme); - }, [theme, resolvedTheme]); - - const rawColorMode = colorMode; - const setColorMode = (mode) => { - const allowedModes = ["light", "dark", "system"]; - if (!allowedModes.includes(mode)) { - console.error( - `Invalid color mode "${mode}". Defaulting to "${defaultColorMode}".` - ); - mode = defaultColorMode; - } - setTheme(mode); - }; - return ( - - {children} - - ); -} diff --git a/mex/editor/components.py b/mex/editor/components.py index a1552bd..82f1ab7 100644 --- a/mex/editor/components.py +++ b/mex/editor/components.py @@ -45,8 +45,8 @@ def render_badge(value: EditorValue) -> rx.Component: """Render a generic badge for an editor value.""" return rx.badge( value.badge, - radius="full", - variant="surface", + radius="large", + variant="soft", style={"margin": "auto 0"}, ) diff --git a/mex/editor/edit/main.py b/mex/editor/edit/main.py index 3f1613b..458f597 100644 --- a/mex/editor/edit/main.py +++ b/mex/editor/edit/main.py @@ -19,6 +19,7 @@ def editor_value_switch( checked=value.enabled, on_change=cast(EditState, EditState).toggle_field_value(field_name, value), custom_attrs={"data-testid": f"switch-{field_name}-{primary_source}-{index}"}, + color_scheme="jade", ) @@ -53,6 +54,7 @@ def primary_source_switch( field_name, model.name.href ), custom_attrs={"data-testid": f"switch-{field_name}-{model.identifier}"}, + color_scheme="jade", ) @@ -130,7 +132,7 @@ def submit_button() -> rx.Component: color_scheme="jade", size="3", on_click=EditState.submit_rule_set, - style={"margin": "1em 0"}, + style={"margin": "var(--line-height-1) 0"}, custom_attrs={"data-testid": "submit-button"}, ) @@ -146,11 +148,8 @@ def edit_heading() -> rx.Component: ), custom_attrs={"data-testid": "edit-heading"}, style={ - "margin": "1em 0", "whiteSpace": "nowrap", "overflow": "hidden", - "textOverflow": "ellipsis", - "maxWidth": "80vw", }, ) @@ -158,22 +157,16 @@ def edit_heading() -> rx.Component: def index() -> rx.Component: """Return the index for the edit component.""" return page( - rx.box( + rx.vstack( edit_heading(), - rx.vstack( - rx.foreach( - EditState.fields, - editor_field, - ), - rx.cond( - EditState.fields, - submit_button(), - ), + rx.foreach( + EditState.fields, + editor_field, + ), + rx.cond( + EditState.fields, + submit_button(), ), - style={ - "width": "100%", - "margin": "0 2em 1em", - }, - custom_attrs={"data-testid": "edit-section"}, + style={"width": "100%"}, ), ) diff --git a/mex/editor/edit/state.py b/mex/editor/edit/state.py index aa18e94..4cfde55 100644 --- a/mex/editor/edit/state.py +++ b/mex/editor/edit/state.py @@ -40,7 +40,7 @@ def refresh(self) -> Generator[EventSpec | None, None, None]: try: extracted_items_response = connector.fetch_extracted_items( None, - self.item_id, + self.router.page.params["identifier"], None, 0, 100, @@ -60,15 +60,15 @@ def refresh(self) -> Generator[EventSpec | None, None, None]: else [] ) try: - rule_set = connector.get_rule_set( - self.item_id, - ) + rule_set = connector.get_rule_set(self.router.page.params["identifier"]) except HTTPError as exc: if exc.response.status_code == status.HTTP_404_NOT_FOUND and self.stem_type: rule_set_class = RULE_SET_RESPONSE_CLASSES_BY_NAME[ ensure_postfix(self.stem_type, "RuleSetResponse") ] - rule_set = rule_set_class(stableTargetId=self.item_id) + rule_set = rule_set_class( + stableTargetId=self.router.page.params["identifier"] + ) else: self.reset() yield from escalate_error( @@ -95,7 +95,7 @@ def submit_rule_set(self) -> Generator[EventSpec | None, None, None]: # TODO(ND): use proper connector method when available (stop-gap MX-1762) connector.request( method="PUT", - endpoint=f"rule-set/{self.item_id}", + endpoint=f"rule-set/{self.router.page.params['identifier']}", payload=rule_set, ) except HTTPError as exc: diff --git a/mex/editor/layout.py b/mex/editor/layout.py index 50d87e7..7c4b32f 100644 --- a/mex/editor/layout.py +++ b/mex/editor/layout.py @@ -22,38 +22,37 @@ def user_button() -> rx.Component: def user_menu() -> rx.Component: """Return a user menu with a trigger, the user's name and a logout button.""" return rx.menu.root( - rx.menu.trigger(user_button()), + rx.menu.trigger( + user_button(), + custom_attrs={"data-testid": "user-menu"}, + ), rx.menu.content( - rx.menu.item( - cast(User, State.user).name, - disabled=True, - style={"color": "var(--gray-12)"}, - ), + rx.menu.item(cast(User, State.user).name, disabled=True), rx.menu.separator(), rx.menu.item( "Logout", on_select=State.logout, + custom_attrs={"data-testid": "logout-button"}, ), ), - custom_attrs={"data-testid": "user-menu"}, ) def nav_link(item: NavItem) -> rx.Component: """Return a link component for the given navigation item.""" return rx.link( - item.title, - href=item.href, + rx.text(item.title, size="4", weight="medium"), + href=item.raw_path, underline=item.underline, - padding="0.2em", + class_name="nav-item", ) -def mex_editor_logo() -> rx.Component: - """Return the editor's logo with icon and label.""" +def app_logo() -> rx.Component: + """Return the app logo with icon and label.""" return rx.hstack( rx.icon( - "file-pen-line", + "circuit-board", size=28, ), rx.heading( @@ -61,40 +60,71 @@ def mex_editor_logo() -> rx.Component: weight="medium", style={"userSelect": "none"}, ), + custom_attrs={"data-testid": "app-logo"}, ) def nav_bar() -> rx.Component: """Return a navigation bar component.""" - return rx.hstack( - mex_editor_logo(), - rx.divider(orientation="vertical", size="2"), - rx.foreach(State.nav_items, nav_link), - rx.divider(orientation="vertical", size="2"), - user_menu(), - rx.spacer(), - rx.color_mode.button(), - padding="1em", - position="fixed", - style={"background": "var(--accent-4)"}, - top="0px", - width="100%", - z_index="1000", - custom_attrs={"data-testid": "nav-bar"}, + return rx.vstack( + rx.box( + style={ + "height": "var(--space-6)", + "width": "100%", + "backdropFilter": " var(--backdrop-filter-panel)", + "backgroundColor": "var(--card-background-color)", + }, + ), + rx.card( + rx.hstack( + app_logo(), + rx.divider(orientation="vertical", size="2"), + rx.hstack( + rx.foreach(State.nav_items, nav_link), + justify="start", + spacing="4", + ), + rx.divider(orientation="vertical", size="2"), + user_menu(), + rx.spacer(), + rx.color_mode.button(), + justify="between", + align_items="center", + ), + size="2", + custom_attrs={"data-testid": "nav-bar"}, + style={ + "width": "100%", + "marginTop": "calc(-1 * var(--base-card-border-width))", + }, + ), + spacing="0", + style={ + "maxWidth": "calc(1480px * var(--scaling))", + "minWidth": "calc(800px * var(--scaling))", + "position": "fixed", + "top": "0", + "width": "100%", + "zIndex": "1000", + }, ) -def page(*children: str | rx.Component) -> rx.Component: +def page(*children: rx.Component) -> rx.Component: """Return a page fragment with navigation bar and given children.""" return rx.cond( State.user, - rx.fragment( + rx.center( nav_bar(), rx.hstack( *children, - min_height="85vh", - margin="4em 0 0", - spacing="5", + style={ + "maxWidth": "calc(1480px * var(--scaling))", + "minWidth": "calc(800px * var(--scaling))", + "padding": "calc(var(--space-6) * 4) var(--space-6) var(--space-6)", + "width": "100%", + }, + custom_attrs={"data-testid": "page-body"}, ), ), rx.center( diff --git a/mex/editor/login/main.py b/mex/editor/login/main.py index 2efa8f8..ad082a7 100644 --- a/mex/editor/login/main.py +++ b/mex/editor/login/main.py @@ -1,59 +1,75 @@ import reflex as rx -from mex.editor.layout import mex_editor_logo +from mex.editor.layout import app_logo from mex.editor.login.state import LoginState -def login_form() -> rx.Component: - """Return a vertical stack of login form components.""" +def login_user() -> rx.Component: + """Return a form field for the user name.""" return rx.vstack( - rx.vstack( - rx.text("Username"), - rx.input( - autofocus=True, - on_change=LoginState.set_username, - placeholder="Username", - size="3", - tab_index=1, - style={"width": "80%"}, - ), - style={"width": "100%"}, - ), - rx.vstack( - rx.text("Password"), - rx.input( - on_change=LoginState.set_password, - placeholder="Password", - size="3", - tab_index=2, - type="password", - style={"width": "80%"}, - ), - style={"width": "100%"}, + rx.text("Username"), + rx.input( + autofocus=True, + on_change=LoginState.set_username, + placeholder="Username", + size="3", + tab_index=1, + style={"width": "80%"}, ), - rx.button( - "Log in", - on_click=LoginState.login, + style={"width": "100%"}, + ) + + +def login_password() -> rx.Component: + """Return a form field for the password.""" + return rx.vstack( + rx.text("Password"), + rx.input( + on_change=LoginState.set_password, + placeholder="Password", size="3", - tab_index=3, - width="6em", + tab_index=2, + type="password", + style={"width": "80%"}, ), style={"width": "100%"}, ) +def login_button() -> rx.Component: + """Return a submit button for the login form.""" + return rx.button( + "Login", + on_click=LoginState.login, + size="3", + tab_index=3, + style={ + "padding": "0 var(--space-6)", + "marginTop": "var(--space-4)", + }, + custom_attrs={"data-testid": "login-button"}, + ) + + def index() -> rx.Component: """Return the index for the login component.""" return rx.center( rx.card( rx.vstack( - mex_editor_logo(), + app_logo(), rx.divider(size="4"), - login_form(), + rx.vstack( + login_user(), + login_password(), + login_button(), + style={"width": "100%"}, + ), spacing="4", ), - top="20vh", - width="400px", + style={ + "width": "calc(400px * var(--scaling))", + "top": "20vh", + }, variant="classic", custom_attrs={"data-testid": "login-card"}, ), diff --git a/mex/editor/login/state.py b/mex/editor/login/state.py index 0c32afb..d200b3d 100644 --- a/mex/editor/login/state.py +++ b/mex/editor/login/state.py @@ -13,8 +13,9 @@ class LoginState(State): username: str password: str + @rx.event def login(self) -> EventSpec: - """Log in a user.""" + """Login a user.""" read_access = has_read_access(self.username, self.password) write_access = has_write_access(self.username, self.password) if read_access: @@ -25,6 +26,9 @@ def login(self) -> EventSpec: write_access=write_access, ) if self.target_path_after_login: - return rx.redirect(self.target_path_after_login) - return rx.redirect("/") + target_path_after_login = self.target_path_after_login + else: + target_path_after_login = "/" + self.reset() # reset username/password + return rx.redirect(target_path_after_login) return rx.window_alert("Invalid credentials.") diff --git a/mex/editor/models.py b/mex/editor/models.py index 540b5f1..64a5386 100644 --- a/mex/editor/models.py +++ b/mex/editor/models.py @@ -1,4 +1,5 @@ from importlib.resources import files +from typing import Literal import reflex as rx import yaml @@ -28,10 +29,19 @@ class User(rx.Base): class NavItem(rx.Base): """Model for one navigation bar item.""" - title: str - href: str = "/" - href_template: str = "/" - underline: str = "none" + title: str = "" + path: str = "/" + raw_path: str = "/" + underline: Literal["always", "none"] = "none" + + def update_raw_path(self, params: dict[str, int | str | list[str]]) -> None: + """Render the parameters into a raw path.""" + raw_path = self.path + param_tuples = list(params.items()) + for key, value in param_tuples: + if f"[{key}]" in raw_path: + raw_path = raw_path.replace(f"[{key}]", f"{value}") + self.raw_path = raw_path class ModelConfig(BaseModel): diff --git a/mex/editor/search/main.py b/mex/editor/search/main.py index f5a89ff..ba5076b 100644 --- a/mex/editor/search/main.py +++ b/mex/editor/search/main.py @@ -1,7 +1,6 @@ from typing import cast import reflex as rx -import reflex_chakra as rc from mex.editor.components import render_value from mex.editor.layout import page @@ -20,7 +19,11 @@ def search_result(result: SearchResult) -> rx.Component: render_value, ) ), - style={"fontWeight": "bold"}, + style={ + "fontWeight": "var(--font-weight-bold)", + "overflow": "hidden", + "whiteSpace": "nowrap", + }, ), rx.box( rx.hstack( @@ -31,7 +34,7 @@ def search_result(result: SearchResult) -> rx.Component: ), style={ "color": "var(--gray-12)", - "fontWeight": "light", + "fontWeight": "var(--font-weight-light)", "textDecoration": "none", }, ), @@ -43,61 +46,66 @@ def search_result(result: SearchResult) -> rx.Component: def search_input() -> rx.Component: """Render a search input element that will trigger the results to refresh.""" - return rx.debounce_input( - rx.input( - rx.input.slot(rx.icon("search"), padding_left="0"), - placeholder="Search here...", - value=SearchState.query_string, - on_change=SearchState.set_query_string, - max_length=100, - style={ - "--text-field-selection-color": "", - "--text-field-focus-color": "transparent", - "--text-field-border-width": "1px", - "backgroundClip": "content-box", - "backgroundColor": "transparent", - "boxShadow": ("inset 0 0 0 var(--text-field-border-width) transparent"), - "color": "", - }, + return rx.card( + rx.debounce_input( + rx.input( + rx.input.slot( + rx.icon("search"), + autofocus=True, + padding_left="0", + tab_index=1, + ), + placeholder="Search here...", + value=SearchState.query_string, + on_change=SearchState.set_query_string, + max_length=100, + style={ + "--text-field-selection-color": "", + "--text-field-focus-color": "transparent", + "--text-field-border-width": "calc(1px * var(--scaling))", + "boxShadow": ( + "inset 0 0 0 var(--text-field-border-width) transparent" + ), + }, + ), + style={"margin": "var(--space-4) 0 var(--space-4)"}, + debounce_timeout=250, ), - style={"margin": "1em 0 1em"}, - debounce_timeout=250, + style={"width": "100%"}, + ) + + +def entity_type_choice(choice: tuple[str, bool]) -> rx.Component: + """Render a single checkboxes for filtering by entity type.""" + return rx.checkbox( + choice[0][len("Merged") :], + checked=choice[1], + on_change=SearchState.set_entity_type(choice[0]), ) def entity_type_filter() -> rx.Component: """Render checkboxes for filtering the search results by entity type.""" - return rx.vstack( - rx.foreach( - SearchState.entity_types, - lambda choice: rx.debounce_input( - rc.checkbox( - choice[0], - checked=choice[1], - on_change=lambda val: cast( - SearchState, SearchState - ).set_entity_type( - val, - choice[0], - ), - ), - debounce_timeout=100, + return rx.card( + rx.vstack( + rx.foreach( + SearchState.entity_types, + entity_type_choice, ), + custom_attrs={"data-testid": "entity-types"}, ), - custom_attrs={"data-testid": "entity-types"}, - style={"margin": "2em 0"}, + style={"width": "100%"}, ) def sidebar() -> rx.Component: """Render sidebar with a search input and checkboxes for filtering entity types.""" - return rx.box( + return rx.vstack( search_input(), entity_type_filter(), - width="20vw", - padding="2em 2em 10em", - bg="linear-gradient(to bottom, var(--gray-2) 0%, var(--color-background) 100%)", + spacing="4", custom_attrs={"data-testid": "search-sidebar"}, + style={"width": "25%"}, ) @@ -108,11 +116,9 @@ def pagination() -> rx.Component: rx.text("Previous"), on_click=SearchState.go_to_previous_page, disabled=SearchState.disable_previous_page, - spacing="2", - width="120px", - margin_right="10px", variant="surface", custom_attrs={"data-testid": "pagination-previous-button"}, + style={"minWidth": "10%"}, ), rx.select( SearchState.total_pages, @@ -124,43 +130,55 @@ def pagination() -> rx.Component: rx.text("Next"), on_click=SearchState.go_to_next_page, disabled=SearchState.disable_next_page, - spacing="2", - width="120px", - margin_left="10px", variant="surface", custom_attrs={"data-testid": "pagination-next-button"}, + style={"minWidth": "10%"}, + ), + spacing="4", + style={"width": "100%"}, + ) + + +def results_summary() -> rx.Component: + """Render a summary of the results found.""" + return rx.center( + rx.text( + f"Showing {SearchState.current_results_length} " + f"of {SearchState.total} items", + style={ + "color": "var(--gray-12)", + "fontWeight": "var(--font-weight-bold)", + "margin": "var(--space-4)", + "userSelect": "none", + }, + custom_attrs={"data-testid": "search-results-summary"}, ), - style={"margin": "1em 0"}, - width="100%", + style={"width": "100%"}, ) def search_results() -> rx.Component: - """Render the search results with a heading, result list, and pagination.""" + """Render the search results with a summary, result list, and pagination.""" return rx.vstack( - rx.center( - rx.heading( - f"showing {SearchState.current_results_length} " - f"of total {SearchState.total} items found", - custom_attrs={"data-testid": "search-results-heading"}, - size="3", - ), - style={"margin": "1em 0"}, - width="100%", - ), + results_summary(), rx.foreach( SearchState.results, search_result, ), pagination(), + spacing="4", custom_attrs={"data-testid": "search-results-section"}, - width="70vw", + style={"width": "100%"}, ) def index() -> rx.Component: """Return the index for the search component.""" return page( - sidebar(), - search_results(), + rx.hstack( + sidebar(), + search_results(), + spacing="4", + style={"width": "100%"}, + ) ) diff --git a/mex/editor/search/state.py b/mex/editor/search/state.py index 2d04f77..b944885 100644 --- a/mex/editor/search/state.py +++ b/mex/editor/search/state.py @@ -48,13 +48,17 @@ def total_pages(self) -> list[str]: def set_query_string(self, value: str) -> Generator[EventSpec | None, None, None]: """Set the query string and refresh the results.""" self.query_string = value - return self.search() + self.current_page = 1 + yield from self.search() @rx.event - def set_entity_type(self, value, index) -> Generator[EventSpec | None, None, None]: + def set_entity_type( + self, index: str, value: bool + ) -> Generator[EventSpec | None, None, None]: """Set the entity type for filtering and refresh the results.""" self.entity_types[index] = value - return self.search() + self.current_page = 1 + yield from self.search() @rx.event def set_page( @@ -62,17 +66,17 @@ def set_page( ) -> Generator[EventSpec | None, None, None]: """Set the current page and refresh the results.""" self.current_page = int(page_number) - return self.search() + yield from self.search() @rx.event def go_to_previous_page(self) -> Generator[EventSpec | None, None, None]: """Navigate to the previous page.""" - return self.set_page(self.current_page - 1) + yield from self.set_page(self.current_page - 1) @rx.event def go_to_next_page(self) -> Generator[EventSpec | None, None, None]: """Navigate to the next page.""" - return self.set_page(self.current_page + 1) + yield from self.set_page(self.current_page + 1) @rx.event def search(self) -> Generator[EventSpec | None, None, None]: @@ -87,18 +91,18 @@ def search(self) -> Generator[EventSpec | None, None, None]: limit=self.limit, ) except HTTPError as exc: - self.reset() + self.results = [] + self.total = 0 + self.current_page = 1 yield from escalate_error( "backend", "error fetching merged items", exc.response.text ) - return - - yield rx.call_script("window.scrollTo({top: 0, behavior: 'smooth'});") - self.results = transform_models_to_results(response.items) - self.total = response.total + else: + self.results = transform_models_to_results(response.items) + self.total = response.total + yield rx.call_script("window.scrollTo({top: 0, behavior: 'smooth'});") @rx.event def refresh(self) -> Generator[EventSpec | None, None, None]: """Refresh the search page.""" - self.reset() - return self.search() + yield from self.search() diff --git a/mex/editor/state.py b/mex/editor/state.py index 10a801c..9324934 100644 --- a/mex/editor/state.py +++ b/mex/editor/state.py @@ -11,9 +11,21 @@ class State(rx.State): user: User | None = None target_path_after_login: str | None = None nav_items: list[NavItem] = [ - NavItem(title="Search", href_template=r"/"), - NavItem(title="Edit", href_template=r"/item/{item_id}/"), - NavItem(title="Merge", href_template=r"/merge/"), + NavItem( + title="Search", + path="/", + raw_path="/", + ), + NavItem( + title="Edit", + path="/item/[identifier]", + raw_path=f"/item/{MEX_PRIMARY_SOURCE_STABLE_TARGET_ID}/", + ), + NavItem( + title="Merge", + path="/merge", + raw_path="/merge/", + ), ] @rx.event @@ -33,9 +45,9 @@ def check_login(self) -> EventSpec | None: @rx.event def load_nav(self) -> None: """Event hook for updating the navigation on page loads.""" - self.item_id = self.router.page.params.get("item_id") - item_id = self.item_id or MEX_PRIMARY_SOURCE_STABLE_TARGET_ID for nav_item in self.nav_items: - nav_item.href = nav_item.href_template.format(item_id=item_id) - current_nav = self.router.page.raw_path == nav_item.href - nav_item.underline = "always" if current_nav else "none" + if self.router.page.path == nav_item.path: + nav_item.update_raw_path(self.router.page.params) + nav_item.underline = "always" + else: + nav_item.underline = "none" diff --git a/mex/mex.py b/mex/mex.py index 782401e..c1e95c1 100644 --- a/mex/mex.py +++ b/mex/mex.py @@ -22,7 +22,7 @@ ) app.add_page( edit_index, - route="/item/[item_id]", + route="/item/[identifier]", title="MEx Editor | Edit", on_load=[State.check_login, State.load_nav, EditState.refresh], ) @@ -48,6 +48,10 @@ check_system_status, tags=["system"], ) +app.api.title = "mex-editor" +app.api.version = "v0" +app.api.contact = {"name": "MEx Team", "email": "mex@rki.de"} +app.api.description = "Metadata editor web application." app.register_lifespan_task( lambda: logger.info(EditorSettings.get().text()), ) diff --git a/pdm.lock b/pdm.lock index 805047f..0bde100 100644 --- a/pdm.lock +++ b/pdm.lock @@ -5,7 +5,7 @@ groups = ["default", "dev"] strategy = ["inherit_metadata"] lock_version = "4.5.0" -content_hash = "sha256:9e9ccdd67c6c891618c17b0e14e7eff382c6577394a766edb78b33de13f98ec2" +content_hash = "sha256:498dc0f6284794c74aad5b984f3abfe7e8fc0aedd6f6fbb717597431aaefd409" [[metadata.targets]] requires_python = "==3.11.*" diff --git a/pyproject.toml b/pyproject.toml index baf7ff9..e0275ee 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,6 @@ dependencies = [ "pydantic>=2,<3", "pytz>=2024,<2025", "pyyaml>=6,<7", - "reflex-chakra>=0.6,<1", "reflex>=0.6,<1", "requests>=2,<3", "starlette>=0.41,<1", @@ -53,10 +52,6 @@ plugins = ["pydantic.mypy"] module = "reflex.*" ignore_missing_imports = true -[[tool.mypy.overrides]] -module = "reflex_chakra.*" -ignore_missing_imports = true - [tool.pdm] distribution = true @@ -64,7 +59,7 @@ distribution = true update-all = { cmd = "pdm update --group :all --update-all --save-compatible" } lock-all = { cmd = "pdm lock --group :all --python='==3.11.*'" } install-lockfile = { cmd = "pdm install --group :all --frozen-lockfile" } -install-playwright = { cmd = "playwright install firefox" } +install-playwright = { cmd = "pdm run playwright install firefox" } install-all = { composite = ["install-lockfile", "install-playwright"] } export-all = { cmd = "pdm export --group :all --no-hashes -f requirements" } apidoc = { cmd = "pdm run sphinx-apidoc -f -o docs/source mex" } diff --git a/tests/conftest.py b/tests/conftest.py index a240002..c402b8a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -100,7 +100,7 @@ def login_user( page.goto(frontend_url) page.get_by_placeholder("Username").fill(username) page.get_by_placeholder("Password").fill(password.get_secret_value()) - page.get_by_text("Log in").click() + page.get_by_test_id("login-button").click() return page diff --git a/tests/edit/test_main.py b/tests/edit/test_main.py index 5f5ad92..73b3cc3 100644 --- a/tests/edit/test_main.py +++ b/tests/edit/test_main.py @@ -19,8 +19,8 @@ def edit_page( ) -> Page: page = writer_user_page page.goto(f"{frontend_url}/item/{extracted_activity.stableTargetId}") - section = page.get_by_test_id("edit-section") - expect(section).to_be_visible() + page_body = page.get_by_test_id("page-body") + expect(page_body).to_be_visible() return page @@ -30,7 +30,8 @@ def test_edit_page_updates_nav_bar(edit_page: Page) -> None: nav_bar = page.get_by_test_id("nav-bar") page.screenshot(path="tests_edit_test_main-test_edit_page_updates_nav_bar.png") expect(nav_bar).to_be_visible() - nav_item = nav_bar.get_by_text("Edit", exact=True) + nav_item = nav_bar.locator(".nav-item").all()[1] + expect(nav_item).to_contain_text("Edit") expect(nav_item).to_have_class(re.compile("rt-underline-always")) diff --git a/tests/login/test_main.py b/tests/login/test_main.py index 0d6474c..a0d68d5 100644 --- a/tests/login/test_main.py +++ b/tests/login/test_main.py @@ -17,7 +17,12 @@ def test_login( ) page.screenshot(path="tests_login_test_main-test_login-on-load.png") - page.get_by_text("Log in").click() + page.get_by_test_id("login-button").click() expect(page.get_by_test_id("nav-bar")).to_be_visible() expect(page.get_by_test_id("merge-heading")).to_be_visible() page.screenshot(path="tests_login_test_main-test_login-after-login.png") + + page.get_by_test_id("user-menu").click() + expect(page.get_by_test_id("logout-button")).to_be_visible() + page.get_by_test_id("logout-button").click() + expect(page.get_by_test_id("login-button")).to_be_visible() diff --git a/tests/search/test_main.py b/tests/search/test_main.py index bd92d5a..b9a7ad8 100644 --- a/tests/search/test_main.py +++ b/tests/search/test_main.py @@ -16,7 +16,7 @@ def test_index(frontend_url: str, writer_user_page: Page) -> None: page.screenshot(path="tests_search_test_main-test_index-on-load.png") # check heading is showing - expect(page.get_by_text("showing 7 of total 7 items found")).to_be_visible() + expect(page.get_by_text("Showing 7 of 7 items")).to_be_visible() # check mex primary source is showing primary_source = page.get_by_text(re.compile(r"^PrimarySource$")) @@ -66,13 +66,13 @@ def test_search_input( search_input = page.get_by_placeholder("Search here...") expect(search_input).to_be_visible() search_input.fill("mex") - expect(page.get_by_text("showing 1 of total 1 items found")).to_be_visible() + expect(page.get_by_text("Showing 1 of 1 items")).to_be_visible() page.screenshot( path="tests_search_test_main-test_index-on-search-input-1-found.png" ) search_input.fill("totally random search dPhGDHu3uiEcU6VNNs0UA74bBdubC3") - expect(page.get_by_text("showing 0 of total 0 items found")).to_be_visible() + expect(page.get_by_text("Showing 0 of 0 items")).to_be_visible() page.screenshot( path="tests_search_test_main-test_index-on-search-input-0-found.png" ) @@ -95,11 +95,11 @@ def test_entity_types( # check entity types are showing and functioning entity_types = page.get_by_test_id("entity-types") expect(entity_types).to_be_visible() - assert "MergedPrimarySource" in entity_types.all_text_contents()[0] + assert "PrimarySource" in entity_types.all_text_contents()[0] - entity_types.get_by_text("MergedActivity").click() - expect(page.get_by_text("showing 1 of total 1 items found")).to_be_visible() + entity_types.get_by_text("Activity").click() + expect(page.get_by_text("Showing 1 of 1 items")).to_be_visible() page.screenshot( path="tests_search_test_main-test_index-on-select-entity-1-found.png" ) - entity_types.get_by_text("MergedActivity").click() + entity_types.get_by_text("Activity").click()