Merge remote-tracking branch 'reflex-dev/main' into vy/feature/context-menu-wrapping
This commit is contained in:
commit
16f1ec8da8
@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "reflex"
|
||||
version = "0.6.6dev1"
|
||||
version = "0.6.7dev1"
|
||||
description = "Web apps in pure Python."
|
||||
license = "Apache-2.0"
|
||||
authors = [
|
||||
|
@ -454,6 +454,10 @@ export const connect = async (
|
||||
queueEvents(update.events, socket);
|
||||
}
|
||||
});
|
||||
socket.current.on("reload", async (event) => {
|
||||
event_processing = false;
|
||||
queueEvents([...initialEvents(), JSON5.parse(event)], socket);
|
||||
});
|
||||
|
||||
document.addEventListener("visibilitychange", checkVisibility);
|
||||
};
|
||||
@ -486,23 +490,30 @@ export const uploadFiles = async (
|
||||
return false;
|
||||
}
|
||||
|
||||
// Track how many partial updates have been processed for this upload.
|
||||
let resp_idx = 0;
|
||||
const eventHandler = (progressEvent) => {
|
||||
// handle any delta / event streamed from the upload event handler
|
||||
const event_callbacks = socket._callbacks.$event;
|
||||
// Whenever called, responseText will contain the entire response so far.
|
||||
const chunks = progressEvent.event.target.responseText.trim().split("\n");
|
||||
// So only process _new_ chunks beyond resp_idx.
|
||||
chunks.slice(resp_idx).map((chunk) => {
|
||||
try {
|
||||
socket._callbacks.$event.map((f) => {
|
||||
f(chunk);
|
||||
});
|
||||
event_callbacks.map((f, ix) => {
|
||||
f(chunk)
|
||||
.then(() => {
|
||||
if (ix === event_callbacks.length - 1) {
|
||||
// Mark this chunk as processed.
|
||||
resp_idx += 1;
|
||||
} catch (e) {
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
if (progressEvent.progress === 1) {
|
||||
// Chunk may be incomplete, so only report errors when full response is available.
|
||||
console.log("Error parsing chunk", chunk, e);
|
||||
}
|
||||
return;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@ -848,7 +859,7 @@ export const useEventLoop = (
|
||||
if (router.components[router.pathname].error) {
|
||||
delete router.components[router.pathname].error;
|
||||
}
|
||||
}
|
||||
};
|
||||
router.events.on("routeChangeStart", change_start);
|
||||
router.events.on("routeChangeComplete", change_complete);
|
||||
router.events.on("routeChangeError", change_error);
|
||||
|
@ -73,6 +73,7 @@ from reflex.event import (
|
||||
EventSpec,
|
||||
EventType,
|
||||
IndividualEventType,
|
||||
get_hydrate_event,
|
||||
window_alert,
|
||||
)
|
||||
from reflex.model import Model, get_db_status
|
||||
@ -1259,6 +1260,21 @@ async def process(
|
||||
)
|
||||
# Get the state for the session exclusively.
|
||||
async with app.state_manager.modify_state(event.substate_token) as state:
|
||||
# When this is a brand new instance of the state, signal the
|
||||
# frontend to reload before processing it.
|
||||
if (
|
||||
not state.router_data
|
||||
and event.name != get_hydrate_event(state)
|
||||
and app.event_namespace is not None
|
||||
):
|
||||
await asyncio.create_task(
|
||||
app.event_namespace.emit(
|
||||
"reload",
|
||||
data=format.json_dumps(event),
|
||||
to=sid,
|
||||
)
|
||||
)
|
||||
return
|
||||
# re-assign only when the value is different
|
||||
if state.router_data != router_data:
|
||||
# assignment will recurse into substates and force recalculation of
|
||||
|
@ -293,13 +293,15 @@ class Upload(MemoizationLeaf):
|
||||
format.to_camel_case(key): value for key, value in upload_props.items()
|
||||
}
|
||||
|
||||
use_dropzone_arguments = {
|
||||
use_dropzone_arguments = Var.create(
|
||||
{
|
||||
"onDrop": event_var,
|
||||
**upload_props,
|
||||
}
|
||||
)
|
||||
|
||||
left_side = f"const {{getRootProps: {root_props_unique_name}, getInputProps: {input_props_unique_name}}} "
|
||||
right_side = f"useDropzone({str(Var.create(use_dropzone_arguments))})"
|
||||
right_side = f"useDropzone({str(use_dropzone_arguments)})"
|
||||
|
||||
var_data = VarData.merge(
|
||||
VarData(
|
||||
@ -307,6 +309,7 @@ class Upload(MemoizationLeaf):
|
||||
hooks={Hooks.EVENTS: None},
|
||||
),
|
||||
event_var._get_all_var_data(),
|
||||
use_dropzone_arguments._get_all_var_data(),
|
||||
VarData(
|
||||
hooks={
|
||||
callback_str: None,
|
||||
|
@ -570,6 +570,9 @@ class Textarea(BaseHTML):
|
||||
# Visible width of the text control, in average character widths
|
||||
cols: Var[Union[str, int, bool]]
|
||||
|
||||
# The default value of the textarea when initially rendered
|
||||
default_value: Var[str]
|
||||
|
||||
# Name part of the textarea to submit in 'dir' and 'name' pair when form is submitted
|
||||
dirname: Var[Union[str, int, bool]]
|
||||
|
||||
|
@ -1350,6 +1350,7 @@ class Textarea(BaseHTML):
|
||||
auto_focus: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
|
||||
auto_height: Optional[Union[Var[bool], bool]] = None,
|
||||
cols: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
|
||||
default_value: Optional[Union[Var[str], str]] = None,
|
||||
dirname: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
|
||||
disabled: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
|
||||
enter_key_submit: Optional[Union[Var[bool], bool]] = None,
|
||||
@ -1439,6 +1440,7 @@ class Textarea(BaseHTML):
|
||||
auto_focus: Automatically focuses the textarea when the page loads
|
||||
auto_height: Automatically fit the content height to the text (use min-height with this prop)
|
||||
cols: Visible width of the text control, in average character widths
|
||||
default_value: The default value of the textarea when initially rendered
|
||||
dirname: Name part of the textarea to submit in 'dir' and 'name' pair when form is submitted
|
||||
disabled: Disables the textarea
|
||||
enter_key_submit: Enter key submits form (shift-enter adds new line)
|
||||
|
@ -41,6 +41,9 @@ class TextArea(RadixThemesComponent, elements.Textarea):
|
||||
# Automatically focuses the textarea when the page loads
|
||||
auto_focus: Var[bool]
|
||||
|
||||
# The default value of the textarea when initially rendered
|
||||
default_value: Var[str]
|
||||
|
||||
# Name part of the textarea to submit in 'dir' and 'name' pair when form is submitted
|
||||
dirname: Var[str]
|
||||
|
||||
|
@ -123,6 +123,7 @@ class TextArea(RadixThemesComponent, elements.Textarea):
|
||||
] = None,
|
||||
auto_complete: Optional[Union[Var[bool], bool]] = None,
|
||||
auto_focus: Optional[Union[Var[bool], bool]] = None,
|
||||
default_value: Optional[Union[Var[str], str]] = None,
|
||||
dirname: Optional[Union[Var[str], str]] = None,
|
||||
disabled: Optional[Union[Var[bool], bool]] = None,
|
||||
form: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
|
||||
@ -217,6 +218,7 @@ class TextArea(RadixThemesComponent, elements.Textarea):
|
||||
radius: The radius of the text area: "none" | "small" | "medium" | "large" | "full"
|
||||
auto_complete: Whether the form control should have autocomplete enabled
|
||||
auto_focus: Automatically focuses the textarea when the page loads
|
||||
default_value: The default value of the textarea when initially rendered
|
||||
dirname: Name part of the textarea to submit in 'dir' and 'name' pair when form is submitted
|
||||
disabled: Disables the textarea
|
||||
form: Associates the textarea with a form (by id)
|
||||
|
@ -3,7 +3,6 @@
|
||||
from typing import Dict, Literal
|
||||
|
||||
from reflex.components.component import Component, MemoizationLeaf, NoSSRComponent
|
||||
from reflex.utils import console
|
||||
|
||||
|
||||
class Recharts(Component):
|
||||
@ -11,19 +10,8 @@ class Recharts(Component):
|
||||
|
||||
library = "recharts@2.13.0"
|
||||
|
||||
def render(self) -> Dict:
|
||||
"""Render the tag.
|
||||
|
||||
Returns:
|
||||
The rendered tag.
|
||||
"""
|
||||
tag = super().render()
|
||||
if any(p.startswith("css") for p in tag["props"]):
|
||||
console.warn(
|
||||
f"CSS props do not work for {self.__class__.__name__}. Consult docs to style it with its own prop."
|
||||
)
|
||||
tag["props"] = [p for p in tag["props"] if not p.startswith("css")]
|
||||
return tag
|
||||
def _get_style(self) -> Dict:
|
||||
return {"wrapperStyle": self.style}
|
||||
|
||||
|
||||
class RechartsCharts(NoSSRComponent, MemoizationLeaf):
|
||||
|
@ -11,7 +11,6 @@ from reflex.style import Style
|
||||
from reflex.vars.base import Var
|
||||
|
||||
class Recharts(Component):
|
||||
def render(self) -> Dict: ...
|
||||
@overload
|
||||
@classmethod
|
||||
def create( # type: ignore
|
||||
|
@ -1959,6 +1959,9 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
|
||||
if var in self.base_vars or var in self._backend_vars:
|
||||
self._was_touched = True
|
||||
break
|
||||
if var == constants.ROUTER_DATA and self.parent_state is None:
|
||||
self._was_touched = True
|
||||
break
|
||||
|
||||
def _get_was_touched(self) -> bool:
|
||||
"""Check current dirty_vars and flag to determine if state instance was modified.
|
||||
|
@ -10,6 +10,13 @@ from selenium.webdriver import Firefox
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.remote.webdriver import WebDriver
|
||||
|
||||
from reflex.state import (
|
||||
State,
|
||||
StateManagerDisk,
|
||||
StateManagerMemory,
|
||||
StateManagerRedis,
|
||||
_substate_key,
|
||||
)
|
||||
from reflex.testing import AppHarness
|
||||
|
||||
from . import utils
|
||||
@ -74,7 +81,7 @@ def ClientSide():
|
||||
return rx.fragment(
|
||||
rx.input(
|
||||
value=ClientSideState.router.session.client_token,
|
||||
is_read_only=True,
|
||||
read_only=True,
|
||||
id="token",
|
||||
),
|
||||
rx.input(
|
||||
@ -604,6 +611,110 @@ async def test_client_side_state(
|
||||
assert s2.text == "s2 value"
|
||||
assert s3.text == "s3 value"
|
||||
|
||||
# Simulate state expiration
|
||||
if isinstance(client_side.state_manager, StateManagerRedis):
|
||||
await client_side.state_manager.redis.delete(
|
||||
_substate_key(token, State.get_full_name())
|
||||
)
|
||||
await client_side.state_manager.redis.delete(_substate_key(token, state_name))
|
||||
await client_side.state_manager.redis.delete(
|
||||
_substate_key(token, sub_state_name)
|
||||
)
|
||||
await client_side.state_manager.redis.delete(
|
||||
_substate_key(token, sub_sub_state_name)
|
||||
)
|
||||
elif isinstance(client_side.state_manager, (StateManagerMemory, StateManagerDisk)):
|
||||
del client_side.state_manager.states[token]
|
||||
if isinstance(client_side.state_manager, StateManagerDisk):
|
||||
client_side.state_manager.token_expiration = 0
|
||||
client_side.state_manager._purge_expired_states()
|
||||
|
||||
# Ensure the state is gone (not hydrated)
|
||||
async def poll_for_not_hydrated():
|
||||
state = await client_side.get_state(_substate_key(token or "", state_name))
|
||||
return not state.is_hydrated
|
||||
|
||||
assert await AppHarness._poll_for_async(poll_for_not_hydrated)
|
||||
|
||||
# Trigger event to get a new instance of the state since the old was expired.
|
||||
state_var_input = driver.find_element(By.ID, "state_var")
|
||||
state_var_input.send_keys("re-triggering")
|
||||
|
||||
# get new references to all cookie and local storage elements (again)
|
||||
c1 = driver.find_element(By.ID, "c1")
|
||||
c2 = driver.find_element(By.ID, "c2")
|
||||
c3 = driver.find_element(By.ID, "c3")
|
||||
c4 = driver.find_element(By.ID, "c4")
|
||||
c5 = driver.find_element(By.ID, "c5")
|
||||
c6 = driver.find_element(By.ID, "c6")
|
||||
c7 = driver.find_element(By.ID, "c7")
|
||||
l1 = driver.find_element(By.ID, "l1")
|
||||
l2 = driver.find_element(By.ID, "l2")
|
||||
l3 = driver.find_element(By.ID, "l3")
|
||||
l4 = driver.find_element(By.ID, "l4")
|
||||
s1 = driver.find_element(By.ID, "s1")
|
||||
s2 = driver.find_element(By.ID, "s2")
|
||||
s3 = driver.find_element(By.ID, "s3")
|
||||
c1s = driver.find_element(By.ID, "c1s")
|
||||
l1s = driver.find_element(By.ID, "l1s")
|
||||
s1s = driver.find_element(By.ID, "s1s")
|
||||
|
||||
assert c1.text == "c1 value"
|
||||
assert c2.text == "c2 value"
|
||||
assert c3.text == "" # temporary cookie expired after reset state!
|
||||
assert c4.text == "c4 value"
|
||||
assert c5.text == "c5 value"
|
||||
assert c6.text == "c6 value"
|
||||
assert c7.text == "c7 value"
|
||||
assert l1.text == "l1 value"
|
||||
assert l2.text == "l2 value"
|
||||
assert l3.text == "l3 value"
|
||||
assert l4.text == "l4 value"
|
||||
assert s1.text == "s1 value"
|
||||
assert s2.text == "s2 value"
|
||||
assert s3.text == "s3 value"
|
||||
assert c1s.text == "c1s value"
|
||||
assert l1s.text == "l1s value"
|
||||
assert s1s.text == "s1s value"
|
||||
|
||||
# Get the backend state and ensure the values are still set
|
||||
async def get_sub_state():
|
||||
root_state = await client_side.get_state(
|
||||
_substate_key(token or "", sub_state_name)
|
||||
)
|
||||
state = root_state.substates[client_side.get_state_name("_client_side_state")]
|
||||
sub_state = state.substates[
|
||||
client_side.get_state_name("_client_side_sub_state")
|
||||
]
|
||||
return sub_state
|
||||
|
||||
async def poll_for_c1_set():
|
||||
sub_state = await get_sub_state()
|
||||
return sub_state.c1 == "c1 value"
|
||||
|
||||
assert await AppHarness._poll_for_async(poll_for_c1_set)
|
||||
sub_state = await get_sub_state()
|
||||
assert sub_state.c1 == "c1 value"
|
||||
assert sub_state.c2 == "c2 value"
|
||||
assert sub_state.c3 == ""
|
||||
assert sub_state.c4 == "c4 value"
|
||||
assert sub_state.c5 == "c5 value"
|
||||
assert sub_state.c6 == "c6 value"
|
||||
assert sub_state.c7 == "c7 value"
|
||||
assert sub_state.l1 == "l1 value"
|
||||
assert sub_state.l2 == "l2 value"
|
||||
assert sub_state.l3 == "l3 value"
|
||||
assert sub_state.l4 == "l4 value"
|
||||
assert sub_state.s1 == "s1 value"
|
||||
assert sub_state.s2 == "s2 value"
|
||||
assert sub_state.s3 == "s3 value"
|
||||
sub_sub_state = sub_state.substates[
|
||||
client_side.get_state_name("_client_side_sub_sub_state")
|
||||
]
|
||||
assert sub_sub_state.c1s == "c1s value"
|
||||
assert sub_sub_state.l1s == "l1s value"
|
||||
assert sub_sub_state.s1s == "s1s value"
|
||||
|
||||
# clear the cookie jar and local storage, ensure state reset to default
|
||||
driver.delete_all_cookies()
|
||||
local_storage.clear()
|
||||
|
@ -19,10 +19,14 @@ def UploadFile():
|
||||
|
||||
import reflex as rx
|
||||
|
||||
LARGE_DATA = "DUMMY" * 1024 * 512
|
||||
|
||||
class UploadState(rx.State):
|
||||
_file_data: Dict[str, str] = {}
|
||||
event_order: List[str] = []
|
||||
progress_dicts: List[dict] = []
|
||||
disabled: bool = False
|
||||
large_data: str = ""
|
||||
|
||||
async def handle_upload(self, files: List[rx.UploadFile]):
|
||||
for file in files:
|
||||
@ -33,6 +37,7 @@ def UploadFile():
|
||||
for file in files:
|
||||
upload_data = await file.read()
|
||||
self._file_data[file.filename or ""] = upload_data.decode("utf-8")
|
||||
self.large_data = LARGE_DATA
|
||||
yield UploadState.chain_event
|
||||
|
||||
def upload_progress(self, progress):
|
||||
@ -41,13 +46,15 @@ def UploadFile():
|
||||
self.progress_dicts.append(progress)
|
||||
|
||||
def chain_event(self):
|
||||
assert self.large_data == LARGE_DATA
|
||||
self.large_data = ""
|
||||
self.event_order.append("chain_event")
|
||||
|
||||
def index():
|
||||
return rx.vstack(
|
||||
rx.input(
|
||||
value=UploadState.router.session.client_token,
|
||||
is_read_only=True,
|
||||
read_only=True,
|
||||
id="token",
|
||||
),
|
||||
rx.heading("Default Upload"),
|
||||
@ -56,6 +63,7 @@ def UploadFile():
|
||||
rx.button("Select File"),
|
||||
rx.text("Drag and drop files here or click to select files"),
|
||||
),
|
||||
disabled=UploadState.disabled,
|
||||
),
|
||||
rx.button(
|
||||
"Upload",
|
||||
|
@ -1007,7 +1007,8 @@ async def test_dynamic_route_var_route_change_completed_on_load(
|
||||
substate_token = _substate_key(token, DynamicState)
|
||||
sid = "mock_sid"
|
||||
client_ip = "127.0.0.1"
|
||||
state = await app.state_manager.get_state(substate_token)
|
||||
async with app.state_manager.modify_state(substate_token) as state:
|
||||
state.router_data = {"simulate": "hydrated"}
|
||||
assert state.dynamic == ""
|
||||
exp_vals = ["foo", "foobar", "baz"]
|
||||
|
||||
@ -1180,6 +1181,7 @@ async def test_process_events(mocker, token: str):
|
||||
"ip": "127.0.0.1",
|
||||
}
|
||||
app = App(state=GenState)
|
||||
|
||||
mocker.patch.object(app, "_postprocess", AsyncMock())
|
||||
event = Event(
|
||||
token=token,
|
||||
@ -1187,6 +1189,8 @@ async def test_process_events(mocker, token: str):
|
||||
payload={"c": 5},
|
||||
router_data=router_data,
|
||||
)
|
||||
async with app.state_manager.modify_state(event.substate_token) as state:
|
||||
state.router_data = {"simulate": "hydrated"}
|
||||
|
||||
async for _update in process(app, event, "mock_sid", {}, "127.0.0.1"):
|
||||
pass
|
||||
|
@ -1982,6 +1982,10 @@ class BackgroundTaskState(BaseState):
|
||||
order: List[str] = []
|
||||
dict_list: Dict[str, List[int]] = {"foo": [1, 2, 3]}
|
||||
|
||||
def __init__(self, **kwargs): # noqa: D107
|
||||
super().__init__(**kwargs)
|
||||
self.router_data = {"simulate": "hydrate"}
|
||||
|
||||
@rx.var
|
||||
def computed_order(self) -> List[str]:
|
||||
"""Get the order as a computed var.
|
||||
@ -2732,7 +2736,7 @@ def test_set_base_field_via_setter():
|
||||
assert "c2" in bfss.dirty_vars
|
||||
|
||||
|
||||
def exp_is_hydrated(state: State, is_hydrated: bool = True) -> Dict[str, Any]:
|
||||
def exp_is_hydrated(state: BaseState, is_hydrated: bool = True) -> Dict[str, Any]:
|
||||
"""Expected IS_HYDRATED delta that would be emitted by HydrateMiddleware.
|
||||
|
||||
Args:
|
||||
@ -2811,7 +2815,8 @@ async def test_preprocess(app_module_mock, token, test_state, expected, mocker):
|
||||
app = app_module_mock.app = App(
|
||||
state=State, load_events={"index": [test_state.test_handler]}
|
||||
)
|
||||
state = State()
|
||||
async with app.state_manager.modify_state(_substate_key(token, State)) as state:
|
||||
state.router_data = {"simulate": "hydrate"}
|
||||
|
||||
updates = []
|
||||
async for update in rx.app.process(
|
||||
@ -2858,7 +2863,8 @@ async def test_preprocess_multiple_load_events(app_module_mock, token, mocker):
|
||||
state=State,
|
||||
load_events={"index": [OnLoadState.test_handler, OnLoadState.test_handler]},
|
||||
)
|
||||
state = State()
|
||||
async with app.state_manager.modify_state(_substate_key(token, State)) as state:
|
||||
state.router_data = {"simulate": "hydrate"}
|
||||
|
||||
updates = []
|
||||
async for update in rx.app.process(
|
||||
|
Loading…
Reference in New Issue
Block a user