Skip to content

Commit b0dca52

Browse files
author
ada mandala
committed
refactor editable header + test
1 parent 7a11794 commit b0dca52

File tree

4 files changed

+162
-135
lines changed

4 files changed

+162
-135
lines changed

rust/perspective-viewer/src/rust/components/column_settings_sidebar/sidebar.rs

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,6 @@ impl Component for ColumnSettingsSidebar {
121121
type Properties = ColumnSettingsProps;
122122

123123
fn create(ctx: &yew::prelude::Context<Self>) -> Self {
124-
tracing::error!("Create! {:?}", ctx.props());
125124
let column_name = ctx
126125
.props()
127126
.selected_column
@@ -192,7 +191,6 @@ impl Component for ColumnSettingsSidebar {
192191
}
193192

194193
fn changed(&mut self, ctx: &yew::prelude::Context<Self>, old_props: &Self::Properties) -> bool {
195-
tracing::error!("Changed! {old_props:?} -> {:?}", ctx.props());
196194
if ctx.props() != old_props {
197195
let selected_tab = self.selected_tab;
198196
*self = Self::create(ctx);
@@ -210,7 +208,6 @@ impl Component for ColumnSettingsSidebar {
210208
}
211209

212210
fn update(&mut self, ctx: &yew::prelude::Context<Self>, msg: Self::Message) -> bool {
213-
tracing::error!("Updated! {msg:?}");
214211
match msg {
215212
ColumnSettingsMsg::SetExprValue(val) => {
216213
if self.expr_value != val {
@@ -287,8 +284,6 @@ impl Component for ColumnSettingsSidebar {
287284
}
288285

289286
fn view(&self, ctx: &yew::prelude::Context<Self>) -> Html {
290-
tracing::error!("Render!");
291-
292287
let header_props = EditableHeaderProps {
293288
icon_type: self
294289
.maybe_ty

rust/perspective-viewer/src/rust/components/editable_header.rs

Lines changed: 133 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,11 @@ use std::rc::Rc;
1515
use derivative::Derivative;
1616
use itertools::Itertools;
1717
use web_sys::{FocusEvent, HtmlInputElement, KeyboardEvent};
18-
use yew::{classes, function_component, html, Callback, Html, Properties, TargetCast};
18+
use yew::{classes, html, Callback, Component, Html, NodeRef, Properties, TargetCast};
1919

2020
use super::type_icon::TypeIconType;
21-
use crate::clone;
2221
use crate::components::type_icon::TypeIcon;
22+
use crate::maybe;
2323
use crate::session::Session;
2424

2525
#[derive(PartialEq, Properties, Derivative, Clone)]
@@ -35,6 +35,19 @@ pub struct EditableHeaderProps {
3535
#[derivative(Debug = "ignore")]
3636
pub session: Session,
3737
}
38+
impl EditableHeaderProps {
39+
fn split_placeholder(&self) -> String {
40+
let split = self
41+
.placeholder
42+
.split_once('\n')
43+
.map(|(a, _)| a)
44+
.unwrap_or(&*self.placeholder);
45+
match split.char_indices().nth(25) {
46+
None => split.to_string(),
47+
Some((idx, _)) => split[..idx].to_owned(),
48+
}
49+
}
50+
}
3851

3952
#[derive(Default, Debug, PartialEq, Copy, Clone)]
4053
pub enum ValueState {
@@ -43,132 +56,132 @@ pub enum ValueState {
4356
Edited,
4457
}
4558

46-
#[function_component(EditableHeader)]
47-
pub fn editable_header(p: &EditableHeaderProps) -> Html {
48-
let noderef = yew::use_node_ref();
49-
let value_state = yew::use_state_eq(|| ValueState::Unedited);
50-
let valid = yew::use_state_eq(|| true);
51-
let new_value = yew::use_state_eq(|| p.initial_value.clone());
59+
pub enum EditableHeaderMsg {
60+
SetNewValue(String),
61+
OnClick(()),
62+
}
63+
64+
#[derive(Default, Debug)]
65+
pub struct EditableHeader {
66+
noderef: NodeRef,
67+
edited: bool,
68+
valid: bool,
69+
value: Option<String>,
70+
placeholder: String,
71+
}
72+
impl Component for EditableHeader {
73+
type Message = EditableHeaderMsg;
74+
type Properties = EditableHeaderProps;
5275

53-
{
54-
clone!(new_value, p.initial_value);
55-
yew::use_effect_with(p.reset_count, move |_| new_value.set(initial_value.clone()));
76+
fn create(ctx: &yew::prelude::Context<Self>) -> Self {
77+
Self {
78+
value: ctx.props().initial_value.clone(),
79+
placeholder: ctx.props().split_placeholder(),
80+
valid: true,
81+
..Self::default()
82+
}
5683
}
57-
{
58-
clone!(value_state.setter());
59-
yew::use_effect_with(p.initial_value.clone(), move |_| {
60-
setter.set(ValueState::Unedited);
61-
})
84+
85+
fn changed(&mut self, ctx: &yew::prelude::Context<Self>, old_props: &Self::Properties) -> bool {
86+
if ctx.props().reset_count != old_props.reset_count {
87+
self.value = ctx.props().initial_value.clone();
88+
}
89+
if ctx.props().initial_value != old_props.initial_value {
90+
self.edited = false;
91+
self.value = ctx.props().initial_value.clone();
92+
}
93+
if !ctx.props().editable {
94+
self.edited = false;
95+
}
96+
self.placeholder = ctx.props().split_placeholder();
97+
ctx.props() != old_props
6298
}
6399

64-
let set_new_value = yew::use_callback(
65-
(
66-
new_value.clone(),
67-
p.on_change.clone(),
68-
p.initial_value.clone(),
69-
value_state.clone(),
70-
valid.clone(),
71-
p.session.clone(),
72-
),
73-
|s: String, (new_value, on_change, initial_value, value_state, valid, session)| {
74-
let maybe_s = (!s.is_empty()).then_some(s);
75-
if maybe_s == *initial_value {
76-
value_state.set(ValueState::Unedited);
77-
} else {
78-
value_state.set(ValueState::Edited);
79-
}
80-
81-
let title_is_valid = initial_value == &maybe_s
82-
|| maybe_s
83-
.as_ref()
84-
.and_then(|s| {
85-
let metadata = session.metadata();
86-
let expressions = metadata.get_expression_columns();
87-
let found = metadata
88-
.get_table_columns()?
89-
.iter()
90-
.chain(expressions)
91-
.contains(s);
92-
Some(!found)
93-
})
94-
.unwrap_or(true);
95-
96-
valid.set(title_is_valid);
97-
new_value.set(maybe_s.clone());
98-
on_change.emit((maybe_s, title_is_valid));
99-
},
100-
);
101-
102-
{
103-
clone!(value_state, new_value);
104-
yew::use_effect_with(
105-
(p.editable, p.initial_value.clone()),
106-
move |(editable, value)| {
107-
if !editable {
108-
value_state.set(ValueState::Unedited);
109-
}
110-
new_value.set(value.to_owned());
100+
fn update(&mut self, ctx: &yew::prelude::Context<Self>, msg: Self::Message) -> bool {
101+
match msg {
102+
EditableHeaderMsg::SetNewValue(new_value) => {
103+
let maybe_value = (!new_value.is_empty()).then_some(new_value.clone());
104+
self.edited = ctx.props().initial_value != maybe_value;
105+
106+
self.valid = maybe!({
107+
if maybe_value
108+
.as_ref()
109+
.map(|v| v == &self.placeholder)
110+
.unwrap_or(true)
111+
{
112+
return Some(true);
113+
}
114+
if !self.edited {
115+
return Some(true);
116+
}
117+
let metadata = ctx.props().session.metadata();
118+
let expressions = metadata.get_expression_columns();
119+
let found = metadata
120+
.get_table_columns()?
121+
.iter()
122+
.chain(expressions)
123+
.contains(&new_value);
124+
Some(!found)
125+
})
126+
.unwrap_or(true);
127+
128+
self.value = maybe_value.clone();
129+
ctx.props().on_change.emit((maybe_value, self.valid));
130+
131+
tracing::error!("EditableHeader: SetNewValue! {:?}", self);
132+
133+
true
134+
},
135+
EditableHeaderMsg::OnClick(()) => {
136+
self.noderef
137+
.cast::<HtmlInputElement>()
138+
.unwrap()
139+
.focus()
140+
.unwrap();
141+
false
111142
},
112-
);
143+
}
113144
}
114145

115-
let onclick = yew::use_callback(noderef.clone(), |_, noderef| {
116-
noderef.cast::<HtmlInputElement>().unwrap().focus().unwrap();
117-
});
118-
let onblur = yew::use_callback(
119-
set_new_value.clone(),
120-
move |e: FocusEvent, set_new_value| {
146+
fn view(&self, ctx: &yew::prelude::Context<Self>) -> Html {
147+
let mut classes = classes!("sidebar_header_contents");
148+
if ctx.props().editable {
149+
classes.push("editable");
150+
}
151+
if !self.valid {
152+
classes.push("invalid");
153+
}
154+
if self.edited {
155+
classes.push("edited");
156+
}
157+
158+
let onkeyup = ctx.link().callback(|e: KeyboardEvent| {
121159
let value = e.target_unchecked_into::<HtmlInputElement>().value();
122-
set_new_value.emit(value);
123-
},
124-
);
125-
let onkeyup = yew::use_callback(
126-
set_new_value.clone(),
127-
move |e: KeyboardEvent, set_new_value| {
160+
EditableHeaderMsg::SetNewValue(value)
161+
});
162+
let onblur = ctx.link().callback(|e: FocusEvent| {
128163
let value = e.target_unchecked_into::<HtmlInputElement>().value();
129-
set_new_value.emit(value);
130-
},
131-
);
132-
133-
let mut classes = classes!("sidebar_header_contents");
134-
if p.editable {
135-
classes.push("editable");
136-
}
137-
if !*valid {
138-
classes.push("invalid");
139-
}
140-
match *value_state {
141-
ValueState::Unedited => {},
142-
ValueState::Edited => classes.push("edited"),
143-
}
144-
let split = p
145-
.placeholder
146-
.split_once('\n')
147-
.map(|(a, _)| a)
148-
.unwrap_or(&p.placeholder);
149-
let placeholder = match split.char_indices().nth(25) {
150-
None => split.to_string(),
151-
Some((idx, _)) => split[..idx].to_owned(),
152-
};
153-
154-
html! {
155-
<div
156-
class={classes}
157-
{onclick}
158-
>
159-
if let Some(icon) = p.icon_type {
160-
<TypeIcon ty={icon}/>
161-
}
162-
<input
163-
ref={noderef}
164-
type="search"
165-
class="sidebar_header_title"
166-
disabled={!p.editable}
167-
{onblur}
168-
{onkeyup}
169-
value={(*new_value).clone()}
170-
{placeholder}
171-
/>
172-
</div>
164+
EditableHeaderMsg::SetNewValue(value)
165+
});
166+
html! {
167+
<div
168+
class={classes}
169+
onclick={ctx.link().callback(|_| EditableHeaderMsg::OnClick(()))}
170+
>
171+
if let Some(icon) = ctx.props().icon_type {
172+
<TypeIcon ty={icon}/>
173+
}
174+
<input
175+
ref={self.noderef.clone()}
176+
type="search"
177+
class="sidebar_header_title"
178+
disabled={!ctx.props().editable}
179+
{onblur}
180+
{onkeyup}
181+
value={self.value.clone()}
182+
placeholder={self.placeholder.clone()}
183+
/>
184+
</div>
185+
}
173186
}
174187
}

rust/perspective-viewer/test/js/column_settings.spec.ts

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -164,29 +164,35 @@ test.describe("Plugin Styles", () => {
164164
await editor.textarea.clear();
165165
await checkWidth();
166166
});
167-
test("Selected tab stays selected when manipulating column", async ({page}) => {
167+
test("Selected tab stays selected when manipulating column", async ({
168+
page,
169+
}) => {
168170
const view = new PageView(page);
169171
view.restore({
170172
expressions: {
171-
"expr": "1234"
173+
expr: "1234",
172174
},
173-
columns: [
174-
"Row ID"
175-
],
176-
settings: true
175+
columns: ["Row ID"],
176+
settings: true,
177177
});
178178

179-
const col = await view.settingsPanel.inactiveColumns.getColumnByName("expr");
179+
const col = await view.settingsPanel.inactiveColumns.getColumnByName(
180+
"expr"
181+
);
180182
await col.editBtn.click();
181183
await view.columnSettingsSidebar.openTab("Attributes");
182184
await checkTab(view.columnSettingsSidebar, false, true);
183-
const selectedTab = async () => {return await view.columnSettingsSidebar.selectedTab.innerText()};
185+
const selectedTab = async () => {
186+
return await view.columnSettingsSidebar.selectedTab.innerText();
187+
};
184188
expect(await selectedTab()).toBe("Attributes");
185189
await col.activeBtn.click();
186190
await checkTab(view.columnSettingsSidebar, true, true, true);
187191
expect(await selectedTab()).toBe("Attributes");
188-
await view.columnSettingsSidebar.attributesTab.expressionEditor.textarea.type("'new expr value'");
192+
await view.columnSettingsSidebar.attributesTab.expressionEditor.textarea.type(
193+
"'new expr value'"
194+
);
189195
await view.columnSettingsSidebar.attributesTab.saveBtn.click();
190196
expect(await selectedTab()).toBe("Attributes");
191-
})
197+
});
192198
});

rust/perspective-viewer/test/js/column_settings/attributes_tab.spec.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,4 +147,17 @@ test.describe("Attributes Tab", () => {
147147
let config = await view.save();
148148
expect(config?.expressions).toStrictEqual({});
149149
});
150+
test("Rename empty header as expression value", async ({ page }) => {
151+
let view = new PageView(page);
152+
let settingsPanel = await view.openSettingsPanel();
153+
await settingsPanel.createNewExpression("", "1234");
154+
await view.columnSettingsSidebar.nameInput.type("1234");
155+
await expect(
156+
view.columnSettingsSidebar.nameInputWrapper
157+
).not.toHaveClass("invalid");
158+
// NOTE: Currently when you rename a column as the contents of its placeholder,
159+
// it gets serialized with the expression name. This confuses the components and it deserializes
160+
// as if it had an empty header.
161+
// Changing this behavior may require changing the way we serialize expressions.
162+
});
150163
});

0 commit comments

Comments
 (0)