use crate::{
ChartAnnotation, ChartPoint, GridRow, GridRowsWindow, GridState, InboxItem, JsInteropDispatch,
PubSubCommand, RuntimeCommand, RuntimeEvent, ServerMessage, StreamBatchOperation,
StreamPosition, Toast,
};
use std::collections::BTreeMap;
use std::fmt;
use uuid::Uuid;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct SessionId(String);
impl SessionId {
pub fn new() -> Self {
Self(Uuid::new_v4().to_string())
}
pub(crate) fn from_string(value: String) -> Self {
Self(value)
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl Default for SessionId {
fn default() -> Self {
Self::new()
}
}
impl fmt::Display for SessionId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Debug, Clone)]
pub struct Context {
session_id: SessionId,
target_dom_id: String,
route_path: String,
route_params: BTreeMap<String, String>,
tenant_id: Option<String>,
connected: bool,
render_after_event: bool,
render_cadence_override_ms: Option<u64>,
pushes: Vec<ServerMessage>,
pubsub: Vec<PubSubCommand>,
runtime: Vec<RuntimeCommand>,
}
impl Context {
pub fn new(target_dom_id: impl Into<String>) -> Self {
Self::new_with_session_id(SessionId::new(), target_dom_id)
}
pub(crate) fn new_with_session_id(
session_id: SessionId,
target_dom_id: impl Into<String>,
) -> Self {
Self {
session_id,
target_dom_id: target_dom_id.into(),
route_path: "/".to_string(),
route_params: BTreeMap::new(),
tenant_id: None,
connected: false,
render_after_event: true,
render_cadence_override_ms: None,
pushes: Vec::new(),
pubsub: Vec::new(),
runtime: Vec::new(),
}
}
pub fn session_id(&self) -> &SessionId {
&self.session_id
}
pub fn target_dom_id(&self) -> &str {
&self.target_dom_id
}
pub fn route_path(&self) -> &str {
&self.route_path
}
pub fn route_params(&self) -> &BTreeMap<String, String> {
&self.route_params
}
pub fn route_param(&self, key: &str) -> Option<&str> {
self.route_params.get(key).map(String::as_str)
}
pub fn tenant_id(&self) -> Option<&str> {
self.tenant_id.as_deref()
}
pub fn set_tenant_id(&mut self, tenant_id: impl Into<String>) {
self.tenant_id = normalize_tenant_id(Some(tenant_id.into()));
}
pub fn set_tenant_id_optional(&mut self, tenant_id: Option<String>) {
self.tenant_id = normalize_tenant_id(tenant_id);
}
pub fn clear_tenant_id(&mut self) {
self.tenant_id = None;
}
pub(crate) fn set_route(&mut self, path: impl Into<String>, params: BTreeMap<String, String>) {
self.route_path = path.into();
self.route_params = params;
}
pub fn is_connected(&self) -> bool {
self.connected
}
pub fn set_connected(&mut self, connected: bool) {
self.connected = connected;
}
pub fn push_message(&mut self, message: ServerMessage) {
self.pushes.push(message);
}
pub fn subscribe(&mut self, topic: impl Into<String>) {
self.pubsub.push(PubSubCommand::Subscribe {
topic: topic.into(),
});
}
pub fn broadcast(&mut self, topic: impl Into<String>, messages: Vec<ServerMessage>) {
self.skip_render();
self.pubsub.push(PubSubCommand::Broadcast {
topic: topic.into(),
messages,
});
}
pub fn broadcast_message(&mut self, topic: impl Into<String>, message: ServerMessage) {
self.broadcast(topic, vec![message]);
}
pub fn schedule_once(
&mut self,
id: impl Into<String>,
delay_ms: u64,
event: impl Into<String>,
value: impl Into<serde_json::Value>,
) {
self.skip_render();
self.runtime.push(RuntimeCommand::ScheduleOnce {
id: id.into(),
delay_ms,
dispatch: RuntimeEvent::new(event, value),
});
}
pub fn schedule_once_to(
&mut self,
id: impl Into<String>,
delay_ms: u64,
target: impl Into<String>,
event: impl Into<String>,
value: impl Into<serde_json::Value>,
) {
self.skip_render();
self.runtime.push(RuntimeCommand::ScheduleOnce {
id: id.into(),
delay_ms,
dispatch: RuntimeEvent::with_target(target, event, value),
});
}
pub fn schedule_interval(
&mut self,
id: impl Into<String>,
every_ms: u64,
event: impl Into<String>,
value: impl Into<serde_json::Value>,
) {
self.skip_render();
self.runtime.push(RuntimeCommand::ScheduleInterval {
id: id.into(),
every_ms,
dispatch: RuntimeEvent::new(event, value),
});
}
pub fn schedule_interval_to(
&mut self,
id: impl Into<String>,
every_ms: u64,
target: impl Into<String>,
event: impl Into<String>,
value: impl Into<serde_json::Value>,
) {
self.skip_render();
self.runtime.push(RuntimeCommand::ScheduleInterval {
id: id.into(),
every_ms,
dispatch: RuntimeEvent::with_target(target, event, value),
});
}
pub fn cancel_schedule(&mut self, id: impl Into<String>) {
self.skip_render();
self.runtime.push(RuntimeCommand::Cancel { id: id.into() });
}
pub fn request_render(&mut self) {
self.render_after_event = true;
}
pub fn set_render_cadence_ms(&mut self, cadence_ms: u64) {
self.render_cadence_override_ms = Some(cadence_ms);
}
pub fn clear_render_cadence_override(&mut self) {
self.render_cadence_override_ms = None;
}
pub fn skip_render(&mut self) {
self.render_after_event = false;
}
pub fn redirect(&mut self, to: impl Into<String>) {
self.push_message(ServerMessage::Redirect { to: to.into() });
}
pub fn patch_url(&mut self, to: impl Into<String>) {
self.push_message(ServerMessage::PatchUrl { to: to.into() });
}
pub fn navigate(&mut self, to: impl Into<String>) {
self.push_message(ServerMessage::Navigate { to: to.into() });
}
pub fn stream_insert(
&mut self,
target: impl Into<String>,
id: impl Into<String>,
html: impl Into<String>,
at: StreamPosition,
) {
self.skip_render();
self.push_message(ServerMessage::StreamInsert {
target: target.into(),
id: id.into(),
html: html.into(),
at,
});
}
pub fn stream_append(
&mut self,
target: impl Into<String>,
id: impl Into<String>,
html: impl Into<String>,
) {
self.stream_insert(target, id, html, StreamPosition::Append);
}
pub fn stream_prepend(
&mut self,
target: impl Into<String>,
id: impl Into<String>,
html: impl Into<String>,
) {
self.stream_insert(target, id, html, StreamPosition::Prepend);
}
pub fn stream_delete(&mut self, target: impl Into<String>, id: impl Into<String>) {
self.skip_render();
self.push_message(ServerMessage::StreamDelete {
target: target.into(),
id: id.into(),
});
}
pub fn stream_batch(
&mut self,
target: impl Into<String>,
operations: Vec<StreamBatchOperation>,
) {
if operations.is_empty() {
return;
}
self.skip_render();
self.push_message(ServerMessage::StreamBatch {
target: target.into(),
operations,
});
}
pub fn stream_append_many(&mut self, target: impl Into<String>, items: Vec<(String, String)>) {
if items.is_empty() {
return;
}
self.stream_batch(
target,
items
.into_iter()
.map(|(id, html)| StreamBatchOperation::Insert {
id,
html,
at: StreamPosition::Append,
})
.collect(),
);
}
pub fn stream_prepend_many(&mut self, target: impl Into<String>, items: Vec<(String, String)>) {
if items.is_empty() {
return;
}
self.stream_batch(
target,
items
.into_iter()
.map(|(id, html)| StreamBatchOperation::Insert {
id,
html,
at: StreamPosition::Prepend,
})
.collect(),
);
}
pub fn chart_series_append(
&mut self,
chart: impl Into<String>,
series: impl Into<String>,
point: ChartPoint,
) {
self.skip_render();
self.push_message(ServerMessage::ChartSeriesAppend {
chart: chart.into(),
series: series.into(),
point,
});
}
pub fn chart_series_append_many(
&mut self,
chart: impl Into<String>,
series: impl Into<String>,
points: Vec<ChartPoint>,
) {
if points.is_empty() {
return;
}
self.skip_render();
self.push_message(ServerMessage::ChartSeriesAppendMany {
chart: chart.into(),
series: series.into(),
points,
});
}
pub fn chart_series_replace(
&mut self,
chart: impl Into<String>,
series: impl Into<String>,
points: Vec<ChartPoint>,
) {
self.skip_render();
self.push_message(ServerMessage::ChartSeriesReplace {
chart: chart.into(),
series: series.into(),
points,
});
}
pub fn chart_reset(&mut self, chart: impl Into<String>) {
self.skip_render();
self.push_message(ServerMessage::ChartReset {
chart: chart.into(),
});
}
pub fn chart_annotation_upsert(
&mut self,
chart: impl Into<String>,
annotation: ChartAnnotation,
) {
self.skip_render();
self.push_message(ServerMessage::ChartAnnotationUpsert {
chart: chart.into(),
annotation,
});
}
pub fn chart_annotation_delete(&mut self, chart: impl Into<String>, id: impl Into<String>) {
self.skip_render();
self.push_message(ServerMessage::ChartAnnotationDelete {
chart: chart.into(),
id: id.into(),
});
}
pub fn toast_push(&mut self, toast: Toast) {
self.skip_render();
self.push_message(ServerMessage::ToastPush { toast });
}
pub fn toast_dismiss(&mut self, id: impl Into<String>) {
self.skip_render();
self.push_message(ServerMessage::ToastDismiss { id: id.into() });
}
pub fn inbox_upsert(&mut self, item: InboxItem) {
self.skip_render();
self.push_message(ServerMessage::InboxUpsert { item });
}
pub fn inbox_delete(&mut self, id: impl Into<String>) {
self.skip_render();
self.push_message(ServerMessage::InboxDelete { id: id.into() });
}
pub fn grid_replace(&mut self, grid: impl Into<String>, state: GridState) {
self.skip_render();
self.push_message(ServerMessage::GridReplace {
grid: grid.into(),
state,
});
}
pub fn grid_rows_replace(
&mut self,
grid: impl Into<String>,
offset: usize,
total_rows: usize,
rows: Vec<GridRow>,
) {
self.skip_render();
self.push_message(ServerMessage::GridRowsReplace {
grid: grid.into(),
window: GridRowsWindow {
offset,
total_rows,
rows,
},
});
}
pub fn interop_dispatch(
&mut self,
event: impl Into<String>,
detail: impl Into<serde_json::Value>,
) {
self.interop_dispatch_to(None::<String>, event, detail);
}
pub fn interop_dispatch_to(
&mut self,
target: impl Into<Option<String>>,
event: impl Into<String>,
detail: impl Into<serde_json::Value>,
) {
self.skip_render();
self.push_message(ServerMessage::InteropDispatch {
dispatch: JsInteropDispatch {
target: target.into(),
event: event.into(),
detail: detail.into(),
bubbles: true,
},
});
}
pub fn webrtc_signal(&mut self, detail: impl Into<serde_json::Value>) {
self.interop_dispatch("shelly:webrtc-signal", detail);
}
pub fn webrtc_signal_to(
&mut self,
target: impl Into<String>,
detail: impl Into<serde_json::Value>,
) {
self.interop_dispatch_to(Some(target.into()), "shelly:webrtc-signal", detail);
}
pub(crate) fn drain_pushes(&mut self) -> Vec<ServerMessage> {
std::mem::take(&mut self.pushes)
}
pub(crate) fn drain_pubsub_commands(&mut self) -> Vec<PubSubCommand> {
std::mem::take(&mut self.pubsub)
}
pub(crate) fn drain_runtime_commands(&mut self) -> Vec<RuntimeCommand> {
std::mem::take(&mut self.runtime)
}
pub(crate) fn schedule_internal_once(
&mut self,
id: impl Into<String>,
delay_ms: u64,
event: impl Into<String>,
value: impl Into<serde_json::Value>,
) {
self.runtime.push(RuntimeCommand::ScheduleOnce {
id: id.into(),
delay_ms,
dispatch: RuntimeEvent::new(event, value),
});
}
pub(crate) fn cancel_schedule_internal(&mut self, id: impl Into<String>) {
self.runtime.push(RuntimeCommand::Cancel { id: id.into() });
}
pub(crate) fn take_render_after_event(&mut self) -> bool {
let render = self.render_after_event;
self.render_after_event = true;
render
}
pub(crate) fn render_cadence_override_ms(&self) -> Option<u64> {
self.render_cadence_override_ms
}
}
fn normalize_tenant_id(tenant_id: Option<String>) -> Option<String> {
tenant_id
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty())
}
#[cfg(test)]
mod tests {
use super::Context;
use crate::{
ChartAnnotation, ChartPoint, GridColumn, GridPinned, GridRow, GridRowsWindow,
GridSavedView, GridSort, GridSortDirection, GridState, InboxItem, JsInteropDispatch,
PubSubCommand, RuntimeCommand, RuntimeEvent, ServerMessage, StreamBatchOperation,
StreamPosition, Toast, ToastLevel,
};
use serde_json::json;
#[test]
fn context_can_queue_redirects() {
let mut ctx = Context::new("root");
ctx.redirect("/next");
assert_eq!(
ctx.drain_pushes(),
vec![ServerMessage::Redirect {
to: "/next".to_string()
}]
);
}
#[test]
fn context_tracks_route_params() {
let mut ctx = Context::new("root");
ctx.set_route(
"/pages/intro",
[("slug".to_string(), "intro".to_string())].into(),
);
assert_eq!(ctx.route_path(), "/pages/intro");
assert_eq!(ctx.route_param("slug"), Some("intro"));
assert_eq!(ctx.route_param("missing"), None);
}
#[test]
fn context_tracks_tenant_context() {
let mut ctx = Context::new("root");
assert_eq!(ctx.tenant_id(), None);
ctx.set_tenant_id("tenant-a");
assert_eq!(ctx.tenant_id(), Some("tenant-a"));
ctx.set_tenant_id_optional(Some(" ".to_string()));
assert_eq!(ctx.tenant_id(), None);
ctx.set_tenant_id_optional(Some("tenant-b".to_string()));
assert_eq!(ctx.tenant_id(), Some("tenant-b"));
ctx.clear_tenant_id();
assert_eq!(ctx.tenant_id(), None);
}
#[test]
fn context_can_queue_navigation_messages() {
let mut ctx = Context::new("root");
ctx.patch_url("/pages/intro");
ctx.navigate("/users/1");
assert_eq!(
ctx.drain_pushes(),
vec![
ServerMessage::PatchUrl {
to: "/pages/intro".to_string()
},
ServerMessage::Navigate {
to: "/users/1".to_string()
},
]
);
}
#[test]
fn context_can_queue_stream_messages_without_root_render() {
let mut ctx = Context::new("root");
ctx.stream_append("messages", "msg-1", "<li id=\"msg-1\">hi</li>");
ctx.stream_prepend("messages", "msg-0", "<li id=\"msg-0\">first</li>");
ctx.stream_delete("messages", "msg-1");
assert!(!ctx.take_render_after_event());
assert_eq!(
ctx.drain_pushes(),
vec![
ServerMessage::StreamInsert {
target: "messages".to_string(),
id: "msg-1".to_string(),
html: "<li id=\"msg-1\">hi</li>".to_string(),
at: StreamPosition::Append,
},
ServerMessage::StreamInsert {
target: "messages".to_string(),
id: "msg-0".to_string(),
html: "<li id=\"msg-0\">first</li>".to_string(),
at: StreamPosition::Prepend,
},
ServerMessage::StreamDelete {
target: "messages".to_string(),
id: "msg-1".to_string(),
},
]
);
assert!(ctx.take_render_after_event());
}
#[test]
fn context_can_set_and_clear_render_cadence_override() {
let mut ctx = Context::new("root");
assert_eq!(ctx.render_cadence_override_ms(), None);
ctx.set_render_cadence_ms(24);
assert_eq!(ctx.render_cadence_override_ms(), Some(24));
ctx.clear_render_cadence_override();
assert_eq!(ctx.render_cadence_override_ms(), None);
}
#[test]
fn context_can_queue_stream_batch_messages_without_root_render() {
let mut ctx = Context::new("root");
ctx.stream_batch(
"messages",
vec![
StreamBatchOperation::Insert {
id: "msg-2".to_string(),
html: "<li id=\"msg-2\">batch</li>".to_string(),
at: StreamPosition::Append,
},
StreamBatchOperation::Delete {
id: "msg-1".to_string(),
},
],
);
assert!(!ctx.take_render_after_event());
assert_eq!(
ctx.drain_pushes(),
vec![ServerMessage::StreamBatch {
target: "messages".to_string(),
operations: vec![
StreamBatchOperation::Insert {
id: "msg-2".to_string(),
html: "<li id=\"msg-2\">batch</li>".to_string(),
at: StreamPosition::Append,
},
StreamBatchOperation::Delete {
id: "msg-1".to_string(),
},
],
}]
);
assert!(ctx.take_render_after_event());
}
#[test]
fn context_can_queue_pubsub_commands_without_root_render() {
let mut ctx = Context::new("root");
ctx.subscribe("chat:lobby");
ctx.broadcast_message(
"chat:lobby",
ServerMessage::StreamDelete {
target: "messages".to_string(),
id: "msg-1".to_string(),
},
);
assert!(!ctx.take_render_after_event());
assert_eq!(
ctx.drain_pubsub_commands(),
vec![
PubSubCommand::Subscribe {
topic: "chat:lobby".to_string()
},
PubSubCommand::Broadcast {
topic: "chat:lobby".to_string(),
messages: vec![ServerMessage::StreamDelete {
target: "messages".to_string(),
id: "msg-1".to_string(),
}],
}
]
);
}
#[test]
fn context_can_queue_runtime_commands_without_root_render() {
let mut ctx = Context::new("root");
ctx.schedule_once("retry-once", 250, "load_more", json!({"cursor": "a1"}));
ctx.schedule_interval_to(
"heartbeat",
1000,
"grid",
"tick",
json!({"source": "timer"}),
);
ctx.cancel_schedule("retry-once");
assert!(!ctx.take_render_after_event());
assert_eq!(
ctx.drain_runtime_commands(),
vec![
RuntimeCommand::ScheduleOnce {
id: "retry-once".to_string(),
delay_ms: 250,
dispatch: RuntimeEvent::new("load_more", json!({"cursor": "a1"})),
},
RuntimeCommand::ScheduleInterval {
id: "heartbeat".to_string(),
every_ms: 1000,
dispatch: RuntimeEvent::with_target("grid", "tick", json!({"source": "timer"}),),
},
RuntimeCommand::Cancel {
id: "retry-once".to_string(),
},
]
);
assert!(ctx.take_render_after_event());
}
#[test]
fn context_can_queue_chart_stream_message_without_root_render() {
let mut ctx = Context::new("root");
ctx.chart_series_append("traffic", "requests", ChartPoint { x: 1.0, y: 2.5 });
assert!(!ctx.take_render_after_event());
assert_eq!(
ctx.drain_pushes(),
vec![ServerMessage::ChartSeriesAppend {
chart: "traffic".to_string(),
series: "requests".to_string(),
point: ChartPoint { x: 1.0, y: 2.5 },
}]
);
assert!(ctx.take_render_after_event());
}
#[test]
fn context_can_queue_chart_series_replace_and_reset_without_root_render() {
let mut ctx = Context::new("root");
ctx.chart_series_replace(
"traffic",
"requests",
vec![ChartPoint { x: 1.0, y: 2.5 }, ChartPoint { x: 2.0, y: 3.0 }],
);
ctx.chart_reset("traffic");
assert!(!ctx.take_render_after_event());
assert_eq!(
ctx.drain_pushes(),
vec![
ServerMessage::ChartSeriesReplace {
chart: "traffic".to_string(),
series: "requests".to_string(),
points: vec![ChartPoint { x: 1.0, y: 2.5 }, ChartPoint { x: 2.0, y: 3.0 }],
},
ServerMessage::ChartReset {
chart: "traffic".to_string(),
},
]
);
assert!(ctx.take_render_after_event());
}
#[test]
fn context_can_queue_chart_append_many_without_root_render() {
let mut ctx = Context::new("root");
ctx.chart_series_append_many(
"traffic",
"requests",
vec![ChartPoint { x: 3.0, y: 4.0 }, ChartPoint { x: 4.0, y: 5.0 }],
);
assert!(!ctx.take_render_after_event());
assert_eq!(
ctx.drain_pushes(),
vec![ServerMessage::ChartSeriesAppendMany {
chart: "traffic".to_string(),
series: "requests".to_string(),
points: vec![ChartPoint { x: 3.0, y: 4.0 }, ChartPoint { x: 4.0, y: 5.0 }],
}]
);
assert!(ctx.take_render_after_event());
}
#[test]
fn context_can_queue_chart_annotation_messages_without_root_render() {
let mut ctx = Context::new("root");
ctx.chart_annotation_upsert(
"traffic",
ChartAnnotation {
id: "release-1".to_string(),
x: 18.0,
label: "Deploy".to_string(),
},
);
ctx.chart_annotation_delete("traffic", "release-1");
assert!(!ctx.take_render_after_event());
assert_eq!(
ctx.drain_pushes(),
vec![
ServerMessage::ChartAnnotationUpsert {
chart: "traffic".to_string(),
annotation: ChartAnnotation {
id: "release-1".to_string(),
x: 18.0,
label: "Deploy".to_string(),
},
},
ServerMessage::ChartAnnotationDelete {
chart: "traffic".to_string(),
id: "release-1".to_string(),
},
]
);
assert!(ctx.take_render_after_event());
}
#[test]
fn context_can_queue_toast_and_inbox_messages_without_root_render() {
let mut ctx = Context::new("root");
ctx.toast_push(Toast {
id: "toast-1".to_string(),
level: ToastLevel::Success,
title: Some("Saved".to_string()),
message: "Profile updated".to_string(),
ttl_ms: Some(2000),
});
ctx.toast_dismiss("toast-1");
ctx.inbox_upsert(InboxItem {
id: "msg-1".to_string(),
title: "Welcome".to_string(),
body: "Thanks for joining".to_string(),
read: false,
inserted_at: None,
});
ctx.inbox_delete("msg-1");
assert!(!ctx.take_render_after_event());
assert_eq!(
ctx.drain_pushes(),
vec![
ServerMessage::ToastPush {
toast: Toast {
id: "toast-1".to_string(),
level: ToastLevel::Success,
title: Some("Saved".to_string()),
message: "Profile updated".to_string(),
ttl_ms: Some(2000),
},
},
ServerMessage::ToastDismiss {
id: "toast-1".to_string(),
},
ServerMessage::InboxUpsert {
item: InboxItem {
id: "msg-1".to_string(),
title: "Welcome".to_string(),
body: "Thanks for joining".to_string(),
read: false,
inserted_at: None,
},
},
ServerMessage::InboxDelete {
id: "msg-1".to_string(),
},
]
);
assert!(ctx.take_render_after_event());
}
#[test]
fn context_can_queue_interop_and_webrtc_messages_without_root_render() {
let mut ctx = Context::new("root");
ctx.interop_dispatch_to(
Some("peer-a".to_string()),
"map:pan_to",
json!({"lat": 42.0, "lng": -71.0}),
);
ctx.webrtc_signal(json!({"kind": "offer", "sdp": "v=0..."}));
assert!(!ctx.take_render_after_event());
assert_eq!(
ctx.drain_pushes(),
vec![
ServerMessage::InteropDispatch {
dispatch: JsInteropDispatch {
target: Some("peer-a".to_string()),
event: "map:pan_to".to_string(),
detail: json!({"lat": 42.0, "lng": -71.0}),
bubbles: true,
},
},
ServerMessage::InteropDispatch {
dispatch: JsInteropDispatch {
target: None,
event: "shelly:webrtc-signal".to_string(),
detail: json!({"kind": "offer", "sdp": "v=0..."}),
bubbles: true,
},
},
]
);
assert!(ctx.take_render_after_event());
}
#[test]
fn context_can_queue_grid_replace_without_root_render() {
let mut ctx = Context::new("root");
ctx.grid_replace(
"enterprise-grid",
GridState {
columns: vec![GridColumn {
id: "name".to_string(),
label: "Name".to_string(),
width_px: Some(220),
min_width_px: Some(100),
pinned: GridPinned::Left,
sortable: true,
resizable: true,
editable: false,
}],
rows: vec![GridRow {
id: "row-1".to_string(),
cells: std::iter::once(("name".to_string(), json!("Ada"))).collect(),
group: Some("Engineering".to_string()),
}],
total_rows: 2000,
offset: 0,
limit: 200,
views: vec![GridSavedView {
id: "eng".to_string(),
label: "Engineering".to_string(),
}],
active_view: Some("eng".to_string()),
group_by: Some("department".to_string()),
query: Some("ada".to_string()),
sort: Some(GridSort {
column: "name".to_string(),
direction: GridSortDirection::Asc,
}),
},
);
assert!(!ctx.take_render_after_event());
assert_eq!(
ctx.drain_pushes(),
vec![ServerMessage::GridReplace {
grid: "enterprise-grid".to_string(),
state: GridState {
columns: vec![GridColumn {
id: "name".to_string(),
label: "Name".to_string(),
width_px: Some(220),
min_width_px: Some(100),
pinned: GridPinned::Left,
sortable: true,
resizable: true,
editable: false,
}],
rows: vec![GridRow {
id: "row-1".to_string(),
cells: std::iter::once(("name".to_string(), json!("Ada"))).collect(),
group: Some("Engineering".to_string()),
}],
total_rows: 2000,
offset: 0,
limit: 200,
views: vec![GridSavedView {
id: "eng".to_string(),
label: "Engineering".to_string(),
}],
active_view: Some("eng".to_string()),
group_by: Some("department".to_string()),
query: Some("ada".to_string()),
sort: Some(GridSort {
column: "name".to_string(),
direction: GridSortDirection::Asc,
}),
},
}]
);
assert!(ctx.take_render_after_event());
}
#[test]
fn context_can_queue_grid_window_replace_without_root_render() {
let mut ctx = Context::new("root");
ctx.grid_rows_replace(
"enterprise-grid",
400,
1000,
vec![GridRow {
id: "acct-401".to_string(),
cells: serde_json::Map::from_iter([
("name".to_string(), json!("Acme North")),
("arr".to_string(), json!(166000)),
]),
group: Some("Enterprise".to_string()),
}],
);
assert!(!ctx.take_render_after_event());
assert_eq!(
ctx.drain_pushes(),
vec![ServerMessage::GridRowsReplace {
grid: "enterprise-grid".to_string(),
window: GridRowsWindow {
offset: 400,
total_rows: 1000,
rows: vec![GridRow {
id: "acct-401".to_string(),
cells: serde_json::Map::from_iter([
("name".to_string(), json!("Acme North")),
("arr".to_string(), json!(166000)),
]),
group: Some("Enterprise".to_string()),
}],
},
}]
);
assert!(ctx.take_render_after_event());
}
}