perspective_viewer/components/
column_selector.rs1mod 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 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
90pub 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}