perspective_viewer/components/
column_selector.rs1mod active_column;
14mod add_expression_button;
15mod aggregate_selector;
16mod column_selector_column_row;
17mod config_selector;
18mod empty_column;
19mod expr_edit_button;
20mod filter_column;
21mod inactive_column;
22mod invalid_column;
23mod pivot_column;
24mod sort_column;
25
26use std::iter::*;
27use std::rc::Rc;
28
29pub use column_selector_column_row::*;
30pub use empty_column::*;
31pub use invalid_column::*;
32use perspective_client::config::{ViewConfig, ViewConfigUpdate};
33pub use pivot_column::*;
34use web_sys::*;
35use yew::prelude::*;
36
37use self::active_column::*;
38use self::add_expression_button::AddExpressionButton;
39use self::config_selector::ConfigSelector;
40use self::inactive_column::*;
41use super::containers::scroll_panel::*;
42use super::containers::split_panel::{Orientation, SplitPanel};
43use crate::components::column_dropdown::{ColumnDropDownElement, ColumnDropDownPortal};
44use crate::components::containers::scroll_panel_item::ScrollPanelItem;
45use crate::config::PluginStaticConfig;
46use crate::presentation::{ColumnLocator, ColumnSettingsTarget, DragDropContainer, Presentation};
47use crate::queries::{
48 ActiveColumnState, ActiveColumnStateData, ColumnsIteratorSet, get_current_column_locator,
49};
50use crate::renderer::*;
51use crate::session::drag_drop_update::*;
52use crate::session::*;
53use crate::tasks::apply_and_render;
54use crate::utils::*;
55
56#[derive(Properties)]
57pub struct ColumnSelectorProps {
58 pub on_open_expr_panel: Callback<ColumnSettingsTarget>,
60
61 pub selected_column: Option<ColumnLocator>,
63
64 pub has_table: Option<TableLoadState>,
66 pub named_column_count: usize,
67
68 pub plugin_static_config: Rc<PluginStaticConfig>,
71 pub view_config: PtrEqRc<ViewConfig>,
72 pub drag_column: Option<String>,
73
74 pub metadata: SessionMetadataRc,
77
78 pub selected_theme: Option<String>,
80
81 pub session: Session,
83 pub renderer: Renderer,
84 pub presentation: Presentation,
85
86 #[prop_or_default]
88 pub on_resize: Option<Rc<PubSub<()>>>,
89
90 #[prop_or_default]
94 pub initial_width: f64,
95
96 #[prop_or_default]
99 pub on_auto_width: Callback<f64>,
100
101 #[prop_or_default]
105 pub on_dimensions_reset: Option<Rc<PubSub<()>>>,
106}
107
108impl PartialEq for ColumnSelectorProps {
109 fn eq(&self, rhs: &Self) -> bool {
110 self.selected_column == rhs.selected_column
111 && self.has_table == rhs.has_table
112 && self.named_column_count == rhs.named_column_count
113 && self.plugin_static_config == rhs.plugin_static_config
114 && self.view_config == rhs.view_config
115 && self.drag_column == rhs.drag_column
116 && self.metadata == rhs.metadata
117 && self.selected_theme == rhs.selected_theme
118 && self.initial_width == rhs.initial_width
119 }
120}
121
122#[derive(Debug)]
123pub enum ColumnSelectorMsg {
124 Redraw,
127 HoverActiveIndex(Option<usize>),
128 Drop((String, DragTarget, DragEffect, usize)),
129}
130
131use ColumnSelectorMsg::*;
132
133pub struct ColumnSelector {
136 _subscriptions: Vec<Subscription>,
137 drag_container: DragDropContainer,
138 column_dropdown: ColumnDropDownElement,
139 on_reset: Rc<PubSub<()>>,
140}
141
142fn close_column_settings_if_displaced(
143 presentation: &Presentation,
144 renderer: &Renderer,
145 metadata: &SessionMetadata,
146 view_config: &ViewConfig,
147 update: &ViewConfigUpdate,
148) {
149 let Some(columns) = &update.columns else {
150 return;
151 };
152
153 let ocs = presentation.get_open_column_settings();
154 if get_current_column_locator(&ocs, renderer, view_config, metadata).is_none() {
155 return;
156 }
157
158 let next_config = ViewConfig {
159 columns: columns.clone(),
160 ..view_config.clone()
161 };
162
163 if get_current_column_locator(&ocs, renderer, &next_config, metadata).is_none() {
164 presentation.set_open_column_settings(None);
165 }
166}
167
168impl Component for ColumnSelector {
169 type Message = ColumnSelectorMsg;
170 type Properties = ColumnSelectorProps;
171
172 fn create(ctx: &Context<Self>) -> Self {
173 let ColumnSelectorProps {
174 presentation,
175 session,
176 ..
177 } = ctx.props();
178
179 let drop_sub = {
180 let cb = ctx.link().callback(ColumnSelectorMsg::Drop);
181 presentation.drop_received.add_listener(cb)
182 };
183
184 let drag_container = DragDropContainer::new(|| {}, {
185 let link = ctx.link().clone();
186 move || link.send_message(ColumnSelectorMsg::HoverActiveIndex(None))
187 });
188
189 let column_dropdown = ColumnDropDownElement::new(session.clone());
190 let on_reset: Rc<PubSub<()>> = Default::default();
191 let mut subscriptions = vec![drop_sub];
192 if let Some(outer_reset) = ctx.props().on_dimensions_reset.as_ref() {
193 let on_reset = on_reset.clone();
194 subscriptions.push(outer_reset.add_listener(move |()| on_reset.emit(())));
195 }
196
197 Self {
198 _subscriptions: subscriptions,
199 drag_container,
200 column_dropdown,
201 on_reset,
202 }
203 }
204
205 fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
206 match msg {
207 Redraw => true,
208 HoverActiveIndex(Some(to_index)) => ctx
209 .props()
210 .presentation
211 .notify_drag_enter(DragTarget::Active, to_index),
212 HoverActiveIndex(_) => {
213 ctx.props()
214 .presentation
215 .notify_drag_leave(DragTarget::Active);
216 true
217 },
218 Drop((column, DragTarget::Active, DragEffect::Move(DragTarget::Active), index)) => {
219 let is_invalid = {
220 let config = &ctx.props().view_config;
221 let from_index = config
222 .columns
223 .iter()
224 .position(|x| x.as_ref() == Some(&column));
225
226 let min_cols = ctx.props().renderer.metadata().min_config_columns;
227 let is_to_empty = !config
228 .columns
229 .get(index)
230 .map(|x| x.is_some())
231 .unwrap_or_default();
232
233 min_cols
234 .and_then(|x| from_index.map(|fi| fi < x))
235 .unwrap_or_default()
236 && is_to_empty
237 };
238 if !is_invalid {
239 let col_type = ctx
240 .props()
241 .metadata
242 .get_column_table_type(column.as_str())
243 .unwrap();
244
245 let update = ctx.props().view_config.create_drag_drop_update(
246 column,
247 col_type,
248 index,
249 DragTarget::Active,
250 DragEffect::Move(DragTarget::Active),
251 &ctx.props().renderer.metadata(),
252 ctx.props().metadata.get_features().unwrap(),
253 );
254
255 close_column_settings_if_displaced(
256 &ctx.props().presentation,
257 &ctx.props().renderer,
258 &ctx.props().metadata,
259 &ctx.props().view_config,
260 &update,
261 );
262
263 let session = ctx.props().session.clone();
264 let renderer = ctx.props().renderer.clone();
265 if let Ok(task) = apply_and_render(&session, &renderer, update) {
266 spawn_owned("column-selector", task);
267 }
268 }
269
270 true
271 },
272 Drop((column, DragTarget::Active, effect, index)) => {
273 let col_type = ctx
274 .props()
275 .metadata
276 .get_column_table_type(column.as_str())
277 .unwrap();
278 let update = ctx.props().view_config.create_drag_drop_update(
279 column,
280 col_type,
281 index,
282 DragTarget::Active,
283 effect,
284 &ctx.props().renderer.metadata(),
285 ctx.props().metadata.get_features().unwrap(),
286 );
287
288 close_column_settings_if_displaced(
289 &ctx.props().presentation,
290 &ctx.props().renderer,
291 &ctx.props().metadata,
292 &ctx.props().view_config,
293 &update,
294 );
295
296 let session = ctx.props().session.clone();
297 let renderer = ctx.props().renderer.clone();
298 if let Ok(task) = apply_and_render(&session, &renderer, update) {
299 spawn_owned("column-selector", task);
300 }
301
302 true
303 },
304 Drop((_, _, DragEffect::Move(DragTarget::Active), _)) => true,
305 Drop((..)) => true,
306 }
307 }
308
309 fn view(&self, ctx: &Context<Self>) -> Html {
310 let ColumnSelectorProps {
311 session,
312 renderer,
313 presentation,
314 ..
315 } = ctx.props();
316 let metadata = &ctx.props().metadata;
317
318 let prop_config = &ctx.props().view_config;
322 let config = if prop_config.columns.is_empty() {
323 if let Some(table_cols) = metadata.get_table_columns() {
324 ViewConfig {
325 columns: table_cols.iter().map(|c| Some(c.clone())).collect(),
326 ..(**prop_config).clone()
327 }
328 .into()
329 } else {
330 prop_config.clone()
331 }
332 } else {
333 prop_config.clone()
334 };
335
336 let is_aggregated = config.is_aggregated();
337 let columns_iter = ColumnsIteratorSet::new(&config, metadata, renderer, presentation);
338 let onselect = ctx.link().callback(|()| Redraw);
339 let ondragenter = ctx.link().callback(HoverActiveIndex);
340 let ondragover = Callback::from(|_event: DragEvent| _event.prevent_default());
341 let ondrop = Callback::from({
342 clone!(presentation);
343 move |event| presentation.notify_drop(&event)
344 });
345
346 let ondragend = Callback::from({
347 clone!(presentation);
348 move |_| presentation.notify_drag_end()
349 });
350
351 let mut active_classes = classes!("scrollable");
352 if ctx.props().drag_column.is_some() {
353 active_classes.push("dragdrop-highlight");
354 };
355
356 if is_aggregated {
357 active_classes.push("is-aggregated");
358 }
359
360 let size_hint = 28.0f64.mul_add(
361 (config.group_by.len()
362 + config.split_by.len()
363 + config.filter.len()
364 + config.sort.len()) as f64,
365 metadata
366 .get_features()
367 .map(|x| {
368 let mut y = 0.0;
369 if !x.filter_ops.is_empty() {
370 y += 1.0;
371 }
372
373 if x.group_by {
374 y += 1.0;
375 }
376
377 if x.split_by {
378 y += 1.0;
379 }
380
381 if x.sort {
382 y += 1.0;
383 }
384
385 y * 55.0
386 })
387 .unwrap_or_default(),
388 );
389
390 let config_selector = html_nested! {
391 <ScrollPanelItem key="config_selector" {size_hint}>
392 <ConfigSelector
393 onselect={onselect.clone()}
394 ondragenter={ctx.link().callback(|()| Redraw)}
395 view_config={ctx.props().view_config.clone()}
396 drag_column={ctx.props().drag_column.clone()}
397 metadata={metadata.clone()}
398 selected_theme={ctx.props().selected_theme.clone()}
399 plugin_static_config={ctx.props().plugin_static_config.clone()}
400 {presentation}
401 {renderer}
402 {session}
403 />
404 </ScrollPanelItem>
405 };
406
407 let mut named_count = ctx.props().named_column_count;
408 let mut active_columns: Vec<_> = columns_iter
409 .active()
410 .enumerate()
411 .map(|(idx, name): (usize, ActiveColumnState)| {
412 let ondragenter = ondragenter.reform(move |_| Some(idx));
413 let size_hint = if named_count > 0 { 50.0 } else { 28.0 };
414 named_count = named_count.saturating_sub(1);
415 let key = name
416 .get_name()
417 .map(|x| x.to_owned())
418 .unwrap_or_else(|| format!("__auto_{idx}__"));
419
420 let column_dropdown = self.column_dropdown.clone();
421 let is_editing = matches!(
422 &ctx.props().selected_column,
423 Some(ColumnLocator::Table(x))
424 | Some(ColumnLocator::Expression(x))
425 | Some(ColumnLocator::Window(x))
426 if x == &key );
427
428 let col_type = name
433 .get_name()
434 .and_then(|n| metadata.get_column_table_type(n))
435 .or_else(|| {
436 if matches!(name.state, ActiveColumnStateData::DragOver) {
437 presentation
438 .get_drag_column()
439 .and_then(|c| metadata.get_column_table_type(&c))
440 } else {
441 None
442 }
443 });
444
445 let is_expression = name
446 .get_name()
447 .map(|n| metadata.is_column_expression(n))
448 .unwrap_or(false);
449
450 let is_window = name
451 .get_name()
452 .map(|n| metadata.is_column_window(n))
453 .unwrap_or(false);
454
455 let can_render_styles =
456 name.get_name().is_some() && renderer.can_render_column_styles();
457
458 let show_edit_btn = is_expression || is_window || can_render_styles;
459 let on_open_expr_panel = &ctx.props().on_open_expr_panel;
460 html_nested! {
461 <ScrollPanelItem {key} {size_hint}>
462 <ActiveColumn
463 {column_dropdown}
464 {idx}
465 {is_aggregated}
466 {is_editing}
467 {is_expression}
468 {is_window}
469 {show_edit_btn}
470 {col_type}
471 view_config={config.clone()}
472 metadata={metadata.clone()}
473 {name}
474 {on_open_expr_panel}
475 {ondragenter}
476 ondragend={&ondragend}
477 onselect={&onselect}
478 {presentation}
479 {renderer}
480 {session}
481 />
482 </ScrollPanelItem>
483 }
484 })
485 .collect();
486
487 let mut inactive_children: Vec<_> = columns_iter
488 .expression()
489 .chain(columns_iter.window())
490 .chain(columns_iter.inactive())
491 .enumerate()
492 .map(|(idx, vc)| {
493 let selected_column = ctx.props().selected_column.as_ref();
494 let is_editing = matches!(
495 selected_column,
496 Some(ColumnLocator::Expression(x)) | Some(ColumnLocator::Window(x))
497 if x.as_str() == vc.name
498 );
499 let is_expression = metadata.is_column_expression(vc.name);
500 let is_window = metadata.is_column_window(vc.name);
501 html_nested! {
502 <ScrollPanelItem key={vc.name} size_hint=28.0>
503 <InactiveColumn
504 {idx}
505 visible={vc.is_visible}
506 name={vc.name.to_owned()}
507 {is_editing}
508 {is_expression}
509 {is_window}
510 view_config={config.clone()}
511 metadata={metadata.clone()}
512 onselect={&onselect}
513 ondragend={&ondragend}
514 on_open_expr_panel={&ctx.props().on_open_expr_panel}
515 {presentation}
516 {renderer}
517 {session}
518 />
519 </ScrollPanelItem>
520 }
521 })
522 .collect();
523
524 let size = 28.0;
525
526 let add_column = if metadata.get_features().unwrap().expressions {
527 html_nested! {
528 <ScrollPanelItem key="__add_expression__" size_hint={size}>
529 <AddExpressionButton
530 on_open_expr_panel={&ctx.props().on_open_expr_panel}
531 selected_column={ctx.props().selected_column.clone()}
532 />
533 </ScrollPanelItem>
534 }
535 } else {
536 html_nested! {
537 <ScrollPanelItem key="__add_expression__" size_hint=0_f64><span /></ScrollPanelItem>
538 }
539 };
540
541 if inactive_children.is_empty() {
542 active_columns.push(add_column)
543 } else {
544 inactive_children.insert(0, add_column);
545 }
546
547 let mut selected_columns = vec![html! {
548 <div id="selected-columns" key="__active_columns__">
549 <ScrollPanel
550 id="active-columns"
551 omit_autosize_div={true}
552 class={active_classes}
553 dragover={ondragover}
554 dragenter={&self.drag_container.dragenter}
555 dragleave={&self.drag_container.dragleave}
556 viewport_ref={&self.drag_container.noderef}
557 initial_width={ctx.props().initial_width}
558 on_auto_width={ctx.props().on_auto_width.clone()}
559 drop={ondrop}
560 on_resize={&ctx.props().on_resize}
561 on_dimensions_reset={&self.on_reset}
562 children={std::iter::once(config_selector).chain(active_columns).collect::<Vec<_>>()}
563 />
564 </div>
565 }];
566
567 if !inactive_children.is_empty() {
568 selected_columns.push(html! {
569 <ScrollPanel
570 id="sub-columns"
571 key="__sub_columns__"
572 class={classes!("scrollable")}
573 on_resize={&ctx.props().on_resize}
574 on_dimensions_reset={&self.on_reset}
575 children={inactive_children}
576 />
577 })
578 }
579
580 html! {
581 <>
582 <SplitPanel
583 no_wrap=true
584 on_reset={self.on_reset.callback()}
585 skip_empty=true
586 orientation={Orientation::Vertical}
587 >
588 { for selected_columns }
589 </SplitPanel>
590 <ColumnDropDownPortal
591 element={self.column_dropdown.clone()}
592 theme={ctx.props().selected_theme.clone().unwrap_or_default()}
593 />
594 </>
595 }
596 }
597}