perspective_viewer/components/
column_selector.rs

1// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
2// ┃ ██████ ██████ ██████       █      █      █      █      █ █▄  ▀███ █       ┃
3// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█  ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄  ▀█ █ ▀▀▀▀▀ ┃
4// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄   █ ▄▄▄▄▄ ┃
5// ┃ █      ██████ █  ▀█▄       █ ██████      █      ███▌▐███ ███████▄ █       ┃
6// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
7// ┃ Copyright (c) 2017, the Perspective Authors.                              ┃
8// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
9// ┃ This file is part of the Perspective library, distributed under the terms ┃
10// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
13mod active_column;
14mod add_expression_button;
15mod aggregate_selector;
16mod config_selector;
17mod empty_column;
18mod expression_toolbar;
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 web_sys::*;
31use yew::prelude::*;
32
33use self::active_column::*;
34use self::add_expression_button::AddExpressionButton;
35use self::config_selector::ConfigSelector;
36use self::inactive_column::*;
37use super::containers::scroll_panel::*;
38use super::containers::split_panel::{Orientation, SplitPanel};
39use super::style::LocalStyle;
40use super::viewer::ColumnLocator;
41use crate::components::containers::scroll_panel_item::ScrollPanelItem;
42use crate::custom_elements::ColumnDropDownElement;
43use crate::dragdrop::*;
44use crate::model::*;
45use crate::presentation::Presentation;
46use crate::renderer::*;
47use crate::session::*;
48use crate::utils::*;
49use crate::*;
50
51#[derive(Properties)]
52pub struct ColumnSelectorProps {
53    pub session: Session,
54    pub renderer: Renderer,
55    pub dragdrop: DragDrop,
56    pub presentation: Presentation,
57
58    pub on_open_expr_panel: Callback<ColumnLocator>,
59
60    /// This is passed to the add_expression_button for styling.
61    pub selected_column: Option<ColumnLocator>,
62
63    #[prop_or_default]
64    pub on_resize: Option<Rc<PubSub<()>>>,
65
66    #[prop_or_default]
67    pub on_dimensions_reset: Option<Rc<PubSub<()>>>,
68}
69
70derive_model!(DragDrop, Renderer, Session for ColumnSelectorProps);
71
72impl PartialEq for ColumnSelectorProps {
73    fn eq(&self, rhs: &Self) -> bool {
74        self.selected_column == rhs.selected_column
75    }
76}
77
78#[derive(Debug)]
79pub enum ColumnSelectorMsg {
80    TableLoaded,
81    ViewCreated,
82    HoverActiveIndex(Option<usize>),
83    Drag(DragEffect),
84    DragEnd,
85    Drop((String, DragTarget, DragEffect, usize)),
86}
87
88use ColumnSelectorMsg::*;
89
90/// A `ColumnSelector` controls the `columns` field of the `ViewConfig`,
91/// deriving its options from the table columns and `ViewConfig` expressions.
92pub struct ColumnSelector {
93    _subscriptions: [Subscription; 5],
94    named_row_count: usize,
95    drag_container: DragDropContainer,
96    column_dropdown: ColumnDropDownElement,
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 table_sub = {
106            let cb = ctx.link().callback(|_| ColumnSelectorMsg::TableLoaded);
107            ctx.props().session.table_loaded.add_listener(cb)
108        };
109
110        let view_sub = {
111            let cb = ctx.link().callback(|_| ColumnSelectorMsg::ViewCreated);
112            ctx.props().session.view_created.add_listener(cb)
113        };
114
115        let drop_sub = {
116            let cb = ctx.link().callback(ColumnSelectorMsg::Drop);
117            ctx.props().dragdrop.drop_received.add_listener(cb)
118        };
119
120        let drag_sub = {
121            let cb = ctx.link().callback(ColumnSelectorMsg::Drag);
122            ctx.props().dragdrop.dragstart_received.add_listener(cb)
123        };
124
125        let dragend_sub = {
126            let cb = ctx.link().callback(|_| ColumnSelectorMsg::DragEnd);
127            ctx.props().dragdrop.dragend_received.add_listener(cb)
128        };
129
130        let named = maybe! {
131            let plugin =
132                ctx.props().renderer.get_active_plugin().ok()?;
133
134            Some(plugin.config_column_names()?.length() as usize)
135        };
136
137        let named_row_count = named.unwrap_or_default();
138        let drag_container = DragDropContainer::new(|| {}, {
139            let link = ctx.link().clone();
140            move || link.send_message(ColumnSelectorMsg::HoverActiveIndex(None))
141        });
142
143        let column_dropdown = ColumnDropDownElement::new(ctx.props().session.clone());
144        Self {
145            _subscriptions: [table_sub, view_sub, drop_sub, drag_sub, dragend_sub],
146            named_row_count,
147            drag_container,
148            column_dropdown,
149            on_reset: Default::default(),
150        }
151    }
152
153    fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
154        match msg {
155            Drag(DragEffect::Move(DragTarget::Active)) => false,
156            Drag(_) | DragEnd | TableLoaded => true,
157            ViewCreated => {
158                let named = maybe! {
159                    let plugin =
160                        ctx.props().renderer.get_active_plugin().ok()?;
161
162                    Some(plugin.config_column_names()?.length() as usize)
163                };
164
165                self.named_row_count = named.unwrap_or_default();
166                true
167            },
168            HoverActiveIndex(Some(to_index)) => ctx
169                .props()
170                .dragdrop
171                .notify_drag_enter(DragTarget::Active, to_index),
172            HoverActiveIndex(_) => {
173                ctx.props().dragdrop.notify_drag_leave(DragTarget::Active);
174                true
175            },
176            Drop((column, DragTarget::Active, DragEffect::Move(DragTarget::Active), index)) => {
177                if !ctx.props().is_invalid_columns_column(&column, index) {
178                    let update = ctx.props().session.create_drag_drop_update(
179                        column,
180                        index,
181                        DragTarget::Active,
182                        DragEffect::Move(DragTarget::Active),
183                        &ctx.props().renderer.metadata(),
184                    );
185
186                    if let Ok(task) = ctx.props().update_and_render(update) {
187                        ApiFuture::spawn(task);
188                    }
189                }
190
191                true
192            },
193            Drop((column, DragTarget::Active, effect, index)) => {
194                let update = ctx.props().session.create_drag_drop_update(
195                    column,
196                    index,
197                    DragTarget::Active,
198                    effect,
199                    &ctx.props().renderer.metadata(),
200                );
201
202                if let Ok(task) = ctx.props().update_and_render(update) {
203                    ApiFuture::spawn(task);
204                }
205
206                true
207            },
208            Drop((_, _, DragEffect::Move(DragTarget::Active), _)) => true,
209            Drop((..)) => true,
210        }
211    }
212
213    fn view(&self, ctx: &Context<Self>) -> Html {
214        let config = ctx.props().session.get_view_config();
215        let is_aggregated = config.is_aggregated();
216        let columns_iter = ctx.props().column_selector_iter_set(&config);
217        let onselect = ctx.link().callback(|()| ViewCreated);
218        let ondragenter = ctx.link().callback(HoverActiveIndex);
219        let ondragover = Callback::from(|_event: DragEvent| _event.prevent_default());
220        let ondrop = Callback::from({
221            let dragdrop = ctx.props().dragdrop.clone();
222            move |event| dragdrop.notify_drop(&event)
223        });
224
225        let ondragend = Callback::from({
226            let dragdrop = ctx.props().dragdrop.clone();
227            move |_| dragdrop.notify_drag_end()
228        });
229
230        let mut active_classes = classes!();
231        if ctx.props().dragdrop.get_drag_column().is_some() {
232            active_classes.push("dragdrop-highlight");
233        };
234
235        if is_aggregated {
236            active_classes.push("is-aggregated");
237        }
238
239        let size_hint = 28.0f64.mul_add(
240            (config.group_by.len()
241                + config.split_by.len()
242                + config.filter.len()
243                + config.sort.len()) as f64,
244            220.0,
245        );
246
247        let config_selector = html_nested! {
248            <ScrollPanelItem key="config_selector" {size_hint}>
249                <ConfigSelector
250                    dragdrop={&ctx.props().dragdrop}
251                    session={&ctx.props().session}
252                    renderer={&ctx.props().renderer}
253                    onselect={onselect.clone()}
254                    ondragenter={ctx.link().callback(|()| ViewCreated)}
255                />
256            </ScrollPanelItem>
257        };
258
259        let mut named_count = self.named_row_count;
260        let mut active_columns: Vec<_> = columns_iter
261            .active()
262            .enumerate()
263            .map(|(idx, name)| {
264                let ondragenter = ondragenter.reform(move |_| Some(idx));
265                let size_hint = if named_count > 0 { 50.0 } else { 28.0 };
266                named_count = named_count.saturating_sub(1);
267                let key = name
268                    .get_name()
269                    .map(|x| x.to_owned())
270                    .unwrap_or_else(|| format!("__auto_{}__", idx));
271
272                let column_dropdown = self.column_dropdown.clone();
273                let is_editing = matches!(
274                    &ctx.props().selected_column,
275                    Some(ColumnLocator::Table(x)) | Some(ColumnLocator::Expression(x))
276                if x == &key );
277
278                let on_open_expr_panel = &ctx.props().on_open_expr_panel;
279                html_nested! {
280                    <ScrollPanelItem {key} {size_hint}>
281                        <ActiveColumn
282                            {column_dropdown}
283                            {idx}
284                            {is_aggregated}
285                            {is_editing}
286                            {name}
287                            {on_open_expr_panel}
288                            dragdrop={&ctx.props().dragdrop}
289                            session={&ctx.props().session}
290                            renderer={&ctx.props().renderer}
291                            presentation={&ctx.props().presentation}
292                            {ondragenter}
293                            ondragend={&ondragend}
294                            onselect={&onselect}
295                        />
296                    </ScrollPanelItem>
297                }
298            })
299            .collect();
300
301        let mut inactive_children: Vec<_> = columns_iter
302            .expression()
303            .chain(columns_iter.inactive())
304            .enumerate()
305            .map(|(idx, vc)| {
306                let selected_column = ctx.props().selected_column.as_ref();
307                let is_editing = matches!(selected_column, Some(ColumnLocator::Expression(x)) if x.as_str() == vc.name);
308                html_nested! {
309                    <ScrollPanelItem key={vc.name} size_hint=28.0>
310                        <InactiveColumn
311                            {idx}
312                            visible={vc.is_visible}
313                            name={vc.name.to_owned()}
314                            dragdrop={&ctx.props().dragdrop}
315                            session={&ctx.props().session}
316                            renderer={&ctx.props().renderer}
317                            presentation={&ctx.props().presentation}
318                            {is_editing}
319                            onselect={&onselect}
320                            ondragend={&ondragend}
321                            on_open_expr_panel={&ctx.props().on_open_expr_panel}
322                        />
323                    </ScrollPanelItem>
324                }
325            })
326            .collect();
327
328        let size = if !inactive_children.is_empty() {
329            56.0
330        } else {
331            28.0
332        };
333
334        let add_column = html_nested! {
335            <ScrollPanelItem key="__add_expression__" size_hint={size}>
336                <AddExpressionButton
337                    on_open_expr_panel={&ctx.props().on_open_expr_panel}
338                    selected_column={ctx.props().selected_column.clone()}
339                />
340            </ScrollPanelItem>
341        };
342
343        if inactive_children.is_empty() {
344            active_columns.push(add_column)
345        } else {
346            inactive_children.insert(0, add_column);
347        }
348
349        let selected_columns = html! {
350            <div id="selected-columns">
351                <ScrollPanel
352                    id="active-columns"
353                    class={active_classes}
354                    dragover={ondragover}
355                    dragenter={&self.drag_container.dragenter}
356                    dragleave={&self.drag_container.dragleave}
357                    viewport_ref={&self.drag_container.noderef}
358                    drop={ondrop}
359                    on_resize={&ctx.props().on_resize}
360                    on_dimensions_reset={&self.on_reset}
361                    children={std::iter::once(config_selector).chain(active_columns).collect::<Vec<_>>()}
362                />
363            </div>
364        };
365
366        html! {
367            <>
368                <LocalStyle href={css!("column-selector")} />
369                <SplitPanel
370                    no_wrap=true
371                    on_reset={self.on_reset.callback()}
372                    skip_empty=true
373                    orientation={Orientation::Vertical}
374                >
375                    { selected_columns }
376                    if !inactive_children.is_empty() {
377                        <ScrollPanel
378                            id="sub-columns"
379                            on_resize={&ctx.props().on_resize}
380                            on_dimensions_reset={&self.on_reset}
381                            children={inactive_children}
382                        />
383                    }
384                </SplitPanel>
385            </>
386        }
387    }
388}