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