perspective_viewer/components/
column_selector.rs1mod active_column;
14mod add_expression_button;
15mod aggregate_selector;
16mod config_selector;
17mod empty_column;
18mod expr_edit_button;
19mod filter_column;
20mod inactive_column;
21mod invalid_column;
22mod pivot_column;
23mod sort_column;
24
25use std::iter::*;
26use std::rc::Rc;
27
28pub use empty_column::*;
29pub use invalid_column::*;
30use perspective_js::utils::ApiFuture;
31pub use pivot_column::*;
32use web_sys::*;
33use yew::prelude::*;
34
35use self::active_column::*;
36use self::add_expression_button::AddExpressionButton;
37use self::config_selector::ConfigSelector;
38use self::inactive_column::*;
39use super::containers::scroll_panel::*;
40use super::containers::split_panel::{Orientation, SplitPanel};
41use super::style::LocalStyle;
42use crate::components::containers::scroll_panel_item::ScrollPanelItem;
43use crate::custom_elements::ColumnDropDownElement;
44use crate::dragdrop::*;
45use crate::model::*;
46use crate::presentation::ColumnLocator;
47use crate::renderer::*;
48use crate::session::*;
49use crate::utils::*;
50use crate::*;
51
52#[derive(Properties, PerspectiveProperties!)]
53pub struct ColumnSelectorProps {
54 pub on_open_expr_panel: Callback<ColumnLocator>,
56
57 pub selected_column: Option<ColumnLocator>,
59
60 #[prop_or_default]
62 pub on_resize: Option<Rc<PubSub<()>>>,
63
64 pub session: Session,
66 pub renderer: Renderer,
67 pub dragdrop: DragDrop,
68}
69
70impl PartialEq for ColumnSelectorProps {
71 fn eq(&self, rhs: &Self) -> bool {
72 self.selected_column == rhs.selected_column
73 }
74}
75
76#[derive(Debug)]
77pub enum ColumnSelectorMsg {
78 TableLoaded,
79 ViewCreated,
80 HoverActiveIndex(Option<usize>),
81 SetWidth(f64),
82 Drag(DragEffect),
83 DragEnd,
84 Drop((String, DragTarget, DragEffect, usize)),
85}
86
87use ColumnSelectorMsg::*;
88
89pub struct ColumnSelector {
92 _subscriptions: [Subscription; 5],
93 named_row_count: usize,
94 drag_container: DragDropContainer,
95 column_dropdown: ColumnDropDownElement,
96 viewport_width: f64,
97 on_reset: Rc<PubSub<()>>,
98}
99
100impl Component for ColumnSelector {
101 type Message = ColumnSelectorMsg;
102 type Properties = ColumnSelectorProps;
103
104 fn create(ctx: &Context<Self>) -> Self {
105 let ColumnSelectorProps {
106 dragdrop,
107 renderer,
108 session,
109 ..
110 } = ctx.props();
111 let table_sub = {
112 let cb = ctx.link().callback(|_| ColumnSelectorMsg::TableLoaded);
113 session.table_loaded.add_listener(cb)
114 };
115
116 let view_sub = {
117 let cb = ctx.link().callback(|_| ColumnSelectorMsg::ViewCreated);
118 session.view_created.add_listener(cb)
119 };
120
121 let drop_sub = {
122 let cb = ctx.link().callback(ColumnSelectorMsg::Drop);
123 dragdrop.drop_received.add_listener(cb)
124 };
125
126 let drag_sub = {
127 let cb = ctx.link().callback(ColumnSelectorMsg::Drag);
128 dragdrop.dragstart_received.add_listener(cb)
129 };
130
131 let dragend_sub = {
132 let cb = ctx.link().callback(|_| ColumnSelectorMsg::DragEnd);
133 dragdrop.dragend_received.add_listener(cb)
134 };
135
136 let named = maybe! {
137 let plugin =
138 renderer.get_active_plugin().ok()?;
139
140 Some(plugin.config_column_names()?.length() as usize)
141 };
142
143 let named_row_count = named.unwrap_or_default();
144 let drag_container = DragDropContainer::new(|| {}, {
145 let link = ctx.link().clone();
146 move || link.send_message(ColumnSelectorMsg::HoverActiveIndex(None))
147 });
148
149 let column_dropdown = ColumnDropDownElement::new(session.clone());
150 Self {
151 _subscriptions: [table_sub, view_sub, drop_sub, drag_sub, dragend_sub],
152 named_row_count,
153 viewport_width: 0f64,
154 drag_container,
155 column_dropdown,
156 on_reset: Default::default(),
157 }
158 }
159
160 fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
161 match msg {
162 Drag(DragEffect::Move(DragTarget::Active)) => false,
163 Drag(_) | DragEnd | TableLoaded => true,
164 SetWidth(w) => {
165 self.viewport_width = w;
166 false
167 },
168 ViewCreated => {
169 let named = maybe! {
170 let plugin =
171 ctx.props().renderer.get_active_plugin().ok()?;
172
173 Some(plugin.config_column_names()?.length() as usize)
174 };
175
176 self.named_row_count = named.unwrap_or_default();
177 true
178 },
179 HoverActiveIndex(Some(to_index)) => ctx
180 .props()
181 .dragdrop
182 .notify_drag_enter(DragTarget::Active, to_index),
183 HoverActiveIndex(_) => {
184 ctx.props().dragdrop.notify_drag_leave(DragTarget::Active);
185 true
186 },
187 Drop((column, DragTarget::Active, DragEffect::Move(DragTarget::Active), index)) => {
188 if !ctx.props().is_invalid_columns_column(&column, index) {
189 let update = ctx.props().session.create_drag_drop_update(
190 column,
191 index,
192 DragTarget::Active,
193 DragEffect::Move(DragTarget::Active),
194 &ctx.props().renderer.metadata(),
195 );
196
197 if let Ok(task) = ctx.props().update_and_render(update) {
198 ApiFuture::spawn(task);
199 }
200 }
201
202 true
203 },
204 Drop((column, DragTarget::Active, effect, index)) => {
205 let update = ctx.props().session.create_drag_drop_update(
206 column,
207 index,
208 DragTarget::Active,
209 effect,
210 &ctx.props().renderer.metadata(),
211 );
212
213 if let Ok(task) = ctx.props().update_and_render(update) {
214 ApiFuture::spawn(task);
215 }
216
217 true
218 },
219 Drop((_, _, DragEffect::Move(DragTarget::Active), _)) => true,
220 Drop((..)) => true,
221 }
222 }
223
224 fn view(&self, ctx: &Context<Self>) -> Html {
225 let ColumnSelectorProps {
226 session,
227 renderer,
228 dragdrop,
229 ..
230 } = ctx.props();
231 let config = session.get_view_config();
232 let is_aggregated = config.is_aggregated();
233 let columns_iter = ctx.props().column_selector_iter_set(&config);
234 let onselect = ctx.link().callback(|()| ViewCreated);
235 let ondragenter = ctx.link().callback(HoverActiveIndex);
236 let ondragover = Callback::from(|_event: DragEvent| _event.prevent_default());
237 let ondrop = Callback::from({
238 clone!(dragdrop);
239 move |event| dragdrop.notify_drop(&event)
240 });
241
242 let ondragend = Callback::from({
243 clone!(dragdrop);
244 move |_| dragdrop.notify_drag_end()
245 });
246
247 let mut active_classes = classes!();
248 if ctx.props().dragdrop.get_drag_column().is_some() {
249 active_classes.push("dragdrop-highlight");
250 };
251
252 if is_aggregated {
253 active_classes.push("is-aggregated");
254 }
255
256 let size_hint = 28.0f64.mul_add(
257 (config.group_by.len()
258 + config.split_by.len()
259 + config.filter.len()
260 + config.sort.len()) as f64,
261 session
262 .metadata()
263 .get_features()
264 .map(|x| {
265 let mut y = 0.0;
266 if !x.filter_ops.is_empty() {
267 y += 1.0;
268 }
269
270 if x.group_by {
271 y += 1.0;
272 }
273
274 if x.split_by {
275 y += 1.0;
276 }
277
278 if x.sort {
279 y += 1.0;
280 }
281
282 y * 55.0
283 })
284 .unwrap_or_default(),
285 );
286
287 let config_selector = html_nested! {
288 <ScrollPanelItem key="config_selector" {size_hint}>
289 <ConfigSelector
290 onselect={onselect.clone()}
291 ondragenter={ctx.link().callback(|()| ViewCreated)}
292 {dragdrop}
293 {renderer}
294 {session}
295 />
296 </ScrollPanelItem>
297 };
298
299 let mut named_count = self.named_row_count;
300 let mut active_columns: Vec<_> = columns_iter
301 .active()
302 .enumerate()
303 .map(|(idx, name)| {
304 let ondragenter = ondragenter.reform(move |_| Some(idx));
305 let size_hint = if named_count > 0 { 50.0 } else { 28.0 };
306 named_count = named_count.saturating_sub(1);
307 let key = name
308 .get_name()
309 .map(|x| x.to_owned())
310 .unwrap_or_else(|| format!("__auto_{idx}__"));
311
312 let column_dropdown = self.column_dropdown.clone();
313 let is_editing = matches!(
314 &ctx.props().selected_column,
315 Some(ColumnLocator::Table(x)) | Some(ColumnLocator::Expression(x))
316 if x == &key );
317
318 let on_open_expr_panel = &ctx.props().on_open_expr_panel;
319 html_nested! {
320 <ScrollPanelItem {key} {size_hint}>
321 <ActiveColumn
322 {column_dropdown}
323 {idx}
324 {is_aggregated}
325 {is_editing}
326 {name}
327 {on_open_expr_panel}
328 {ondragenter}
329 ondragend={&ondragend}
330 onselect={&onselect}
331 {dragdrop}
332 {renderer}
333 {session}
334 />
335 </ScrollPanelItem>
336 }
337 })
338 .collect();
339
340 let mut inactive_children: Vec<_> = columns_iter
341 .expression()
342 .chain(columns_iter.inactive())
343 .enumerate()
344 .map(|(idx, vc)| {
345 let selected_column = ctx.props().selected_column.as_ref();
346 let is_editing = matches!(selected_column, Some(ColumnLocator::Expression(x)) if x.as_str() == vc.name);
347 html_nested! {
348 <ScrollPanelItem key={vc.name} size_hint=28.0>
349 <InactiveColumn
350 {idx}
351 visible={vc.is_visible}
352 name={vc.name.to_owned()}
353 {is_editing}
354 onselect={&onselect}
355 ondragend={&ondragend}
356 on_open_expr_panel={&ctx.props().on_open_expr_panel}
357 {dragdrop}
358 {renderer}
359 {session}
360 />
361 </ScrollPanelItem>
362 }
363 })
364 .collect();
365
366 let size = 28.0;
367
368 let add_column = if ctx
369 .props()
370 .session
371 .metadata()
372 .get_features()
373 .unwrap()
374 .expressions
375 {
376 html_nested! {
377 <ScrollPanelItem key="__add_expression__" size_hint={size}>
378 <AddExpressionButton
379 on_open_expr_panel={&ctx.props().on_open_expr_panel}
380 selected_column={ctx.props().selected_column.clone()}
381 />
382 </ScrollPanelItem>
383 }
384 } else {
385 html_nested! {
386 <ScrollPanelItem key="__add_expression__" size_hint=0_f64><span /></ScrollPanelItem>
387 }
388 };
389
390 if inactive_children.is_empty() {
391 active_columns.push(add_column)
392 } else {
393 inactive_children.insert(0, add_column);
394 }
395
396 let mut selected_columns = vec![html! {
397 <div id="selected-columns" key="__active_columns__">
398 <ScrollPanel
399 id="active-columns"
400 class={active_classes}
401 dragover={ondragover}
402 dragenter={&self.drag_container.dragenter}
403 dragleave={&self.drag_container.dragleave}
404 viewport_ref={&self.drag_container.noderef}
405 initial_width={self.viewport_width}
406 on_auto_width={ctx.link().callback(ColumnSelectorMsg::SetWidth)}
407 drop={ondrop}
408 on_resize={&ctx.props().on_resize}
409 on_dimensions_reset={&self.on_reset}
410 children={std::iter::once(config_selector).chain(active_columns).collect::<Vec<_>>()}
411 />
412 </div>
413 }];
414
415 if !inactive_children.is_empty() {
416 selected_columns.push(html! {
417 <ScrollPanel
418 id="sub-columns"
419 key="__sub_columns__"
420 on_resize={&ctx.props().on_resize}
421 on_dimensions_reset={&self.on_reset}
422 children={inactive_children}
423 />
424 })
425 }
426
427 html! {
428 <>
429 <LocalStyle href={css!("column-selector")} />
430 <SplitPanel
431 no_wrap=true
432 on_reset={self.on_reset.callback()}
433 skip_empty=true
434 orientation={Orientation::Vertical}
435 >
436 { for selected_columns }
437 </SplitPanel>
438 </>
439 }
440 }
441}