Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/app/reflex.lock/bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions docs/app/reflex.lock/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
"react-helmet": "6.1.0",
"react-leaflet": "5.0.0",
"react-markdown": "10.1.0",
"react-moment": "1.2.2",
"react-moment": "2.0.2",
"react-player": "3.4.0",
"react-plotly.js": "4.1.0",
"react-responsive-carousel": "3.2.23",
Expand Down Expand Up @@ -90,4 +90,4 @@
"vite": "8.2.2"
},
"overrides": {}
}
}
1 change: 1 addition & 0 deletions packages/reflex-components-moment/news/7003.bugfix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Migrate `rx.moment` to `react-moment` 2.0.2 and include its duration-format dependency.
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

from reflex_base.components.component import MemoizationLeaf, NoSSRComponent, field
from reflex_base.event import EventHandler, passthrough_event_spec
from reflex_base.utils.imports import ImportDict
from reflex_base.utils.imports import ImportDict, ImportVar
from reflex_base.vars.base import LiteralVar, Var


Expand All @@ -31,7 +31,7 @@ class Moment(NoSSRComponent, MemoizationLeaf):

tag: str | None = "Moment"
is_default = True
library: str | None = "react-moment@1.2.2"
library: str | None = "react-moment@2.0.2"
lib_dependencies: list[str] = ["moment@2.30.1"]

interval: Var[int] = field(
Expand All @@ -42,12 +42,12 @@ class Moment(NoSSRComponent, MemoizationLeaf):
doc="Formats the date according to the given format string."
)

trim: Var[bool] = field(
doc="When formatting duration time, the largest-magnitude tokens are automatically trimmed when they have no value."
trim: Var[bool | str] = field(
doc='When formatting duration time, the largest-magnitude tokens are automatically trimmed when they have no value. Also accepts a trim template: "large", "small", "both", "all", "final", "left" or "right".'
)

parse: Var[str] = field(
doc=" Use the parse attribute to tell moment how to parse the given date when non-standard."
parse: Var[str | list[str]] = field(
doc=" Use the parse attribute to tell moment how to parse the given date when non-standard. Accepts a single format string or a list of formats to try."
)

add: Var[MomentDelta] = field(
Expand Down Expand Up @@ -132,5 +132,7 @@ def add_imports(self) -> ImportDict:
imports[""] = "moment/min/locales"
if self.tz is not None:
imports["moment-timezone@0.6.3"] = ""
if self.duration is not None or self.duration_from_now is not None:
imports["moment-duration-format@2.2.2"] = ImportVar(tag=None)

return imports
2 changes: 1 addition & 1 deletion pyi_hashes.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
"packages/reflex-components-gridjs/src/reflex_components_gridjs/datatable.pyi": "2ce1c076ecf5c2fa4945b4abdbf2f91d",
"packages/reflex-components-lucide/src/reflex_components_lucide/icon.pyi": "1e331a3d6420b97e5b1ce7f63ad53de8",
"packages/reflex-components-markdown/src/reflex_components_markdown/markdown.pyi": "79d0a59b1ba12a2f2c4a09fa6b5c776f",
"packages/reflex-components-moment/src/reflex_components_moment/moment.pyi": "85d515f5254bb4c188075873ec4d8a51",
"packages/reflex-components-moment/src/reflex_components_moment/moment.pyi": "92cc72695f0868cdbcec912e916541f2",
"packages/reflex-components-plotly/src/reflex_components_plotly/plotly.pyi": "beb057e382e527224597c320dbb72385",
"packages/reflex-components-radix/src/reflex_components_radix/__init__.pyi": "a77352f60fb6f4135b5d08a6e56efa6d",
"packages/reflex-components-radix/src/reflex_components_radix/primitives/__init__.pyi": "bbd4d1a4fa73275a882c33ba485d0165",
Expand Down
87 changes: 87 additions & 0 deletions tests/integration/test_moment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
"""Integration tests for the Moment component."""

from collections.abc import Generator

import pytest
from selenium.webdriver.common.by import By

from reflex.testing import AppHarness, WebDriver


def MomentApp():
"""Create an app that exercises the react-moment 2.x prop changes."""
import reflex as rx

app = rx.App()

class State(rx.State):
"""State used to wait for the browser connection before asserting output."""

@app.add_page
def index():
return rx.vstack(
rx.el.input(
id="token",
value=State.router.session.client_token,
is_read_only=True,
),
rx.moment(
"2026-08-30",
format="YYYY-MM-DD",
parse=["YYYY-MM-DD"],
id="moment",
),
rx.moment(
date="2026-08-30T00:30:00",
duration="2026-08-30T00:00:00",
format="h [hrs] m [min]",
trim="large",
id="moment-duration",
),
)


@pytest.fixture(scope="module")
def moment_app(
tmp_path_factory, app_harness_env: type[AppHarness]
) -> Generator[AppHarness, None, None]:
"""Start the Moment integration app.

Yields:
The running Moment app harness.
"""
with app_harness_env.create(
root=tmp_path_factory.mktemp("moment"), app_source=MomentApp
) as harness:
assert harness.app_instance is not None, "app is not running"
yield harness


@pytest.fixture
def driver(moment_app: AppHarness) -> Generator[WebDriver, None, None]:
"""Open the Moment integration app in a browser.

Yields:
The browser driver connected to the Moment app.
"""
driver = moment_app.frontend()
try:
token = AppHarness.poll_for_or_raise_timeout(
lambda: driver.find_element(By.ID, "token")
)
AppHarness.poll_for_or_raise_timeout(lambda: token.get_attribute("value"))
yield driver
finally:
driver.quit()


def test_moment_2_props_render(driver: WebDriver) -> None:
"""Changed react-moment 2.x props should render without a client error."""
moment = AppHarness.poll_for_or_raise_timeout(
lambda: driver.find_element(By.ID, "moment")
)
AppHarness.expect(lambda: moment.text == "2026-08-30")
moment_duration = AppHarness.poll_for_or_raise_timeout(
lambda: driver.find_element(By.ID, "moment-duration")
)
AppHarness.expect(lambda: moment_duration.text == "30 mins")
27 changes: 27 additions & 0 deletions tests/units/compiler/test_memoize_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from reflex_base.constants.compiler import MemoizationDisposition, MemoizationMode
from reflex_base.plugins import CompileContext, CompilerHooks, PageContext
from reflex_base.utils import memo_paths
from reflex_base.utils.imports import ImportVar
from reflex_base.vars import VarData
from reflex_base.vars.base import Field, LiteralVar, Var, field
from reflex_components_core.base.bare import Bare
Expand Down Expand Up @@ -1841,6 +1842,32 @@ def test_moment_with_stateful_var_child_does_not_wrap_bare_independently() -> No
)


def test_moment_uses_react_moment_2_props_and_dependencies() -> None:
"""The wrapper exposes the react-moment 2.x props and dependencies."""
assert Moment.library == "react-moment@2.0.2"
assert Moment.lib_dependencies == [
"moment@2.30.1",
]

moment = Moment.create(
"2026-08-30",
trim="large",
parse=["YYYY-MM-DD"],
)
props = moment.render()["props"]
assert 'trim:"large"' in props
assert 'parse:["YYYY-MM-DD"]' in props

duration_from_now = Moment.create(
"2026-08-30",
duration_from_now=True,
)
assert duration_from_now.add_imports()["moment-duration-format@2.2.2"] == ImportVar(
tag=None
)
assert "moment-duration-format@2.2.2" not in moment.add_imports()


def test_moment_memo_body_renders_text_interpolation_not_bare_component() -> None:
"""The moment's memo body must interpolate the state Var as text, not a Bare wrapper."""
ctx, _page_ctx = _compile_single_page(
Expand Down
Loading