@perspective-dev/viewer 4.3.0 → 4.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (227) hide show
  1. package/dist/cdn/perspective-viewer.js +2 -2
  2. package/dist/cdn/perspective-viewer.js.map +4 -4
  3. package/dist/css/botanical.css +1 -1
  4. package/dist/css/dracula.css +1 -1
  5. package/dist/css/gruvbox-dark.css +1 -1
  6. package/dist/css/gruvbox.css +1 -1
  7. package/dist/css/icons.css +1 -1
  8. package/dist/css/intl/de.css +1 -1
  9. package/dist/css/intl/es.css +1 -1
  10. package/dist/css/intl/fr.css +1 -1
  11. package/dist/css/intl/ja.css +1 -1
  12. package/dist/css/intl/pt.css +1 -1
  13. package/dist/css/intl/zh.css +1 -1
  14. package/dist/css/intl.css +1 -1
  15. package/dist/css/monokai.css +1 -1
  16. package/dist/css/pro-dark.css +1 -1
  17. package/dist/css/pro.css +1 -1
  18. package/dist/css/solarized-dark.css +1 -1
  19. package/dist/css/solarized.css +1 -1
  20. package/dist/css/themes.css +1 -1
  21. package/dist/css/vaporwave.css +1 -1
  22. package/dist/esm/perspective-viewer.inline.js +2 -2
  23. package/dist/esm/perspective-viewer.inline.js.map +4 -4
  24. package/dist/esm/perspective-viewer.js +2 -2
  25. package/dist/esm/perspective-viewer.js.map +4 -4
  26. package/dist/wasm/perspective-viewer.d.ts +57 -53
  27. package/dist/wasm/perspective-viewer.js +190 -165
  28. package/dist/wasm/perspective-viewer.wasm +0 -0
  29. package/dist/wasm/perspective-viewer.wasm.d.ts +17 -18
  30. package/package.json +7 -5
  31. package/src/{less/aggregate-selector.less → css/aggregate-selector.css} +23 -20
  32. package/src/css/column-dropdown.css +109 -0
  33. package/src/{less/column-selector.less → css/column-selector.css} +160 -158
  34. package/src/{less/column-settings-panel.less → css/column-settings-panel.css} +69 -59
  35. package/src/{less/column-style.less → css/column-style.css} +52 -66
  36. package/src/{less/column-symbol-attributes.less → css/column-symbol-attributes.css} +15 -14
  37. package/src/{less/config-selector.less → css/config-selector.css} +151 -135
  38. package/src/{less/containers/dropdown-menu.less → css/containers/dropdown-menu.css} +20 -19
  39. package/src/{less/containers/pairs-list.less → css/containers/pairs-list.css} +13 -12
  40. package/src/{themes/variables.less → css/containers/scroll-panel.css} +25 -22
  41. package/src/{less/containers/split-panel.less → css/containers/split-panel.css} +15 -14
  42. package/src/{less/containers/tabs.less → css/containers/tabs.css} +17 -19
  43. package/src/css/dom/checkbox.css +102 -0
  44. package/src/css/dom/scrollbar.css +35 -0
  45. package/src/{less/dom/select.less → css/dom/select.css} +17 -18
  46. package/src/{less/empty-column.less → css/empty-column.css} +19 -18
  47. package/src/{less/expression-editor.less → css/expression-editor.css} +19 -18
  48. package/src/{less/filter-dropdown.less → css/filter-dropdown.css} +12 -11
  49. package/src/{less/filter-item.less → css/filter-item.css} +16 -15
  50. package/src/{less/form/code-editor.less → css/form/code-editor.css} +26 -30
  51. package/src/{less/form/debug.less → css/form/debug.css} +19 -18
  52. package/src/{less/function-dropdown.less → css/function-dropdown.css} +12 -11
  53. package/src/css/plugin-selector.css +261 -0
  54. package/src/{less/render-warning.less → css/render-warning.css} +18 -17
  55. package/src/{less/status-bar.less → css/status-bar.css} +156 -144
  56. package/src/css/type-icon.css +116 -0
  57. package/src/{less/viewer.less → css/viewer.css} +112 -146
  58. package/src/rust/components/column_dropdown.rs +229 -119
  59. package/src/rust/components/column_selector/active_column.rs +81 -62
  60. package/src/rust/components/column_selector/add_expression_button.rs +1 -0
  61. package/src/rust/components/column_selector/aggregate_selector.rs +25 -15
  62. package/src/rust/components/column_selector/config_selector.rs +315 -199
  63. package/src/rust/components/column_selector/empty_column.rs +2 -2
  64. package/src/rust/components/column_selector/expr_edit_button.rs +8 -2
  65. package/src/rust/components/column_selector/filter_column.rs +37 -26
  66. package/src/rust/components/column_selector/inactive_column.rs +41 -29
  67. package/src/rust/components/column_selector/invalid_column.rs +7 -18
  68. package/src/rust/components/column_selector/pivot_column.rs +11 -5
  69. package/src/rust/components/column_selector/sort_column.rs +23 -13
  70. package/src/rust/components/column_selector.rs +163 -84
  71. package/src/rust/components/column_settings_sidebar/style_tab/symbol/row_selector.rs +1 -1
  72. package/src/rust/components/column_settings_sidebar/style_tab/symbol/symbol_pairs.rs +3 -2
  73. package/src/rust/components/column_settings_sidebar/style_tab/symbol/symbol_pairs_item.rs +3 -2
  74. package/src/rust/components/column_settings_sidebar/style_tab/symbol/symbol_selector.rs +2 -3
  75. package/src/rust/components/column_settings_sidebar/style_tab/symbol.rs +7 -1
  76. package/src/rust/components/column_settings_sidebar/style_tab.rs +153 -112
  77. package/src/rust/components/column_settings_sidebar.rs +91 -53
  78. package/src/rust/components/containers/dragdrop_list.rs +2 -1
  79. package/src/rust/components/containers/sidebar_close_button.rs +1 -1
  80. package/src/rust/components/containers/split_panel.rs +1 -0
  81. package/src/rust/components/containers/tab_list.rs +1 -1
  82. package/src/rust/components/copy_dropdown.rs +7 -28
  83. package/src/rust/components/datetime_column_style/custom.rs +2 -2
  84. package/src/rust/components/datetime_column_style/simple.rs +2 -2
  85. package/src/rust/components/datetime_column_style.rs +4 -2
  86. package/src/rust/components/editable_header.rs +7 -4
  87. package/src/rust/components/empty_row.rs +1 -1
  88. package/src/rust/components/export_dropdown.rs +4 -30
  89. package/src/rust/components/expression_editor.rs +19 -10
  90. package/src/rust/components/filter_dropdown.rs +246 -102
  91. package/src/rust/components/font_loader.rs +11 -28
  92. package/src/rust/components/form/code_editor.rs +17 -2
  93. package/src/rust/components/form/color_range_selector.rs +19 -6
  94. package/src/rust/components/form/debug.rs +30 -13
  95. package/src/rust/components/function_dropdown.rs +186 -113
  96. package/src/rust/components/main_panel.rs +71 -89
  97. package/src/rust/components/mod.rs +1 -1
  98. package/src/rust/components/modal.rs +7 -1
  99. package/src/rust/components/number_column_style.rs +22 -7
  100. package/src/rust/components/plugin_selector.rs +34 -102
  101. package/src/rust/components/portal.rs +274 -0
  102. package/src/rust/components/render_warning.rs +72 -123
  103. package/src/rust/components/settings_panel.rs +115 -11
  104. package/src/rust/components/status_bar.rs +222 -98
  105. package/src/rust/components/status_bar_counter.rs +8 -20
  106. package/src/rust/components/status_indicator.rs +64 -114
  107. package/src/rust/components/string_column_style.rs +2 -2
  108. package/src/rust/components/style/style_cache.rs +5 -1
  109. package/src/rust/components/viewer.rs +391 -39
  110. package/src/rust/custom_elements/copy_dropdown.rs +102 -21
  111. package/src/rust/custom_elements/export_dropdown.rs +102 -20
  112. package/src/rust/custom_elements/mod.rs +0 -7
  113. package/src/rust/custom_elements/modal.rs +7 -103
  114. package/src/rust/custom_elements/viewer.rs +99 -35
  115. package/src/rust/custom_events.rs +23 -2
  116. package/src/rust/dragdrop.rs +149 -10
  117. package/src/{less/containers/scroll-panel.less → rust/engines.rs} +15 -13
  118. package/src/rust/js/plugin.rs +1 -1
  119. package/src/rust/lib.rs +5 -4
  120. package/src/rust/presentation/props.rs +39 -0
  121. package/src/rust/presentation/sheets.rs +3 -3
  122. package/src/rust/presentation.rs +44 -8
  123. package/src/rust/renderer/limits.rs +32 -3
  124. package/src/{less/dom/scrollbar.less → rust/renderer/props.rs} +18 -19
  125. package/src/rust/renderer.rs +83 -9
  126. package/src/rust/session/column_defaults_update.rs +1 -1
  127. package/src/rust/session/metadata.rs +23 -2
  128. package/src/rust/session/props.rs +178 -0
  129. package/src/rust/session.rs +124 -117
  130. package/src/rust/tasks/column_locator.rs +133 -0
  131. package/src/rust/{model → tasks}/columns_iter_set.rs +14 -23
  132. package/src/rust/{model → tasks}/edit_expression.rs +34 -10
  133. package/src/rust/{model → tasks}/eject.rs +2 -2
  134. package/src/rust/{model → tasks}/get_viewer_config.rs +0 -11
  135. package/src/rust/{model → tasks}/intersection_observer.rs +19 -3
  136. package/src/{less/containers/radio-list.less → rust/tasks/is_invalid_drop.rs} +21 -14
  137. package/src/rust/tasks/mod.rs +52 -0
  138. package/src/rust/{model → tasks}/plugin_column_styles.rs +69 -46
  139. package/src/rust/{model → tasks}/resize_observer.rs +39 -6
  140. package/src/rust/{model → tasks}/send_plugin_config.rs +1 -1
  141. package/src/rust/tasks/structural.rs +53 -0
  142. package/src/rust/utils/mod.rs +4 -0
  143. package/src/rust/utils/modal_position.rs +110 -0
  144. package/src/rust/utils/ptr_eq_rc.rs +74 -0
  145. package/src/rust/utils/pubsub.rs +11 -1
  146. package/src/svg/bg-pattern.png +0 -0
  147. package/src/svg/close-icon.svg +1 -1
  148. package/src/svg/expression.svg +1 -1
  149. package/src/svg/mega-menu-icons-candlestick.svg +1 -1
  150. package/src/svg/mega-menu-icons-datagrid.svg +1 -2
  151. package/src/svg/mega-menu-icons-heatmap.svg +1 -1
  152. package/src/svg/mega-menu-icons-map-scatter.svg +1 -1
  153. package/src/svg/mega-menu-icons-ohlc.svg +1 -1
  154. package/src/svg/mega-menu-icons-sunburst.svg +1 -1
  155. package/src/svg/mega-menu-icons-treemap.svg +1 -1
  156. package/src/svg/mega-menu-icons-x-bar.svg +1 -1
  157. package/src/svg/mega-menu-icons-x-y-line.svg +1 -1
  158. package/src/svg/mega-menu-icons-x-y-scatter.svg +1 -1
  159. package/src/svg/mega-menu-icons-y-area.svg +1 -1
  160. package/src/svg/mega-menu-icons-y-bar.svg +1 -1
  161. package/src/svg/mega-menu-icons-y-line.svg +1 -1
  162. package/src/svg/mega-menu-icons-y-scatter.svg +1 -1
  163. package/src/svg/radio-hover.svg +1 -1
  164. package/src/svg/radio-off.svg +1 -1
  165. package/src/svg/radio-on.svg +1 -1
  166. package/src/themes/botanical.css +157 -0
  167. package/src/themes/defaults.css +139 -0
  168. package/src/themes/dracula.css +233 -0
  169. package/src/themes/gruvbox-dark.css +255 -0
  170. package/src/themes/gruvbox.css +134 -0
  171. package/src/themes/icons.css +124 -0
  172. package/src/themes/intl/de.css +102 -0
  173. package/src/themes/intl/es.css +102 -0
  174. package/src/themes/intl/fr.css +102 -0
  175. package/src/themes/intl/ja.css +102 -0
  176. package/src/themes/intl/pt.css +102 -0
  177. package/src/themes/intl/zh.css +102 -0
  178. package/src/themes/intl.css +102 -0
  179. package/src/themes/monokai.css +233 -0
  180. package/src/themes/pro-dark.css +158 -0
  181. package/src/themes/{themes.less → pro.css} +17 -21
  182. package/src/themes/solarized-dark.css +135 -0
  183. package/src/themes/solarized.css +95 -0
  184. package/src/themes/themes.css +22 -0
  185. package/src/themes/vaporwave.css +256 -0
  186. package/dist/css/variables.css +0 -0
  187. package/src/less/column-dropdown.less +0 -95
  188. package/src/less/dom/checkbox.less +0 -100
  189. package/src/less/plugin-selector.less +0 -183
  190. package/src/less/type-icon.less +0 -68
  191. package/src/rust/components/error_message.rs +0 -56
  192. package/src/rust/custom_elements/column_dropdown.rs +0 -123
  193. package/src/rust/custom_elements/filter_dropdown.rs +0 -179
  194. package/src/rust/custom_elements/function_dropdown.rs +0 -115
  195. package/src/rust/model/column_locator.rs +0 -82
  196. package/src/rust/model/is_invalid_drop.rs +0 -36
  197. package/src/rust/model/mod.rs +0 -100
  198. package/src/rust/model/reset_all.rs +0 -38
  199. package/src/rust/model/structural.rs +0 -244
  200. package/src/themes/botanical.less +0 -142
  201. package/src/themes/dracula.less +0 -101
  202. package/src/themes/gruvbox-dark.less +0 -116
  203. package/src/themes/gruvbox.less +0 -152
  204. package/src/themes/icons.less +0 -130
  205. package/src/themes/intl/de.less +0 -102
  206. package/src/themes/intl/es.less +0 -102
  207. package/src/themes/intl/fr.less +0 -102
  208. package/src/themes/intl/ja.less +0 -102
  209. package/src/themes/intl/pt.less +0 -102
  210. package/src/themes/intl/zh.less +0 -102
  211. package/src/themes/intl.less +0 -102
  212. package/src/themes/monokai.less +0 -107
  213. package/src/themes/pro-dark.less +0 -147
  214. package/src/themes/pro.less +0 -186
  215. package/src/themes/solarized-dark.less +0 -78
  216. package/src/themes/solarized.less +0 -102
  217. package/src/themes/vaporwave.less +0 -145
  218. /package/dist/wasm/snippets/{perspective-viewer-d729f682ba5c19df → perspective-viewer-68fef752754ffbc6}/inline0.js +0 -0
  219. /package/dist/wasm/snippets/{perspective-viewer-d729f682ba5c19df → perspective-viewer-68fef752754ffbc6}/inline1.js +0 -0
  220. /package/dist/wasm/snippets/{perspective-viewer-d729f682ba5c19df → perspective-viewer-68fef752754ffbc6}/inline2.js +0 -0
  221. /package/dist/wasm/snippets/{perspective-viewer-d729f682ba5c19df → perspective-viewer-68fef752754ffbc6}/inline3.js +0 -0
  222. /package/dist/wasm/snippets/{perspective-viewer-d729f682ba5c19df → perspective-viewer-68fef752754ffbc6}/inline4.js +0 -0
  223. /package/src/rust/{model → tasks}/copy_export.rs +0 -0
  224. /package/src/rust/{model → tasks}/export_app.rs +0 -0
  225. /package/src/rust/{model → tasks}/export_method.rs +0 -0
  226. /package/src/rust/{model → tasks}/restore_and_render.rs +0 -0
  227. /package/src/rust/{model → tasks}/update_and_render.rs +0 -0
@@ -10,153 +10,263 @@
10
10
  // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11
11
  // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
12
 
13
+ use std::cell::RefCell;
14
+ use std::collections::HashSet;
15
+ use std::rc::Rc;
16
+
17
+ use perspective_client::clone;
18
+ use perspective_client::config::Expression;
13
19
  use web_sys::*;
20
+ use yew::html::ImplicitClone;
14
21
  use yew::prelude::*;
15
22
 
16
23
  use super::column_selector::InPlaceColumn;
17
- use super::modal::*;
18
- use crate::utils::WeakScope;
24
+ use super::portal::PortalModal;
25
+ use crate::session::Session;
26
+ use crate::utils::*;
27
+ use crate::*;
19
28
 
20
29
  static CSS: &str = include_str!(concat!(env!("OUT_DIR"), "/css/column-dropdown.css"));
21
30
 
22
- #[derive(Properties, PartialEq)]
23
- pub struct ColumnDropDownProps {
24
- #[prop_or_default]
25
- pub weak_link: WeakScope<ColumnDropDown>,
26
- }
27
-
28
- impl ModalLink<ColumnDropDown> for ColumnDropDownProps {
29
- fn weak_link(&self) -> &'_ WeakScope<ColumnDropDown> {
30
- &self.weak_link
31
- }
31
+ /// Shared state for the column dropdown, updated imperatively.
32
+ #[derive(Default)]
33
+ pub struct ColumnDropDownState {
34
+ pub values: Vec<InPlaceColumn>,
35
+ pub selected: usize,
36
+ pub width: f64,
37
+ pub on_select: Option<Callback<InPlaceColumn>>,
38
+ pub target: Option<HtmlElement>,
39
+ pub no_results: bool,
32
40
  }
33
41
 
34
- pub enum ColumnDropDownMsg {
35
- SetValues(Vec<InPlaceColumn>, f64),
36
- SetCallback(Callback<InPlaceColumn>),
37
- ItemDown,
38
- ItemUp,
39
- ItemSelect,
42
+ /// A clonable handle for the column dropdown shared state.
43
+ #[derive(Clone)]
44
+ pub struct ColumnDropDownElement {
45
+ state: Rc<RefCell<ColumnDropDownState>>,
46
+ session: Session,
47
+ notify: Rc<PubSub<()>>,
40
48
  }
41
49
 
42
- pub struct ColumnDropDown {
43
- values: Option<Vec<InPlaceColumn>>,
44
- selected: usize,
45
- width: f64,
46
- on_select: Option<Callback<InPlaceColumn>>,
50
+ impl PartialEq for ColumnDropDownElement {
51
+ fn eq(&self, other: &Self) -> bool {
52
+ Rc::ptr_eq(&self.state, &other.state)
53
+ }
47
54
  }
48
55
 
49
- impl Component for ColumnDropDown {
50
- type Message = ColumnDropDownMsg;
51
- type Properties = ColumnDropDownProps;
56
+ impl ImplicitClone for ColumnDropDownElement {}
52
57
 
53
- fn create(ctx: &Context<Self>) -> Self {
54
- ctx.set_modal_link();
58
+ impl ColumnDropDownElement {
59
+ pub fn new(session: Session) -> Self {
55
60
  Self {
56
- values: Some(vec![]),
57
- selected: 0,
58
- width: 0.0,
59
- on_select: None,
61
+ state: Default::default(),
62
+ session,
63
+ notify: Rc::default(),
60
64
  }
61
65
  }
62
66
 
63
- fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
64
- match msg {
65
- ColumnDropDownMsg::SetCallback(callback) => {
66
- self.on_select = Some(callback);
67
- false
68
- },
69
- ColumnDropDownMsg::SetValues(values, width) => {
70
- self.values = Some(values);
71
- self.selected = 0;
72
- self.width = width;
73
- true
74
- },
75
- ColumnDropDownMsg::ItemSelect => {
76
- if let Some(ref values) = self.values {
77
- match values.get(self.selected) {
78
- None => {
79
- console::error_1(&"Selected out-of-bounds".into());
80
- false
81
- },
82
- Some(x) => {
83
- self.on_select.as_ref().unwrap().emit(x.clone());
84
- false
85
- },
86
- }
87
- } else {
88
- console::error_1(&"No Values".into());
89
- false
90
- }
91
- },
92
- ColumnDropDownMsg::ItemDown => {
93
- self.selected += 1;
94
- if let Some(ref values) = self.values
95
- && self.selected >= values.len()
96
- {
97
- self.selected = 0;
98
- }
67
+ pub fn autocomplete(
68
+ &self,
69
+ target: HtmlInputElement,
70
+ exclude: HashSet<String>,
71
+ callback: Callback<InPlaceColumn>,
72
+ ) -> Option<()> {
73
+ let input = target.value();
74
+ let metadata = self.session.metadata();
75
+ let mut values: Vec<InPlaceColumn> = vec![];
76
+ let small_input = input.to_lowercase();
77
+ for col in metadata.get_table_columns()? {
78
+ if !exclude.contains(col) && col.to_lowercase().contains(&small_input) {
79
+ values.push(InPlaceColumn::Column(col.to_owned()));
80
+ }
81
+ }
82
+
83
+ for col in self.session.metadata().get_expression_columns() {
84
+ if !exclude.contains(col) && col.to_lowercase().contains(&small_input) {
85
+ values.push(InPlaceColumn::Column(col.to_owned()));
86
+ }
87
+ }
99
88
 
100
- true
101
- },
102
- ColumnDropDownMsg::ItemUp => {
103
- if let Some(ref values) = self.values
104
- && self.selected < 1
105
- {
106
- self.selected = values.len();
89
+ clone!(self.state, self.session, self.notify);
90
+ let target_elem: HtmlElement = target.clone().into();
91
+ let width = target.get_bounding_client_rect().width();
92
+ ApiFuture::spawn(async move {
93
+ if !exclude.contains(&input) {
94
+ let is_expr = session.validate_expr(&input).await?.is_none();
95
+ if is_expr {
96
+ values.push(InPlaceColumn::Expression(Expression::new(
97
+ None,
98
+ input.into(),
99
+ )));
107
100
  }
101
+ }
102
+
103
+ let no_results = values.is_empty();
104
+ {
105
+ let mut s = state.borrow_mut();
106
+ s.values = values;
107
+ s.selected = 0;
108
+ s.width = width;
109
+ s.on_select = Some(callback);
110
+ s.target = Some(target_elem);
111
+ s.no_results = no_results;
112
+ }
113
+ notify.emit(());
114
+ Ok(())
115
+ });
116
+
117
+ Some(())
118
+ }
108
119
 
109
- self.selected -= 1;
110
- true
111
- },
120
+ pub fn item_select(&self) {
121
+ let state = self.state.borrow();
122
+ if let Some(value) = state.values.get(state.selected)
123
+ && let Some(ref cb) = state.on_select
124
+ {
125
+ cb.emit(value.clone());
112
126
  }
113
127
  }
114
128
 
115
- fn changed(&mut self, _ctx: &Context<Self>, _old: &Self::Properties) -> bool {
116
- false
129
+ pub fn item_down(&self) {
130
+ let mut state = self.state.borrow_mut();
131
+ state.selected += 1;
132
+ if state.selected >= state.values.len() {
133
+ state.selected = 0;
134
+ }
135
+
136
+ drop(state);
137
+ self.notify.emit(());
117
138
  }
118
139
 
119
- fn view(&self, _ctx: &Context<Self>) -> Html {
120
- let body = html! {
121
- if let Some(ref values) = self.values {
122
- if !values.is_empty() {
123
- { for values
124
- .iter()
125
- .enumerate()
126
- .map(|(idx, value)| {
127
- let click = self.on_select.as_ref().unwrap().reform({
128
- let value = value.clone();
129
- move |_: MouseEvent| value.clone()
130
- });
131
-
132
- let row = match value {
133
- InPlaceColumn::Column(col) => html! {
134
- <span>{ col }</span>
135
- },
136
- InPlaceColumn::Expression(col) => html! {
137
- <span id="add-expression">{ col.name.clone() }</span>
138
- },
139
- };
140
-
141
- html! {
142
- if idx == self.selected {
143
- <span onmousedown={ click } class="selected">{ row }</span>
144
- } else {
145
- <span onmousedown={ click }>{ row }</span>
146
- }
147
- }
148
- }) }
149
- } else {
150
- <span class="no-results" />
151
- }
152
- }
140
+ pub fn item_up(&self) {
141
+ let mut state = self.state.borrow_mut();
142
+ if state.selected < 1 {
143
+ state.selected = state.values.len();
144
+ }
145
+
146
+ state.selected -= 1;
147
+ drop(state);
148
+ self.notify.emit(());
149
+ }
150
+
151
+ pub fn hide(&self) -> ApiResult<()> {
152
+ self.state.borrow_mut().target = None;
153
+ self.notify.emit(());
154
+ Ok(())
155
+ }
156
+ }
157
+
158
+ /// A portal component that renders the column dropdown. Should be placed in
159
+ /// the view of the component that creates the `ColumnDropDownElement`.
160
+ #[derive(Properties, PartialEq)]
161
+ pub struct ColumnDropDownPortalProps {
162
+ pub element: ColumnDropDownElement,
163
+ pub theme: String,
164
+ }
165
+
166
+ pub struct ColumnDropDownPortal {
167
+ _sub: Subscription,
168
+ }
169
+
170
+ impl Component for ColumnDropDownPortal {
171
+ type Message = ();
172
+ type Properties = ColumnDropDownPortalProps;
173
+
174
+ fn create(ctx: &Context<Self>) -> Self {
175
+ let link = ctx.link().clone();
176
+ let sub = ctx
177
+ .props()
178
+ .element
179
+ .notify
180
+ .add_listener(move |()| link.send_message(()));
181
+ Self { _sub: sub }
182
+ }
183
+
184
+ fn update(&mut self, _ctx: &Context<Self>, _msg: ()) -> bool {
185
+ true
186
+ }
187
+
188
+ fn view(&self, ctx: &Context<Self>) -> Html {
189
+ let state = ctx.props().element.state.borrow();
190
+ let target = state.target.clone();
191
+ let on_close = {
192
+ let element = ctx.props().element.clone();
193
+ Callback::from(move |()| {
194
+ let _ = element.hide();
195
+ })
153
196
  };
154
197
 
155
- let position = format!(
156
- ":host{{min-width:{}px;max-width:{}px}}",
157
- self.width, self.width
158
- );
198
+ if target.is_some() {
199
+ let values = state.values.clone();
200
+ let selected = state.selected;
201
+ let width = state.width;
202
+ let on_select = state.on_select.clone();
203
+ drop(state);
159
204
 
160
- html! { <><style>{ CSS }</style><style>{ position }</style>{ body }</> }
205
+ html! {
206
+ <PortalModal
207
+ tag_name="perspective-dropdown"
208
+ {target}
209
+ own_focus=false
210
+ {on_close}
211
+ theme={ctx.props().theme.clone()}
212
+ >
213
+ <ColumnDropDownView {values} {selected} {width} {on_select} />
214
+ </PortalModal>
215
+ }
216
+ } else {
217
+ html! {}
218
+ }
161
219
  }
162
220
  }
221
+
222
+ /// Pure view component for the column dropdown content.
223
+ #[derive(Properties, PartialEq)]
224
+ struct ColumnDropDownViewProps {
225
+ values: Vec<InPlaceColumn>,
226
+ selected: usize,
227
+ width: f64,
228
+ on_select: Option<Callback<InPlaceColumn>>,
229
+ }
230
+
231
+ #[function_component]
232
+ fn ColumnDropDownView(props: &ColumnDropDownViewProps) -> Html {
233
+ let body = html! {
234
+ if !props.values.is_empty() {
235
+ { for props.values
236
+ .iter()
237
+ .enumerate()
238
+ .map(|(idx, value)| {
239
+ let click = props.on_select.as_ref().unwrap().reform({
240
+ let value = value.clone();
241
+ move |_: MouseEvent| value.clone()
242
+ });
243
+
244
+ let row = match value {
245
+ InPlaceColumn::Column(col) => html! {
246
+ <span>{ col }</span>
247
+ },
248
+ InPlaceColumn::Expression(col) => html! {
249
+ <span id="add-expression"><span class="icon" />{ col.name.clone() }</span>
250
+ },
251
+ };
252
+
253
+ html! {
254
+ if idx == props.selected {
255
+ <span onmousedown={click} class="selected">{ row }</span>
256
+ } else {
257
+ <span onmousedown={click}>{ row }</span>
258
+ }
259
+ }
260
+ }) }
261
+ } else {
262
+ <span class="no-results" />
263
+ }
264
+ };
265
+
266
+ let position = format!(
267
+ ":host{{min-width:{}px;max-width:{}px}}",
268
+ props.width, props.width
269
+ );
270
+
271
+ html! { <><style>{ CSS }</style><style>{ position }</style>{ body }</> }
272
+ }
@@ -13,7 +13,6 @@
13
13
  use std::collections::HashSet;
14
14
 
15
15
  use perspective_client::config::*;
16
- use perspective_client::utils::PerspectiveResultExt;
17
16
  use perspective_js::utils::ApiFuture;
18
17
  use web_sys::*;
19
18
  use yew::prelude::*;
@@ -21,19 +20,18 @@ use yew::prelude::*;
21
20
  use super::InPlaceColumn;
22
21
  use super::aggregate_selector::*;
23
22
  use super::expr_edit_button::*;
24
- use crate::PerspectiveProperties;
23
+ use crate::components::column_dropdown::ColumnDropDownElement;
25
24
  use crate::components::column_selector::{EmptyColumn, InvalidColumn};
26
25
  use crate::components::type_icon::TypeIcon;
27
- use crate::custom_elements::ColumnDropDownElement;
28
26
  use crate::dragdrop::*;
29
27
  use crate::js::plugin::*;
30
- use crate::model::*;
31
28
  use crate::presentation::ColumnLocator;
32
29
  use crate::renderer::*;
33
30
  use crate::session::*;
31
+ use crate::tasks::*;
34
32
  use crate::utils::*;
35
33
 
36
- #[derive(Clone, Properties, PerspectiveProperties!)]
34
+ #[derive(Clone, Properties)]
37
35
  pub struct ActiveColumnProps {
38
36
  /// The column's index in the list.
39
37
  pub idx: usize,
@@ -64,6 +62,27 @@ pub struct ActiveColumnProps {
64
62
  /// Is this column's expression/config side panel open?
65
63
  pub is_editing: bool,
66
64
 
65
+ /// Whether this column is an expression column. Computed by the parent
66
+ /// so that changes to session metadata trigger a re-render via prop diff.
67
+ #[prop_or_default]
68
+ pub is_expression: bool,
69
+
70
+ /// Whether the expression/config edit button should be shown. Computed
71
+ /// by the parent (`is_expression || can_render_column_styles`).
72
+ #[prop_or_default]
73
+ pub show_edit_btn: bool,
74
+
75
+ /// The resolved table column type, if available. Computed by the parent
76
+ /// from session metadata so that metadata updates trigger re-renders.
77
+ #[prop_or_default]
78
+ pub col_type: Option<ColumnType>,
79
+
80
+ /// Session metadata snapshot — threaded from `SessionProps`.
81
+ pub metadata: SessionMetadataRc,
82
+
83
+ /// View config snapshot — threaded from parent as a value prop.
84
+ pub view_config: PtrEqRc<ViewConfig>,
85
+
67
86
  /// State
68
87
  pub session: Session,
69
88
  pub dragdrop: DragDrop,
@@ -71,8 +90,16 @@ pub struct ActiveColumnProps {
71
90
  }
72
91
 
73
92
  impl PartialEq for ActiveColumnProps {
74
- fn eq(&self, _rhs: &Self) -> bool {
75
- false
93
+ fn eq(&self, rhs: &Self) -> bool {
94
+ self.idx == rhs.idx
95
+ && self.name == rhs.name
96
+ && self.is_aggregated == rhs.is_aggregated
97
+ && self.is_editing == rhs.is_editing
98
+ && self.is_expression == rhs.is_expression
99
+ && self.show_edit_btn == rhs.show_edit_btn
100
+ && self.col_type == rhs.col_type
101
+ && self.metadata == rhs.metadata
102
+ && self.view_config == rhs.view_config
76
103
  }
77
104
  }
78
105
 
@@ -121,7 +148,7 @@ impl Component for ActiveColumn {
121
148
  is_render
122
149
  },
123
150
  New(InPlaceColumn::Column(col)) => {
124
- let mut view_config = ctx.props().session.get_view_config().clone();
151
+ let mut view_config = (*ctx.props().view_config).clone();
125
152
  if ctx.props().idx >= view_config.columns.len() {
126
153
  view_config.columns.push(Some(col));
127
154
  } else {
@@ -133,15 +160,21 @@ impl Component for ActiveColumn {
133
160
  ..ViewConfigUpdate::default()
134
161
  };
135
162
 
136
- ctx.props()
137
- .update_and_render(update)
138
- .map(ApiFuture::spawn)
139
- .unwrap_or_log();
163
+ {
164
+ let session = ctx.props().session.clone();
165
+ let renderer = ctx.props().renderer.clone();
166
+ if session.update_view_config(update).is_ok() {
167
+ ApiFuture::spawn(async move {
168
+ renderer.apply_pending_plugin()?;
169
+ renderer.draw(session.validate().await?.create_view()).await
170
+ });
171
+ }
172
+ }
140
173
 
141
174
  true
142
175
  },
143
176
  New(InPlaceColumn::Expression(col)) => {
144
- let mut view_config = ctx.props().session.get_view_config().clone();
177
+ let mut view_config = (*ctx.props().view_config).clone();
145
178
  if ctx.props().idx >= view_config.columns.len() {
146
179
  view_config.columns.push(Some(col.name.as_ref().to_owned()));
147
180
  } else {
@@ -155,10 +188,16 @@ impl Component for ActiveColumn {
155
188
  ..ViewConfigUpdate::default()
156
189
  };
157
190
 
158
- ctx.props()
159
- .update_and_render(update)
160
- .map(ApiFuture::spawn)
161
- .unwrap_or_log();
191
+ {
192
+ let session = ctx.props().session.clone();
193
+ let renderer = ctx.props().renderer.clone();
194
+ if session.update_view_config(update).is_ok() {
195
+ ApiFuture::spawn(async move {
196
+ renderer.apply_pending_plugin()?;
197
+ renderer.draw(session.validate().await?.create_view()).await
198
+ });
199
+ }
200
+ }
162
201
 
163
202
  true
164
203
  },
@@ -234,7 +273,7 @@ impl Component for ActiveColumn {
234
273
  })
235
274
  .collect();
236
275
 
237
- let col_type = ctx.props().get_table_type(&ctx.props().name);
276
+ let col_type = ctx.props().col_type;
238
277
  match (name, col_type) {
239
278
  ((label, ColumnState::Empty), _) => {
240
279
  classes.push("empty-named");
@@ -242,8 +281,7 @@ impl Component for ActiveColumn {
242
281
  let on_select = ctx.link().callback(ActiveColumnMsg::New);
243
282
  let exclude = ctx
244
283
  .props()
245
- .session
246
- .get_view_config()
284
+ .view_config
247
285
  .columns
248
286
  .iter()
249
287
  .flatten()
@@ -292,7 +330,7 @@ impl Component for ActiveColumn {
292
330
  }))
293
331
  };
294
332
 
295
- let ondragend = &ctx.props().ondragend.reform(|_| {});
333
+ let ondragend = &ctx.props().ondragend.reform(|_| ());
296
334
  let ondragstart = ctx.link().callback({
297
335
  let event_name = name.to_owned();
298
336
  let dragdrop = ctx.props().dragdrop.clone();
@@ -312,17 +350,12 @@ impl Component for ActiveColumn {
312
350
  .link()
313
351
  .callback(|event: MouseEvent| MouseEnter(event.which() == 0));
314
352
 
315
- let is_expression = ctx.props().session.metadata().is_column_expression(&name);
353
+ let is_expression = ctx.props().is_expression;
354
+ let show_edit_btn = ctx.props().show_edit_btn;
316
355
  let mut class = ctx.props().renderer.metadata().mode.css();
317
356
  if is_required {
318
357
  class.push("required");
319
358
  };
320
-
321
- let can_render_styles = ctx
322
- .props()
323
- .can_render_column_styles(&name)
324
- .unwrap_or_default();
325
- let show_edit_btn = is_expression || can_render_styles;
326
359
  html! {
327
360
  <div
328
361
  class={outer_classes}
@@ -342,11 +375,14 @@ impl Component for ActiveColumn {
342
375
  {ondragend}
343
376
  >
344
377
  <div class="column-selector-column-border">
378
+ <span class="drag-handle icon" />
345
379
  <TypeIcon ty={col_type} />
346
380
  if ctx.props().is_aggregated {
347
381
  <AggregateSelector
348
382
  column={name.clone()}
349
383
  aggregate={ctx.props().get_aggregate(&name)}
384
+ view_config={ctx.props().view_config.clone()}
385
+ metadata={ctx.props().metadata.clone()}
350
386
  renderer={&ctx.props().renderer}
351
387
  session={&ctx.props().session}
352
388
  />
@@ -359,14 +395,13 @@ impl Component for ActiveColumn {
359
395
  if !ctx.props().is_aggregated {
360
396
  <span class="column-selector--spacer" />
361
397
  }
362
- if show_edit_btn {
363
- <ExprEditButton
364
- name={name.clone()}
365
- on_open_expr_panel={&ctx.props().on_open_expr_panel}
366
- {is_expression}
367
- is_editing={ctx.props().is_editing}
368
- />
369
- }
398
+ <ExprEditButton
399
+ name={name.clone()}
400
+ on_open_expr_panel={&ctx.props().on_open_expr_panel}
401
+ {is_expression}
402
+ is_disabled={!show_edit_btn}
403
+ is_editing={ctx.props().is_editing}
404
+ />
370
405
  </div>
371
406
  </div>
372
407
  </div>
@@ -390,27 +425,6 @@ impl Component for ActiveColumn {
390
425
  }
391
426
 
392
427
  impl ActiveColumnProps {
393
- fn get_name(&self, defn: &ActiveColumnState) -> Option<String> {
394
- match &defn.state {
395
- ActiveColumnStateData::DragOver => Some(self.dragdrop.get_drag_column().unwrap()),
396
- ActiveColumnStateData::Column(name) => Some(name.to_owned()),
397
- ActiveColumnStateData::Required => None,
398
- ActiveColumnStateData::Invalid => None,
399
- }
400
- }
401
-
402
- fn get_table_type(&self, defn: &ActiveColumnState) -> Option<ColumnType> {
403
- self.get_name(defn)
404
- .as_ref()
405
- .and_then(|x| self.session.metadata().get_column_table_type(x))
406
- }
407
-
408
- fn _get_view_type(&self, defn: &ActiveColumnState) -> Option<ColumnType> {
409
- self.get_name(defn)
410
- .as_ref()
411
- .and_then(|x| self.session.metadata().get_column_view_type(x))
412
- }
413
-
414
428
  /// Remove an active column from `columns`, or alternatively make this
415
429
  /// column the only column in `columns` if the shift key is set (via the
416
430
  /// `shift` flag).
@@ -420,7 +434,7 @@ impl ActiveColumnProps {
420
434
  /// with respect to `columns`.
421
435
  /// - `shift` whether to toggle or select this column.
422
436
  fn deactivate_column(&self, name: String, shift: bool) {
423
- let mut columns = self.session.get_view_config().columns.clone();
437
+ let mut columns = self.view_config.columns.clone();
424
438
  let max_cols = self
425
439
  .renderer
426
440
  .metadata()
@@ -457,7 +471,7 @@ impl ActiveColumnProps {
457
471
  }
458
472
 
459
473
  fn get_aggregate(&self, name: &str) -> Option<Aggregate> {
460
- self.session.get_view_config().aggregates.get(name).cloned()
474
+ self.view_config.aggregates.get(name).cloned()
461
475
  }
462
476
 
463
477
  fn apply_columns(&self, columns: Vec<Option<String>>) {
@@ -466,8 +480,13 @@ impl ActiveColumnProps {
466
480
  ..ViewConfigUpdate::default()
467
481
  };
468
482
 
469
- self.update_and_render(config)
470
- .map(ApiFuture::spawn)
471
- .unwrap_or_log();
483
+ if self.session.update_view_config(config).is_ok() {
484
+ let session = self.session.clone();
485
+ let renderer = self.renderer.clone();
486
+ ApiFuture::spawn(async move {
487
+ renderer.apply_pending_plugin()?;
488
+ renderer.draw(session.validate().await?.create_view()).await
489
+ });
490
+ }
472
491
  }
473
492
  }
@@ -58,6 +58,7 @@ pub fn AddExpressionButton(p: &AddExpressionButtonProps) -> Html {
58
58
 
59
59
  html! {
60
60
  <div id="add-expression" data-index="-1" {class} {onmouseover} {onmouseout} {onmousedown}>
61
+ <span class="icon" />
61
62
  <span id="add-expression-button" />
62
63
  </div>
63
64
  }