Panics · P33 Devtools: handlers in new; stale collection moves, environments and diagram tables change nothing

This commit is contained in:
ZacharyZhang-NY committed 2026-10-07 05:15:49 -04:00
1 parent ffe4f38103
commit 05e846b62a
13 files changed
+220 -215

No files matched your search

+15 -7
View File
@@ -156,8 +156,7 @@ pub fn requests(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
.flex_col() .flex_col()
.gap_4() .gap_4()
.child( .child(
EnvironmentSelector::new("devtools-environments", environments(), environment) EnvironmentSelector::new("devtools-environments", environments(), environment, move |key, _, cx| change(&chose, cx, |desk| desk.environment = key.clone())),
.on_change(move |key, _, cx| change(&chose, cx, |desk| desk.environment = key.clone())),
) )
.child( .child(
div() div()
@@ -178,7 +177,11 @@ pub fn requests(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
}) })
}) })
.on_move(move |key, target, at, _, cx| { .on_move(move |key, target, at, _, cx| {
change(&dragged, cx, |desk| desk.collection = moved(&desk.collection, key, target, at)) change(&dragged, cx, |desk| {
if let Some(collection) = moved(&desk.collection, key, target, at) {
desk.collection = collection;
}
})
}), }),
), ),
), ),
@@ -255,8 +258,7 @@ pub fn socket(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
) )
.child( .child(
div().w(px(620.)).child( div().w(px(620.)).child(
WebSocketConsole::new("devtools-socket", "wss://stream.example.com/orders", state, messages, &draft, clock) WebSocketConsole::new("devtools-socket", "wss://stream.example.com/orders", state, messages, &draft, clock, move |text, _, cx| {
.on_send(move |text, _, cx| {
sent.update(cx, |line, cx| { sent.update(cx, |line, cx| {
let at = line.now; let at = line.now;
line.messages.push(SocketMessage { direction: Direction::Sent, text, at }); line.messages.push(SocketMessage { direction: Direction::Sent, text, at });
@@ -333,8 +335,14 @@ pub fn graphql(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
fields: vec![("OPEN".into(), "".into()), ("SHIPPED".into(), "".into())], fields: vec![("OPEN".into(), "".into()), ("SHIPPED".into(), "".into())],
}, },
]; ];
let mut explorer = GraphQLExplorer::new("devtools-graphql", types, &query, &variables, &search) let mut explorer = GraphQLExplorer::new(
.on_run(move |_, _, _, cx| change(&ran, cx, |graph| graph.answer = Some(ANSWER.into()))); "devtools-graphql",
types,
&query,
&variables,
&search,
move |_, _, _, cx| change(&ran, cx, |graph| graph.answer = Some(ANSWER.into())),
);
if let Some(answer) = answer { if let Some(answer) = answer {
explorer = explorer.answer(answer); explorer = explorer.answer(answer);
} }
+2 -5
View File
@@ -270,15 +270,12 @@ pub fn structure(window: &mut Window, cx: &mut App) -> impl IntoElement + use<>
.flex_wrap() .flex_wrap()
.gap_6() .gap_6()
.child( .child(
div().w(px(660.)).child(TableStructureEditor::new("devtools-structure", fields) div().w(px(660.)).child(TableStructureEditor::new("devtools-structure", fields, move |next, _, cx| change(&edited, cx, |build| build.fields = next)),
.on_change(move |next, _, cx| change(&edited, cx, |build| build.fields = next)),
), ),
) )
.child( .child(
div().w(px(300.)).child( div().w(px(300.)).child(
IndexManager::new("devtools-indexes", indexes, names) IndexManager::new("devtools-indexes", indexes, names, move |index, _, cx| change(&created, cx, |build| build.indexes.push(index)), move |name, _, cx| change(&dropped, cx, |build| build.indexes.retain(|index| index.name != *name))),
.on_drop(move |name, _, cx| change(&dropped, cx, |build| build.indexes.retain(|index| index.name != *name)))
.on_create(move |index, _, cx| change(&created, cx, |build| build.indexes.push(index))),
), ),
), ),
) )
+1 -2
View File
@@ -35,8 +35,7 @@ pub fn text(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
.child(div().w(px(380.)).child(Encoder::new("devtools-encoder", "Hello, 世界"))) .child(div().w(px(380.)).child(Encoder::new("devtools-encoder", "Hello, 世界")))
.child( .child(
div().w(px(360.)).child( div().w(px(360.)).child(
ColorContrastChecker::new("devtools-contrast", fore, back) ColorContrastChecker::new("devtools-contrast", fore, back, move |fore, back, _, cx| change(&colors, cx, |colors| *colors = (fore, back))),
.on_change(move |fore, back, _, cx| change(&colors, cx, |colors| *colors = (fore, back))),
), ),
), ),
) )
+23 -11
View File
@@ -85,7 +85,8 @@ fn placed(items: &mut Vec<Saved>, item: Saved, target: &str, at: DropAt) -> Resu
(DropAt::After, _) => items.insert(ix + 1, item), (DropAt::After, _) => items.insert(ix + 1, item),
(DropAt::Inside, Saved::Folder { items, .. }) => items.push(item), (DropAt::Inside, Saved::Folder { items, .. }) => items.push(item),
(DropAt::Inside, Saved::Request { key, .. }) => { (DropAt::Inside, Saved::Request { key, .. }) => {
panic!("collection: {key} is a request, and nothing goes inside it") log::error!("collection: {key} is a request, and nothing goes inside it");
return Err(item);
} }
} }
return Ok(()); return Ok(());
@@ -102,15 +103,19 @@ fn placed(items: &mut Vec<Saved>, item: Saved, target: &str, at: DropAt) -> Resu
Err(item) Err(item)
} }
/// `items` with `key` moved to `at` of `target`, as a drop in the tree asks. /// `items` with `key` moved to `at` of `target`, as a drop in the tree asks; none, logged, when either left the items.
pub fn moved(items: &[Saved], key: &str, target: &str, at: DropAt) -> Vec<Saved> { pub fn moved(items: &[Saved], key: &str, target: &str, at: DropAt) -> Option<Vec<Saved>> {
let mut items = items.to_vec(); let mut items = items.to_vec();
let item = taken(&mut items, key).unwrap_or_else(|| panic!("collection: no {key} to move")); let Some(item) = taken(&mut items, key) else {
log::error!("collection: no {key} to move");
return None;
};
if let Err(item) = placed(&mut items, item, target, at) { if let Err(item) = placed(&mut items, item, target, at) {
panic!("collection: no {target} to move {} beside", item.key()); log::error!("collection: no {target} to move {} beside", item.key());
return None;
} }
log::info!("collection: moved {key} {at:?} {target}"); log::info!("collection: moved {key} {at:?} {target}");
items Some(items)
} }
type OnKey = Rc<dyn Fn(&SharedString, &mut Window, &mut App)>; type OnKey = Rc<dyn Fn(&SharedString, &mut Window, &mut App)>;
@@ -236,25 +241,32 @@ mod tests {
Saved::request("z", "Z", Method::Post), Saved::request("z", "Z", Method::Post),
]; ];
assert_eq!( assert_eq!(
keys(&moved(&items, "x", "b", DropAt::Inside)), keys(&moved(&items, "x", "b", DropAt::Inside).expect("both are in the items")),
["a[y]", "b[x]", "z"] ["a[y]", "b[x]", "z"]
); );
assert_eq!( assert_eq!(
keys(&moved(&items, "z", "a", DropAt::Inside)), keys(&moved(&items, "z", "a", DropAt::Inside).expect("both are in the items")),
["a[x,y,z]", "b[]"], ["a[x,y,z]", "b[]"],
"a drop inside a folder lands last" "a drop inside a folder lands last"
); );
assert_eq!( assert_eq!(
keys(&moved(&items, "z", "x", DropAt::Before)), keys(&moved(&items, "z", "x", DropAt::Before).expect("both are in the items")),
["a[z,x,y]", "b[]"] ["a[z,x,y]", "b[]"]
); );
assert_eq!( assert_eq!(
keys(&moved(&items, "x", "z", DropAt::After)), keys(&moved(&items, "x", "z", DropAt::After).expect("both are in the items")),
["a[y]", "b[]", "z", "x"] ["a[y]", "b[]", "z", "x"]
); );
assert_eq!( assert_eq!(
keys(&moved(&items, "b", "a", DropAt::Before)), keys(&moved(&items, "b", "a", DropAt::Before).expect("both are in the items")),
["b[]", "a[x,y]", "z"] ["b[]", "a[x,y]", "z"]
); );
assert_eq!(moved(&items, "gone", "a", DropAt::Before), None);
assert_eq!(moved(&items, "x", "gone", DropAt::Before), None);
assert_eq!(
moved(&items, "x", "z", DropAt::Inside),
None,
"a request holds nothing"
);
} }
} }
+4 -14
View File
@@ -64,7 +64,7 @@ pub struct ColorContrastChecker {
id: ElementId, id: ElementId,
text: Hsla, text: Hsla,
background: Hsla, background: Hsla,
on_change: Option<OnColors>, on_change: OnColors,
} }
impl ColorContrastChecker { impl ColorContrastChecker {
@@ -72,23 +72,15 @@ impl ColorContrastChecker {
id: impl Into<ElementId>, id: impl Into<ElementId>,
text: impl Into<Hsla>, text: impl Into<Hsla>,
background: impl Into<Hsla>, background: impl Into<Hsla>,
on_change: impl Fn(Hsla, Hsla, &mut Window, &mut App) + 'static,
) -> Self { ) -> Self {
Self { Self {
id: id.into(), id: id.into(),
text: text.into(), text: text.into(),
background: background.into(), background: background.into(),
on_change: None, on_change: Rc::new(on_change),
} }
} }
/// Gets the text's and the background's colors after either changes.
pub fn on_change(
mut self,
handler: impl Fn(Hsla, Hsla, &mut Window, &mut App) + 'static,
) -> Self {
self.on_change = Some(Rc::new(handler));
self
}
} }
impl RenderOnce for ColorContrastChecker { impl RenderOnce for ColorContrastChecker {
@@ -99,9 +91,7 @@ impl RenderOnce for ColorContrastChecker {
..background ..background
}; };
let ratio = contrast(over(text, solid), solid); let ratio = contrast(over(text, solid), solid);
let on_change = self let on_change = self.on_change;
.on_change
.unwrap_or_else(|| panic!("color contrast checker {:?} has no on_change", self.id));
let theme = cx.theme(); let theme = cx.theme();
let well = |name: &'static str, color: Hsla, set: OnColor| { let well = |name: &'static str, color: Hsla, set: OnColor| {
div() div()
+69 -73
View File
@@ -30,7 +30,7 @@ pub struct EnvironmentSelector {
id: ElementId, id: ElementId,
environments: Vec<Environment>, environments: Vec<Environment>,
selected: SharedString, selected: SharedString,
on_change: Option<OnKey>, on_change: OnKey,
} }
impl EnvironmentSelector { impl EnvironmentSelector {
@@ -38,22 +38,15 @@ impl EnvironmentSelector {
id: impl Into<ElementId>, id: impl Into<ElementId>,
environments: impl IntoIterator<Item = Environment>, environments: impl IntoIterator<Item = Environment>,
selected: impl Into<SharedString>, selected: impl Into<SharedString>,
on_change: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
) -> Self { ) -> Self {
Self { Self {
id: id.into(), id: id.into(),
environments: environments.into_iter().collect(), environments: environments.into_iter().collect(),
selected: selected.into(), selected: selected.into(),
on_change: None, on_change: Rc::new(on_change),
} }
} }
pub fn on_change(
mut self,
handler: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
) -> Self {
self.on_change = Some(Rc::new(handler));
self
}
} }
/// What a row shows for a value: dots for a secret until it is opened. /// What a row shows for a value: dots for a secret until it is opened.
@@ -84,71 +77,71 @@ impl RenderOnce for EnvironmentSelector {
.environments .environments
.iter() .iter()
.find(|environment| environment.key == self.selected) .find(|environment| environment.key == self.selected)
.unwrap_or_else(|| { .cloned();
panic!( if chosen.is_none() {
"environment selector {id:?}: no environment {}", log::error!(
self.selected "environment selector {id:?}: no environment {}; none chosen",
) self.selected
}) );
.clone(); }
let shown = window.use_keyed_state((id.clone(), "shown"), cx, |_, _| Vec::<Secret>::new()); let shown = window.use_keyed_state((id.clone(), "shown"), cx, |_, _| Vec::<Secret>::new());
let theme = cx.theme(); let theme = cx.theme();
let on_change = self let on_change = self.on_change;
.on_change let rows = chosen.iter().flat_map(|chosen| {
.unwrap_or_else(|| panic!("environment selector {id:?} has no on_change")); chosen.variables.iter().map(|(name, value)| {
let rows = chosen.variables.iter().map(|(name, value)| { let secret = chosen.secrets.contains(name);
let secret = chosen.secrets.contains(name); let key: Secret = (chosen.key.clone(), name.clone());
let key: Secret = (chosen.key.clone(), name.clone()); let open = shown.read(cx).contains(&key);
let open = shown.read(cx).contains(&key); let flip = shown.clone();
let flip = shown.clone(); let seen = format!(
let seen = format!( "environment-{}-{name}-{}",
"environment-{}-{name}-{}", chosen.key,
chosen.key, if secret && !open { "masked" } else { "shown" }
if secret && !open { "masked" } else { "shown" } );
); div()
div() .flex()
.flex() .items_center()
.items_center() .gap_2()
.gap_2() .py_1()
.py_1() .child(
.child( div()
div() .flex_none()
.flex_none() .w(theme.label_width())
.w(theme.label_width()) .font_family(theme.mono_family.clone())
.font_family(theme.mono_family.clone()) .text_size(theme.text_size(TextSize::Sm))
.text_size(theme.text_size(TextSize::Sm)) .child(Ellipsis::new(format!("{{{{{name}}}}}"))),
.child(Ellipsis::new(format!("{{{{{name}}}}}"))),
)
.child(
div()
.flex_1()
.min_w_0()
.text_size(theme.text_size(TextSize::Sm))
.text_color(theme.colors.fg_muted)
.debug_selector(move || seen)
.child(Ellipsis::new(shown_value(value, secret, open))),
)
.children(secret.then(|| {
IconButton::new(
(id.clone(), format!("reveal-{name}")),
if open {
IconName::EyeOff
} else {
IconName::Eye
},
) )
.tooltip(if open { .child(
"Hide the value" div()
} else { .flex_1()
"Show the value" .min_w_0()
}) .text_size(theme.text_size(TextSize::Sm))
.on_click(move |_, _, cx| { .text_color(theme.colors.fg_muted)
flip.update(cx, |shown, cx| { .debug_selector(move || seen)
flipped(shown, &key); .child(Ellipsis::new(shown_value(value, secret, open))),
cx.notify(); )
.children(secret.then(|| {
IconButton::new(
(id.clone(), format!("reveal-{name}")),
if open {
IconName::EyeOff
} else {
IconName::Eye
},
)
.tooltip(if open {
"Hide the value"
} else {
"Show the value"
}) })
}) .on_click(move |_, _, cx| {
})) flip.update(cx, |shown, cx| {
flipped(shown, &key);
cx.notify();
})
})
}))
})
}); });
div() div()
.flex() .flex()
@@ -161,7 +154,7 @@ impl RenderOnce for EnvironmentSelector {
Choice::new(environment.key.clone(), environment.name.clone()) Choice::new(environment.key.clone(), environment.name.clone())
}), }),
) )
.selected(chosen.key.clone()) .selected(self.selected.clone())
.on_change(move |key, window, cx| { .on_change(move |key, window, cx| {
log::info!("environment selector: {key}"); log::info!("environment selector: {key}");
on_change(key, window, cx); on_change(key, window, cx);
@@ -172,7 +165,10 @@ impl RenderOnce for EnvironmentSelector {
.text_size(theme.text_size(TextSize::Xs)) .text_size(theme.text_size(TextSize::Xs))
.text_color(theme.colors.fg_subtle) .text_color(theme.colors.fg_subtle)
.font_weight(FontWeight::MEDIUM) .font_weight(FontWeight::MEDIUM)
.child(format!("{} variables", chosen.variables.len())), .child(match &chosen {
Some(chosen) => format!("{} variables", chosen.variables.len()),
None => "No environment chosen".into(),
}),
) )
.child(div().flex().flex_col().children(rows)) .child(div().flex().flex_col().children(rows))
} }
+4 -13
View File
@@ -57,7 +57,7 @@ pub struct GraphQLExplorer {
answer: Option<SharedString>, answer: Option<SharedString>,
search: Entity<TextInput>, search: Entity<TextInput>,
running: bool, running: bool,
on_run: Option<OnRun>, on_run: OnRun,
on_pick: Option<OnKey>, on_pick: Option<OnKey>,
} }
@@ -69,6 +69,7 @@ impl GraphQLExplorer {
query: &Entity<CodeEditor>, query: &Entity<CodeEditor>,
variables: &Entity<CodeEditor>, variables: &Entity<CodeEditor>,
search: &Entity<TextInput>, search: &Entity<TextInput>,
on_run: impl Fn(String, String, &mut Window, &mut App) + 'static,
) -> Self { ) -> Self {
Self { Self {
id: id.into(), id: id.into(),
@@ -78,7 +79,7 @@ impl GraphQLExplorer {
answer: None, answer: None,
search: search.clone(), search: search.clone(),
running: false, running: false,
on_run: None, on_run: Rc::new(on_run),
on_pick: None, on_pick: None,
} }
} }
@@ -94,14 +95,6 @@ impl GraphQLExplorer {
self self
} }
pub fn on_run(
mut self,
handler: impl Fn(String, String, &mut Window, &mut App) + 'static,
) -> Self {
self.on_run = Some(Rc::new(handler));
self
}
pub fn on_pick( pub fn on_pick(
mut self, mut self,
handler: impl Fn(&SharedString, &mut Window, &mut App) + 'static, handler: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
@@ -149,9 +142,7 @@ impl RenderOnce for GraphQLExplorer {
.child(text) .child(text)
}; };
let tall = theme.list_max_height(); let tall = theme.list_max_height();
let on_run = self let on_run = self.on_run;
.on_run
.unwrap_or_else(|| panic!("graphql explorer {id:?} has no on_run"));
let (query, variables) = (self.query.clone(), self.variables.clone()); let (query, variables) = (self.query.clone(), self.variables.clone());
let typed = variables.read(cx).text().to_string(); let typed = variables.read(cx).text().to_string();
let unread = (!typed.trim().is_empty()) let unread = (!typed.trim().is_empty())
+11 -11
View File
@@ -245,19 +245,19 @@ pub(crate) fn diagram(
) -> (Vec<Node>, Vec<Edge>) { ) -> (Vec<Node>, Vec<Edge>) {
let nodes = tables let nodes = tables
.iter() .iter()
.map(|table| { .filter_map(|table| {
let at = places let Some((_, at)) = places.iter().find(|(name, _)| *name == table.name) else {
.iter() log::error!("ER diagram: no place for {}; left out", table.name);
.find(|(name, _)| *name == table.name) return None;
.unwrap_or_else(|| panic!("ER diagram: no place for {}", table.name)) };
.1; let at = *at;
table.fields.iter().fold( Some(table.fields.iter().fold(
Node::new(table.name.clone(), table.name.clone(), at), Node::new(table.name.clone(), table.name.clone(), at),
|node, field| { |node, field| {
node.input(Port::new(field.name.clone(), field.name.clone(), "field")) node.input(Port::new(field.name.clone(), field.name.clone(), "field"))
.output(Port::new(field.name.clone(), field.typed(), "field")) .output(Port::new(field.name.clone(), field.typed(), "field"))
}, },
) ))
}) })
.collect(); .collect();
let edges = tables let edges = tables
@@ -337,8 +337,8 @@ mod tests {
} }
#[test] #[test]
#[should_panic(expected = "no place for orders")] fn a_table_without_a_place_is_left_out() {
fn a_table_without_a_place_fails_loud() { let (nodes, edges) = diagram(&[DbTable::new("orders", [])], &[]);
diagram(&[DbTable::new("orders", [])], &[]); assert!(nodes.is_empty() && edges.is_empty());
} }
} }
+4 -14
View File
@@ -53,7 +53,7 @@ pub struct WebSocketConsole {
messages: Vec<SocketMessage>, messages: Vec<SocketMessage>,
draft: Entity<TextInput>, draft: Entity<TextInput>,
now: Timestamp, now: Timestamp,
on_send: Option<OnText>, on_send: OnText,
on_toggle: Option<Run>, on_toggle: Option<Run>,
} }
@@ -66,6 +66,7 @@ impl WebSocketConsole {
messages: impl IntoIterator<Item = SocketMessage>, messages: impl IntoIterator<Item = SocketMessage>,
draft: &Entity<TextInput>, draft: &Entity<TextInput>,
now: Timestamp, now: Timestamp,
on_send: impl Fn(SharedString, &mut Window, &mut App) + 'static,
) -> Self { ) -> Self {
Self { Self {
id: id.into(), id: id.into(),
@@ -74,20 +75,11 @@ impl WebSocketConsole {
messages: messages.into_iter().collect(), messages: messages.into_iter().collect(),
draft: draft.clone(), draft: draft.clone(),
now, now,
on_send: None, on_send: Rc::new(on_send),
on_toggle: None, on_toggle: None,
} }
} }
/// Gets the text to send; the field empties after.
pub fn on_send(
mut self,
handler: impl Fn(SharedString, &mut Window, &mut App) + 'static,
) -> Self {
self.on_send = Some(Rc::new(handler));
self
}
/// Asked to connect while closed, or to disconnect while open. /// Asked to connect while closed, or to disconnect while open.
pub fn on_toggle(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self { pub fn on_toggle(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
self.on_toggle = Some(Rc::new(handler)); self.on_toggle = Some(Rc::new(handler));
@@ -159,9 +151,7 @@ impl RenderOnce for WebSocketConsole {
}; };
let open = self.state == SocketState::Open; let open = self.state == SocketState::Open;
let (messages, now) = (Rc::new(self.messages), self.now); let (messages, now) = (Rc::new(self.messages), self.now);
let on_send = self let on_send = self.on_send;
.on_send
.unwrap_or_else(|| panic!("websocket console {:?} has no on_send", self.id));
let draft = self.draft.clone(); let draft = self.draft.clone();
let ready = open && !draft.read(cx).text().trim().is_empty(); let ready = open && !draft.read(cx).text().trim().is_empty();
let send = Rc::new(move |window: &mut Window, cx: &mut App| { let send = Rc::new(move |window: &mut Window, cx: &mut App| {
+16 -33
View File
@@ -100,32 +100,26 @@ struct Room(f32);
pub struct TableStructureEditor { pub struct TableStructureEditor {
id: ElementId, id: ElementId,
fields: Vec<Field>, fields: Vec<Field>,
on_change: Option<OnEdit<Vec<Field>>>, on_change: OnEdit<Vec<Field>>,
} }
impl TableStructureEditor { impl TableStructureEditor {
pub fn new(id: impl Into<ElementId>, fields: impl IntoIterator<Item = Field>) -> Self { pub fn new(
id: impl Into<ElementId>,
fields: impl IntoIterator<Item = Field>,
on_change: impl Fn(Vec<Field>, &mut Window, &mut App) + 'static,
) -> Self {
Self { Self {
id: id.into(), id: id.into(),
fields: fields.into_iter().collect(), fields: fields.into_iter().collect(),
on_change: None, on_change: Rc::new(on_change),
} }
} }
pub fn on_change(
mut self,
handler: impl Fn(Vec<Field>, &mut Window, &mut App) + 'static,
) -> Self {
self.on_change = Some(Rc::new(handler));
self
}
} }
impl RenderOnce for TableStructureEditor { impl RenderOnce for TableStructureEditor {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let on_change = self let on_change = self.on_change;
.on_change
.unwrap_or_else(|| panic!("table structure editor {:?} has no on_change", self.id));
let (id, fields, on_change) = (self.id, self.fields, &on_change); let (id, fields, on_change) = (self.id, self.fields, &on_change);
let room = window.use_keyed_state((id.clone(), "room"), cx, |_, _| Room::default()); let room = window.use_keyed_state((id.clone(), "room"), cx, |_, _| Room::default());
let rem = window.rem_size(); let rem = window.rem_size();
@@ -303,8 +297,8 @@ pub struct IndexManager {
id: ElementId, id: ElementId,
indexes: Vec<Index>, indexes: Vec<Index>,
fields: Vec<SharedString>, fields: Vec<SharedString>,
on_drop: Option<OnName>, on_drop: OnName,
on_create: Option<OnIndex>, on_create: OnIndex,
} }
impl IndexManager { impl IndexManager {
@@ -313,34 +307,23 @@ impl IndexManager {
id: impl Into<ElementId>, id: impl Into<ElementId>,
indexes: impl IntoIterator<Item = Index>, indexes: impl IntoIterator<Item = Index>,
fields: impl IntoIterator<Item = impl Into<SharedString>>, fields: impl IntoIterator<Item = impl Into<SharedString>>,
on_create: impl Fn(Index, &mut Window, &mut App) + 'static,
on_drop: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
) -> Self { ) -> Self {
Self { Self {
id: id.into(), id: id.into(),
indexes: indexes.into_iter().collect(), indexes: indexes.into_iter().collect(),
fields: fields.into_iter().map(Into::into).collect(), fields: fields.into_iter().map(Into::into).collect(),
on_drop: None, on_drop: Rc::new(on_drop),
on_create: None, on_create: Rc::new(on_create),
} }
} }
pub fn on_drop(
mut self,
handler: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
) -> Self {
self.on_drop = Some(Rc::new(handler));
self
}
pub fn on_create(mut self, handler: impl Fn(Index, &mut Window, &mut App) + 'static) -> Self {
self.on_create = Some(Rc::new(handler));
self
}
} }
impl RenderOnce for IndexManager { impl RenderOnce for IndexManager {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let on_create = self.on_create.expect("an index manager needs on_create"); let on_create = self.on_create;
let on_drop = self.on_drop.expect("an index manager needs on_drop"); let on_drop = self.on_drop;
let id = self.id; let id = self.id;
let draft = window.use_keyed_state((id.clone(), "draft"), cx, |window, cx| Draft { let draft = window.use_keyed_state((id.clone(), "draft"), cx, |window, cx| Draft {
name: cx.new(|cx| TextInput::new(window, cx).placeholder("Index name")), name: cx.new(|cx| TextInput::new(window, cx).placeholder("Index name")),
+54 -17
View File
@@ -100,10 +100,17 @@ fn console(bench: &Bench, window: &mut Window, cx: &mut App, owner: Entity<Bench
_ => SocketState::Closed, _ => SocketState::Closed,
}; };
let toggled = owner.clone(); let toggled = owner.clone();
WebSocketConsole::new("socket", "wss://example.com", state, [], &draft, now) WebSocketConsole::new(
.on_send(move |text, _, cx| say(&owner, format!("send {text}"), cx)) "socket",
.on_toggle(move |_, cx| say(&toggled, "toggle".into(), cx)) "wss://example.com",
.into_any_element() state,
[],
&draft,
now,
move |text, _, cx| say(&owner, format!("send {text}"), cx),
)
.on_toggle(move |_, cx| say(&toggled, "toggle".into(), cx))
.into_any_element()
} }
/// Stops: Disconnect, the message field, then Send once it holds words. /// Stops: Disconnect, the message field, then Send once it holds words.
@@ -179,9 +186,15 @@ fn graph(bench: &Bench, window: &mut Window, cx: &mut App, owner: Entity<Bench>)
return div().into_any_element(); return div().into_any_element();
}; };
let search = window.use_keyed_state("search", cx, TextInput::new); let search = window.use_keyed_state("search", cx, TextInput::new);
GraphQLExplorer::new("graph", [], &query, &variables, &search) GraphQLExplorer::new(
.on_run(move |query, variables, _, cx| say(&owner, format!("{query} {variables}"), cx)) "graph",
.into_any_element() [],
&query,
&variables,
&search,
move |query, variables, _, cx| say(&owner, format!("{query} {variables}"), cx),
)
.into_any_element()
} }
/// Stops: the schema, the query, the variables, then Run while they read. /// Stops: the schema, the query, the variables, then Run while they read.
@@ -216,10 +229,10 @@ fn environments(bench: &Bench, _: &mut Window, _: &mut App, _: Entity<Bench>) ->
variables: vec![("token".into(), token.to_string().into())], variables: vec![("token".into(), token.to_string().into())],
secrets: vec!["token".into()], secrets: vec!["token".into()],
}; };
let selected = if bench.choice == 0 { let selected = match bench.choice {
"staging" 0 => "staging",
} else { 1 => "production",
"production" _ => "gone",
}; };
EnvironmentSelector::new( EnvironmentSelector::new(
"environments", "environments",
@@ -228,8 +241,8 @@ fn environments(bench: &Bench, _: &mut Window, _: &mut App, _: Entity<Bench>) ->
env("production", "sk_live_9999"), env("production", "sk_live_9999"),
], ],
selected, selected,
|_, _, _| {},
) )
.on_change(|_, _, _| {})
.into_any_element() .into_any_element()
} }
@@ -265,10 +278,16 @@ fn schema(_: &Bench, window: &mut Window, cx: &mut App, owner: Entity<Bench>) ->
kind: TypeKind::Object, kind: TypeKind::Object,
fields: vec![("id".into(), "ID!".into())], fields: vec![("id".into(), "ID!".into())],
}; };
GraphQLExplorer::new("graph", [order], &query, &variables, &search) GraphQLExplorer::new(
.on_run(|_, _, _, _| {}) "graph",
.on_pick(move |path, _, cx| say(&owner, path.to_string(), cx)) [order],
.into_any_element() &query,
&variables,
&search,
|_, _, _, _| {},
)
.on_pick(move |path, _, cx| say(&owner, path.to_string(), cx))
.into_any_element()
} }
/// Stops: the schema first, its cursor on Order. /// Stops: the schema first, its cursor on Order.
@@ -297,8 +316,8 @@ fn log(_: &Bench, window: &mut Window, cx: &mut App, owner: Entity<Bench>) -> An
messages, messages,
&draft, &draft,
now, now,
move |text, _, cx| say(&owner, format!("send {text}"), cx),
) )
.on_send(move |text, _, cx| say(&owner, format!("send {text}"), cx))
.into_any_element() .into_any_element()
} }
@@ -314,3 +333,21 @@ fn the_log_holds_at_its_newest_message(cx: &mut TestAppContext) {
"the newest message lies inside the log: {newest:?} in {log:?}" "the newest message lies inside the log: {newest:?} in {log:?}"
); );
} }
#[gpui::test]
fn an_environment_that_left_shows_none_chosen(cx: &mut TestAppContext) {
let (host, cx) = bench(environments, cx);
host.update(cx, |bench, cx| {
bench.choice = 2;
cx.notify();
});
settle(cx);
assert!(
cx.debug_bounds("environment-staging-token-masked")
.is_none()
);
assert!(
cx.debug_bounds("environment-production-token-masked")
.is_none()
);
}
+15 -14
View File
@@ -107,15 +107,18 @@ fn enter_opens_a_connection_and_a_rows_button_leaves_the_row_alone(cx: &mut Test
} }
fn structure(_: &Bench, _: &mut Window, _: &mut App, owner: Entity<Bench>) -> AnyElement { fn structure(_: &Bench, _: &mut Window, _: &mut App, owner: Entity<Bench>) -> AnyElement {
TableStructureEditor::new("structure", [Field::new("id", "int").primary()]) TableStructureEditor::new(
.on_change(move |fields, _, cx| { "structure",
[Field::new("id", "int").primary()],
move |fields, _, cx| {
let names: Vec<String> = fields let names: Vec<String> = fields
.iter() .iter()
.map(|field| format!("{}:{}", field.name, field.ty)) .map(|field| format!("{}:{}", field.name, field.ty))
.collect(); .collect();
say(&owner, names.join(" "), cx) say(&owner, names.join(" "), cx)
}) },
.into_any_element() )
.into_any_element()
} }
/// A row's stops: name, type, empty, key, default and remove; Add field comes after. /// A row's stops: name, type, empty, key, default and remove; Add field comes after.
@@ -204,16 +207,14 @@ fn another_engine_brings_its_port(cx: &mut TestAppContext) {
fn narrow(_: &Bench, _: &mut Window, _: &mut App, _: Entity<Bench>) -> AnyElement { fn narrow(_: &Bench, _: &mut Window, _: &mut App, _: Entity<Bench>) -> AnyElement {
div() div()
.w(px(280.0)) .w(px(280.0))
.child( .child(TableStructureEditor::new(
TableStructureEditor::new( "structure",
"structure", [
[ Field::new("user_id", "timestamptz"),
Field::new("user_id", "timestamptz"), Field::new("total", "numeric"),
Field::new("total", "numeric"), ],
], |_, _, _| {},
) ))
.on_change(|_, _, _| {}),
)
.into_any_element() .into_any_element()
} }
+2 -1
View File
@@ -47,7 +47,7 @@ Each item reads every site in its folders, fixes what the rules name, updates ca
- [x] P30 Calendar — `src/calendar` - [x] P30 Calendar — `src/calendar`
- [x] P31 Project — `src/project` - [x] P31 Project — `src/project`
- [x] P32 Canvas & Design — `src/canvas` - [x] P32 Canvas & Design — `src/canvas`
- [ ] P33 DB & Dev Tools — `src/devtools` - [x] P33 DB & Dev Tools — `src/devtools`
- [ ] P34 Dashboard — `src/dashboard` - [ ] P34 Dashboard — `src/dashboard`
- [ ] P35 Settings — `src/settings` - [ ] P35 Settings — `src/settings`
- [ ] P36 Account — `src/account` - [ ] P36 Account — `src/account`
@@ -98,3 +98,4 @@ One line per item: what changed, what stayed and why, review rounds.
- P30: calendar views step only within jiff's range less a year at each end (`calendar::within`, `stepped`), so every grid around a day fits, and a step past logs and stays; recurrences end where jiff's range ends; the year view's marks stop at an event's last day; the event editor refuses moves, ends, all-day starts and switches to timed that leave jiff's range or a stale painted kind, each with a log; a backward timed draft's days are its first day. Kept: owner dates at jiff's far edges, one list's uniqueness, an event's own order, one organizer, working hours, slots, reminder offsets, constant patterns, picks that always name a value. Review: 2 rounds. - P30: calendar views step only within jiff's range less a year at each end (`calendar::within`, `stepped`), so every grid around a day fits, and a step past logs and stays; recurrences end where jiff's range ends; the year view's marks stop at an event's last day; the event editor refuses moves, ends, all-day starts and switches to timed that leave jiff's range or a stale painted kind, each with a log; a backward timed draft's days are its first day. Kept: owner dates at jiff's far edges, one list's uniqueness, an event's own order, one organizer, working hours, slots, reminder offsets, constant patterns, picks that always name a value. Review: 2 rounds.
- P31: a gone assignee logs and shows nobody; a milestone past its count fills, the text real; a roadmap past jiff's range ends inclusively on its last day; the database timeline leaves out and counts tasks due before they start and counts months in `i32`, and its calendar keeps to days `calendar::within` holds; the board's drag and drop take only columns, cards and places this frame holds. Kept: one list's uniqueness, an initiative's and a milestone's own order, shares, month counts, pomodoro lengths, issue keys, lookups over the components' own tables. Review: 3 rounds. - P31: a gone assignee logs and shows nobody; a milestone past its count fills, the text real; a roadmap past jiff's range ends inclusively on its last day; the database timeline leaves out and counts tasks due before they start and counts months in `i32`, and its calendar keeps to days `calendar::within` holds; the board's drag and drop take only columns, cards and places this frame holds. Kept: one list's uniqueness, an initiative's and a milestone's own order, shares, month counts, pomodoro lengths, issue keys, lookups over the components' own tables. Review: 3 rounds.
- P32: the eight design panels take their handlers in `new` (AGENTS.md says so); edges, links, wires, links in progress, resizes, restacks, topics and whiteboard edits that name a node, port, shape, layer or topic gone from this frame log and change nothing; a wire's release fits its source as it now stands; `Topic::adding`, `removing` and `renaming` return `Option`; a mind map's selection, a palette's tool and a history position no longer held mark none. Kept: one-value contracts (alignment counts, frames, zoom, polygon sides, brush hues and type weights among the theme's tables), lookups over the panels' own option tables, button-gated expects. Review: 2 rounds. - P32: the eight design panels take their handlers in `new` (AGENTS.md says so); edges, links, wires, links in progress, resizes, restacks, topics and whiteboard edits that name a node, port, shape, layer or topic gone from this frame log and change nothing; a wire's release fits its source as it now stands; `Topic::adding`, `removing` and `renaming` return `Option`; a mind map's selection, a palette's tool and a history position no longer held mark none. Kept: one-value contracts (alignment counts, frames, zoom, polygon sides, brush hues and type weights among the theme's tables), lookups over the panels' own option tables, button-gated expects. Review: 2 rounds.
- P33: six devtools take their handlers in `new` (contrast checker, environment selector, GraphQL explorer, WebSocket console, structure editor, index manager); `collection::moved` returns `Option` for a gone key or target or a drop into a request; a gone environment shows none chosen; an ER table without a place stays out. Kept: readings, costs, lookups over the components' own tables, button-gated expects, JSON that read writes back. Review: 1 round.