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