@perspective-dev/viewer 4.0.1 → 4.1.1

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 (183) 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/dracula.css +1 -1
  4. package/dist/css/gruvbox-dark.css +1 -1
  5. package/dist/css/gruvbox.css +1 -1
  6. package/dist/css/icons.css +1 -1
  7. package/dist/css/monokai.css +1 -1
  8. package/dist/css/pro-dark.css +1 -1
  9. package/dist/css/pro.css +1 -1
  10. package/dist/css/solarized-dark.css +1 -1
  11. package/dist/css/solarized.css +1 -1
  12. package/dist/css/themes.css +1 -1
  13. package/dist/css/vaporwave.css +1 -1
  14. package/dist/esm/extensions.d.ts +23 -2
  15. package/dist/esm/perspective-viewer.d.ts +2 -7
  16. package/dist/esm/perspective-viewer.inline.js +2 -2
  17. package/dist/esm/perspective-viewer.inline.js.map +4 -4
  18. package/dist/esm/perspective-viewer.js +2 -2
  19. package/dist/esm/perspective-viewer.js.map +4 -4
  20. package/dist/esm/plugin.d.ts +1 -1
  21. package/dist/esm/ts-rs/ViewerConfigUpdate.d.ts +1 -0
  22. package/dist/wasm/perspective-viewer.d.ts +218 -46
  23. package/dist/wasm/perspective-viewer.js +1242 -753
  24. package/dist/wasm/perspective-viewer.wasm +0 -0
  25. package/dist/wasm/perspective-viewer.wasm.d.ts +38 -19
  26. package/package.json +1 -1
  27. package/src/less/containers/scroll-panel.less +0 -1
  28. package/src/less/plugin-selector.less +15 -5
  29. package/src/less/status-bar.less +75 -27
  30. package/src/less/viewer.less +140 -58
  31. package/src/rust/components/column_dropdown.rs +21 -21
  32. package/src/rust/components/column_selector/active_column.rs +131 -120
  33. package/src/rust/components/column_selector/add_expression_button.rs +5 -0
  34. package/src/rust/components/column_selector/aggregate_selector.rs +8 -4
  35. package/src/rust/components/column_selector/config_selector.rs +170 -161
  36. package/src/rust/components/column_selector/empty_column.rs +16 -11
  37. package/src/rust/components/column_selector/{expression_toolbar.rs → expr_edit_button.rs} +7 -0
  38. package/src/rust/components/column_selector/filter_column.rs +195 -194
  39. package/src/rust/components/column_selector/inactive_column.rs +82 -67
  40. package/src/rust/components/column_selector/pivot_column.rs +16 -11
  41. package/src/rust/components/column_selector/sort_column.rs +9 -7
  42. package/src/rust/components/column_selector.rs +42 -37
  43. package/src/rust/components/column_settings_sidebar/save_settings.rs +3 -1
  44. package/src/rust/components/column_settings_sidebar/style_tab/agg_depth_selector.rs +58 -0
  45. package/src/rust/components/column_settings_sidebar/style_tab/symbol/row_selector.rs +6 -6
  46. package/src/rust/components/column_settings_sidebar/style_tab/symbol/symbol_pairs.rs +2 -94
  47. package/src/rust/components/column_settings_sidebar/style_tab/symbol/symbol_pairs_item.rs +111 -0
  48. package/src/rust/components/column_settings_sidebar/style_tab/symbol.rs +3 -3
  49. package/src/rust/components/column_settings_sidebar/style_tab.rs +23 -83
  50. package/src/rust/components/{column_settings_sidebar/sidebar.rs → column_settings_sidebar.rs} +198 -171
  51. package/src/rust/components/containers/dragdrop_list.rs +20 -20
  52. package/src/rust/components/containers/dropdown_menu.rs +4 -6
  53. package/src/rust/components/containers/mod.rs +1 -4
  54. package/src/rust/components/containers/scroll_panel.rs +80 -80
  55. package/src/rust/components/containers/scroll_panel_item.rs +36 -36
  56. package/src/rust/components/containers/select.rs +46 -44
  57. package/src/rust/components/containers/sidebar.rs +3 -19
  58. package/src/rust/components/{column_settings_sidebar/style_tab/symbol/symbol_config.rs → containers/sidebar_close_button.rs} +15 -9
  59. package/src/rust/components/containers/split_panel.rs +212 -200
  60. package/src/rust/components/containers/tab_list.rs +11 -11
  61. package/src/rust/components/copy_dropdown.rs +22 -25
  62. package/src/rust/components/datetime_column_style/custom.rs +19 -19
  63. package/src/rust/components/datetime_column_style/simple.rs +13 -14
  64. package/src/rust/components/datetime_column_style.rs +75 -76
  65. package/src/rust/components/editable_header.rs +18 -14
  66. package/src/rust/components/empty_row.rs +5 -5
  67. package/src/rust/components/export_dropdown.rs +42 -42
  68. package/src/rust/components/expression_editor.rs +25 -19
  69. package/src/rust/components/filter_dropdown.rs +22 -22
  70. package/src/rust/components/font_loader.rs +11 -9
  71. package/src/rust/components/form/code_editor.rs +106 -105
  72. package/src/rust/components/form/color_range_selector.rs +14 -12
  73. package/src/rust/components/form/color_selector.rs +3 -1
  74. package/src/rust/components/form/debug.rs +95 -94
  75. package/src/rust/components/form/highlight.rs +5 -3
  76. package/src/rust/components/form/mod.rs +3 -2
  77. package/src/rust/components/form/optional_field.rs +2 -2
  78. package/src/rust/components/form/{select_field.rs → select_enum_field.rs} +1 -46
  79. package/src/rust/components/form/select_value_field.rs +64 -0
  80. package/src/rust/components/function_dropdown.rs +21 -21
  81. package/src/rust/components/main_panel.rs +219 -0
  82. package/src/rust/components/mod.rs +6 -6
  83. package/src/rust/components/modal.rs +42 -42
  84. package/src/rust/components/number_column_style.rs +34 -88
  85. package/src/rust/components/plugin_selector.rs +22 -25
  86. package/src/rust/components/render_warning.rs +9 -6
  87. package/src/rust/components/settings_panel.rs +82 -0
  88. package/src/rust/components/status_bar.rs +250 -146
  89. package/src/rust/components/status_bar_counter.rs +26 -119
  90. package/src/rust/components/status_indicator.rs +95 -79
  91. package/src/rust/components/string_column_style.rs +45 -45
  92. package/src/rust/components/style/style_provider.rs +1 -15
  93. package/src/rust/components/style_controls/number_string_format/digits_section.rs +1 -1
  94. package/src/rust/components/style_controls/number_string_format/misc_section.rs +1 -1
  95. package/src/rust/components/style_controls/number_string_format/style_section.rs +1 -1
  96. package/src/rust/components/style_controls/number_string_format.rs +45 -46
  97. package/src/rust/components/type_icon.rs +14 -11
  98. package/src/rust/components/viewer.rs +241 -384
  99. package/src/rust/config/columns_config.rs +2 -2
  100. package/src/rust/config/datetime_column_style.rs +1 -6
  101. package/src/rust/config/mod.rs +1 -0
  102. package/src/rust/config/number_column_style.rs +0 -6
  103. package/src/rust/config/number_string_format.rs +27 -4
  104. package/src/rust/config/viewer_config.rs +27 -167
  105. package/src/rust/custom_elements/copy_dropdown.rs +14 -6
  106. package/src/rust/custom_elements/export_dropdown.rs +15 -7
  107. package/src/rust/custom_elements/filter_dropdown.rs +4 -4
  108. package/src/rust/custom_elements/mod.rs +3 -0
  109. package/src/rust/custom_elements/viewer.rs +367 -169
  110. package/src/rust/custom_events.rs +55 -32
  111. package/src/rust/dragdrop.rs +4 -24
  112. package/src/rust/exprtk/cursor.rs +10 -1
  113. package/src/rust/exprtk/mod.rs +2 -0
  114. package/src/rust/exprtk/tokenize.rs +20 -3
  115. package/src/rust/js/clipboard.rs +2 -2
  116. package/src/rust/js/mimetype.rs +2 -7
  117. package/src/rust/js/mod.rs +0 -1
  118. package/src/rust/js/plugin.rs +7 -0
  119. package/src/rust/lib.rs +18 -5
  120. package/src/rust/model/column_locator.rs +82 -0
  121. package/src/rust/model/columns_iter_set.rs +1 -0
  122. package/src/rust/model/copy_export.rs +50 -14
  123. package/src/rust/model/edit_expression.rs +2 -5
  124. package/src/rust/model/eject.rs +41 -0
  125. package/src/rust/model/get_viewer_config.rs +4 -28
  126. package/src/rust/model/intersection_observer.rs +20 -8
  127. package/src/rust/model/mod.rs +11 -4
  128. package/src/rust/model/plugin_column_styles.rs +0 -31
  129. package/src/rust/model/reset_all.rs +38 -0
  130. package/src/rust/model/resize_observer.rs +34 -7
  131. package/src/rust/model/restore_and_render.rs +12 -7
  132. package/src/rust/{utils/scope.rs → model/send_plugin_config.rs} +32 -35
  133. package/src/rust/model/structural.rs +194 -23
  134. package/src/rust/model/update_and_render.rs +14 -4
  135. package/src/rust/{model/create_col.rs → presentation/column_locator.rs} +73 -42
  136. package/src/rust/{utils/wasm_abi.rs → presentation/sheets.rs} +54 -40
  137. package/src/rust/presentation.rs +60 -119
  138. package/src/rust/renderer/activate.rs +20 -5
  139. package/src/rust/renderer/limits.rs +0 -149
  140. package/src/rust/renderer/render_timer.rs +1 -1
  141. package/src/rust/renderer.rs +34 -18
  142. package/src/rust/root.rs +50 -0
  143. package/src/rust/session/column_defaults_update.rs +4 -4
  144. package/src/rust/session/drag_drop_update.rs +1 -1
  145. package/src/rust/session/metadata.rs +3 -17
  146. package/src/rust/session/replace_expression_update.rs +1 -2
  147. package/src/rust/session.rs +162 -82
  148. package/src/rust/utils/browser/blob.rs +16 -2
  149. package/src/rust/utils/browser/download.rs +1 -0
  150. package/src/rust/{components/column_settings_sidebar/mod.rs → utils/browser/dragdrop.rs} +14 -5
  151. package/src/rust/utils/browser/mod.rs +8 -4
  152. package/src/rust/utils/browser/selection.rs +5 -0
  153. package/src/rust/utils/custom_element.rs +28 -13
  154. package/src/rust/utils/datetime.rs +5 -0
  155. package/src/rust/utils/debounce.rs +7 -1
  156. package/src/rust/utils/hooks/use_async_callback.rs +7 -17
  157. package/src/rust/utils/mod.rs +28 -40
  158. package/src/rust/utils/number_format.rs +6 -5
  159. package/src/rust/utils/pubsub.rs +15 -10
  160. package/src/rust/utils/weak_scope.rs +11 -1
  161. package/src/svg/bookmark-icon.svg +4 -0
  162. package/src/svg/drag-handle copy.svg +10 -0
  163. package/src/svg/drawer-tab-hover.svg +5 -7
  164. package/src/svg/drawer-tab-invert-hover.svg +4 -8
  165. package/src/svg/drawer-tab-invert.svg +4 -7
  166. package/src/svg/drawer-tab.svg +4 -6
  167. package/src/svg/status_ok.svg +24 -24
  168. package/src/ts/extensions.ts +51 -3
  169. package/src/ts/perspective-viewer.ts +2 -14
  170. package/src/ts/plugin.ts +1 -1
  171. package/src/ts/ts-rs/ViewerConfigUpdate.ts +1 -1
  172. package/src/rust/components/column_settings_sidebar/style_tab/column_style.rs +0 -177
  173. package/src/rust/components/containers/tests/mod.rs +0 -11
  174. package/src/rust/components/containers/tests/split_panel.rs +0 -91
  175. package/src/rust/js/testing.rs +0 -149
  176. package/src/rust/utils/tee.rs +0 -88
  177. /package/dist/wasm/snippets/{perspective-viewer-9a89352df1552d2b → perspective-viewer-11a3c51b6310ee99}/inline0.js +0 -0
  178. /package/dist/wasm/snippets/{perspective-viewer-9a89352df1552d2b → perspective-viewer-11a3c51b6310ee99}/inline1.js +0 -0
  179. /package/dist/wasm/snippets/{perspective-viewer-9a89352df1552d2b → perspective-viewer-11a3c51b6310ee99}/inline2.js +0 -0
  180. /package/dist/wasm/snippets/{perspective-viewer-9a89352df1552d2b → perspective-viewer-11a3c51b6310ee99}/inline3.js +0 -0
  181. /package/dist/wasm/snippets/{perspective-viewer-9a89352df1552d2b → perspective-viewer-11a3c51b6310ee99}/inline4.js +0 -0
  182. /package/src/rust/components/{style_controls.rs → style_controls/mod.rs} +0 -0
  183. /package/src/rust/{components/containers → config}/kvpair.rs +0 -0
@@ -16,6 +16,7 @@ use std::rc::Rc;
16
16
  use chrono::{Datelike, NaiveDate, TimeZone, Utc};
17
17
  use perspective_client::config::*;
18
18
  use perspective_client::utils::PerspectiveResultExt;
19
+ use perspective_js::utils::ApiFuture;
19
20
  use wasm_bindgen::JsCast;
20
21
  use web_sys::*;
21
22
  use yew::prelude::*;
@@ -29,30 +30,17 @@ use crate::dragdrop::*;
29
30
  use crate::model::*;
30
31
  use crate::renderer::*;
31
32
  use crate::session::*;
32
- use crate::utils::{posix_to_utc_str, str_to_utc_posix};
33
+ use crate::utils::*;
33
34
  use crate::*;
34
35
 
35
- /// A control for a single filter condition.
36
- pub struct FilterColumn {
37
- input: String,
38
- input_ref: NodeRef,
39
- filter_ops: Rc<Vec<SelectItem<String>>>,
40
- }
41
-
42
- #[derive(Debug)]
43
- pub enum FilterColumnMsg {
44
- FilterInput((usize, String), String),
45
- Close,
46
- FilterOpSelect(String),
47
- FilterKeyDown(u32),
48
- }
49
-
50
- #[derive(Properties, Clone)]
36
+ #[derive(Clone, Properties, PerspectiveProperties!)]
51
37
  pub struct FilterColumnProps {
52
38
  pub filter: Filter,
53
39
  pub idx: usize,
54
40
  pub filter_dropdown: FilterDropDownElement,
55
41
  pub on_keydown: Callback<String>,
42
+
43
+ // State
56
44
  pub session: Session,
57
45
  pub renderer: Renderer,
58
46
  pub dragdrop: DragDrop,
@@ -64,8 +52,6 @@ impl PartialEq for FilterColumnProps {
64
52
  }
65
53
  }
66
54
 
67
- derive_model!(Renderer, Session for FilterColumnProps);
68
-
69
55
  impl DragDropListItemProps for FilterColumnProps {
70
56
  type Item = Filter;
71
57
 
@@ -74,179 +60,42 @@ impl DragDropListItemProps for FilterColumnProps {
74
60
  }
75
61
  }
76
62
 
77
- impl FilterColumnProps {
78
- /// Does this filter item get a "suggestions" auto-complete modal?
79
- fn is_suggestable(&self) -> bool {
80
- // TODO This needs to be moved to Features API. Or ... we just do this
81
- // all string column type filters, or otherwise "fix" this in the UI?
82
- (self.filter.op() == "=="
83
- || self.filter.op() == "!="
84
- || self.filter.op() == "in"
85
- || self.filter.op() == "not in")
86
- && self.get_filter_type() == Some(ColumnType::String)
87
- }
88
-
89
- /// Get this filter's type, e.g. the type of the column.
90
- fn get_filter_type(&self) -> Option<ColumnType> {
91
- self.session
92
- .metadata()
93
- .get_column_table_type(self.filter.column())
94
- }
95
-
96
- // Get the string value, suitable for the `value` field of a `FilterColumns`'s
97
- // `<input>`.
98
- fn get_filter_input(&self) -> Option<String> {
99
- let filter_type = self.get_filter_type()?;
100
- match (&filter_type, &self.filter.term()) {
101
- (ColumnType::Date, FilterTerm::Scalar(Scalar::Float(x))) => {
102
- if *x > 0_f64 {
103
- Some(
104
- Utc.timestamp_opt(*x as i64 / 1000, (*x as u32 % 1000) * 1000)
105
- .earliest()?
106
- .format("%Y-%m-%d")
107
- .to_string(),
108
- )
109
- } else {
110
- None
111
- }
112
- },
113
- (ColumnType::Datetime, FilterTerm::Scalar(Scalar::Float(x))) => {
114
- posix_to_utc_str(*x).ok()
115
- },
116
- (ColumnType::Boolean, FilterTerm::Scalar(Scalar::Bool(x))) => {
117
- Some((if *x { "true" } else { "false" }).to_owned())
118
- },
119
- (ColumnType::Boolean, _) => Some("true".to_owned()),
120
- (_, x) => Some(format!("{x}")),
121
- }
122
- }
123
-
124
- /// Get the allowed `FilterOp`s for this filter.
125
- fn get_filter_ops(&self, col_type: ColumnType) -> Option<Vec<String>> {
126
- let metadata = self.session.metadata();
127
- let features = metadata.get_features()?;
128
- features
129
- .filter_ops
130
- .get(&(col_type as u32))
131
- .map(|x| x.options.clone())
132
- }
133
-
134
- /// Update the filter comparison operator.
135
- ///
136
- /// # Arguments
137
- /// - `op` The new `FilterOp`.
138
- fn update_filter_op(&self, op: String) {
139
- let mut filter = self.session.get_view_config().filter.clone();
140
- let filter_column = &mut filter.get_mut(self.idx).expect("Filter on no column");
141
- *filter_column.op_mut() = op;
142
- let update = ViewConfigUpdate {
143
- filter: Some(filter),
144
- ..ViewConfigUpdate::default()
145
- };
146
-
147
- self.update_and_render(update)
148
- .map(ApiFuture::spawn)
149
- .unwrap_or_log();
150
- }
151
-
152
- /// Update the filter value from the string input read from the DOM.
153
- ///
154
- /// # Arguments
155
- /// - `val` The new filter value.
156
- fn update_filter_input(&self, val: String) {
157
- let mut filter = self.session.get_view_config().filter.clone();
158
- let filter_column = &mut filter.get_mut(self.idx).expect("Filter on no column");
159
-
160
- // TODO This belongs in the Features API.
161
- let filter_input = if filter_column.op() == "in" || filter_column.op() == "not in" {
162
- Some(FilterTerm::Array(
163
- val.split(',')
164
- .map(|x| Scalar::String(x.trim().to_owned()))
165
- .collect(),
166
- ))
167
- } else {
168
- match self.get_filter_type() {
169
- Some(ColumnType::String) => Some(FilterTerm::Scalar(Scalar::String(val))),
170
- Some(ColumnType::Integer) => {
171
- if val.is_empty() {
172
- None
173
- } else if let Ok(num) = val.parse::<f64>() {
174
- Some(FilterTerm::Scalar(Scalar::Float(num.floor())))
175
- } else {
176
- None
177
- }
178
- },
179
- Some(ColumnType::Float) => {
180
- if val.is_empty() {
181
- None
182
- } else if let Ok(num) = val.parse::<f64>() {
183
- Some(FilterTerm::Scalar(Scalar::Float(num)))
184
- } else {
185
- None
186
- }
187
- },
188
- Some(ColumnType::Date) => match NaiveDate::parse_from_str(&val, "%Y-%m-%d") {
189
- Ok(ref posix) => Some(FilterTerm::Scalar(Scalar::String(format!(
190
- "{:0>4}-{:0>2}-{:0>2}",
191
- posix.year(),
192
- posix.month(),
193
- posix.day(),
194
- )))),
195
- _ => None,
196
- },
197
- Some(ColumnType::Datetime) => match str_to_utc_posix(&val) {
198
- Ok(x) => Some(FilterTerm::Scalar(Scalar::Float(x))),
199
- _ => None,
200
- },
201
- Some(ColumnType::Boolean) => Some(FilterTerm::Scalar(match val.as_str() {
202
- "true" => Scalar::Bool(true),
203
- _ => Scalar::Bool(false),
204
- })),
205
-
206
- // shouldn't be reachable ..
207
- _ => None,
208
- }
209
- };
210
-
211
- if let Some(input) = filter_input
212
- && &input != filter_column.term()
213
- {
214
- *filter_column.term_mut() = input;
215
- let update = ViewConfigUpdate {
216
- filter: Some(filter),
217
- ..ViewConfigUpdate::default()
218
- };
219
-
220
- self.update_and_render(update)
221
- .map(ApiFuture::spawn)
222
- .unwrap_or_log();
223
- }
224
- }
63
+ #[derive(Debug)]
64
+ pub enum FilterColumnMsg {
65
+ FilterInput((usize, String), String),
66
+ Close,
67
+ FilterOpSelect(String),
68
+ FilterKeyDown(u32),
225
69
  }
226
70
 
227
- type FilterOpSelector = Select<String>;
71
+ /// A control for a single filter condition.
72
+ pub struct FilterColumn {
73
+ input: String,
74
+ input_ref: NodeRef,
75
+ filter_ops: Rc<Vec<SelectItem<String>>>,
76
+ }
228
77
 
229
78
  impl Component for FilterColumn {
230
79
  type Message = FilterColumnMsg;
231
80
  type Properties = FilterColumnProps;
232
81
 
233
82
  fn create(ctx: &Context<Self>) -> Self {
234
- // css!(ctx, "filter-item");
235
- let input = ctx
83
+ let input_ref = NodeRef::default();
84
+ let mut this = Self {
85
+ input: "".to_string(),
86
+ input_ref,
87
+ filter_ops: Rc::default(),
88
+ };
89
+
90
+ let col_type = ctx.props().get_current_filter_type();
91
+ this.input = ctx
236
92
  .props()
237
93
  .get_filter_input()
238
94
  .unwrap_or_else(|| "".to_owned());
239
- let input_ref = NodeRef::default();
240
- let col_type = ctx.props().get_filter_type();
241
- if col_type == Some(ColumnType::Boolean) {
242
- ctx.props().update_filter_input(input.clone());
243
- }
244
95
 
245
- let filter_ops = Rc::new(
96
+ this.filter_ops = Rc::new(
246
97
  maybe! {
247
- Some(ctx
248
- .props()
249
- .get_filter_ops(col_type?)?
98
+ Some(get_filter_ops(ctx.props().session(), col_type?)?
250
99
  .into_iter()
251
100
  .map(SelectItem::Option)
252
101
  .collect::<Vec<_>>())
@@ -254,18 +103,18 @@ impl Component for FilterColumn {
254
103
  .unwrap_or_default(),
255
104
  );
256
105
 
257
- Self {
258
- input,
259
- input_ref,
260
- filter_ops,
106
+ if col_type == Some(ColumnType::Boolean) {
107
+ ctx.props().update_filter_input(this.input.clone());
261
108
  }
109
+
110
+ this
262
111
  }
263
112
 
264
113
  fn update(&mut self, ctx: &Context<Self>, msg: FilterColumnMsg) -> bool {
265
114
  match msg {
266
115
  FilterColumnMsg::FilterInput(column, input) => {
267
116
  let target = self.input_ref.cast::<HtmlInputElement>().unwrap();
268
- let input = if ctx.props().get_filter_type() == Some(ColumnType::Boolean) {
117
+ let input = if ctx.props().get_current_filter_type() == Some(ColumnType::Boolean) {
269
118
  if target.checked() {
270
119
  "true".to_owned()
271
120
  } else {
@@ -320,23 +169,21 @@ impl Component for FilterColumn {
320
169
  },
321
170
  FilterColumnMsg::FilterKeyDown(_) => false,
322
171
  FilterColumnMsg::FilterOpSelect(op) => {
323
- ctx.props().update_filter_op(op);
172
+ ctx.props().update_filter_op(ctx.props().idx, op);
324
173
  true
325
174
  },
326
175
  }
327
176
  }
328
177
 
329
178
  fn changed(&mut self, ctx: &Context<Self>, old: &Self::Properties) -> bool {
330
- let col_type = ctx.props().get_filter_type();
331
- let old_col_type = old.get_filter_type();
179
+ let col_type = ctx.props().get_current_filter_type();
180
+ let old_col_type = ctx.props().get_filter_type(&old.filter);
332
181
  let mut changed = false;
333
182
  if col_type != old_col_type {
334
183
  changed = true;
335
184
  self.filter_ops = Rc::new(
336
185
  maybe! {
337
- Some(ctx
338
- .props()
339
- .get_filter_ops(col_type?)?
186
+ Some(get_filter_ops(&ctx.props().session, col_type?)?
340
187
  .into_iter()
341
188
  .map(SelectItem::Option)
342
189
  .collect::<Vec<_>>())
@@ -362,9 +209,7 @@ impl Component for FilterColumn {
362
209
  .session
363
210
  .metadata()
364
211
  .get_column_table_type(&column);
365
-
366
212
  let select = ctx.link().callback(FilterColumnMsg::FilterOpSelect);
367
-
368
213
  let noderef = &self.input_ref;
369
214
  let input = ctx.link().callback({
370
215
  let column = column.clone();
@@ -505,7 +350,7 @@ impl Component for FilterColumn {
505
350
  // <TypeIcon ty={ColumnType::String} />
506
351
  <TypeIcon ty={final_col_type} />
507
352
  <span class="column_name">{ filter.column().to_owned() }</span>
508
- <FilterOpSelector
353
+ <Select<String>
509
354
  class="filterop-selector"
510
355
  is_autosize=true
511
356
  values={self.filter_ops.clone()}
@@ -514,7 +359,9 @@ impl Component for FilterColumn {
514
359
  />
515
360
  // TODO: Move this to the Features API.
516
361
  if filter.op() != "is not null" && filter.op() != "is null" {
517
- if col_type == Some(ColumnType::Boolean) { { input_elem } } else {
362
+ if col_type == Some(ColumnType::Boolean) {
363
+ { input_elem }
364
+ } else {
518
365
  <label
519
366
  class={format!("input-sizer {}", type_class)}
520
367
  data-value={format!("{}", filter.term())}
@@ -528,3 +375,157 @@ impl Component for FilterColumn {
528
375
  }
529
376
  }
530
377
  }
378
+
379
+ /// Get the allowed `FilterOp`s for this filter.
380
+ fn get_filter_ops(session: &Session, col_type: ColumnType) -> Option<Vec<String>> {
381
+ let metadata = session.metadata();
382
+ let features = metadata.get_features()?;
383
+ features
384
+ .filter_ops
385
+ .get(&(col_type as u32))
386
+ .map(|x| x.options.clone())
387
+ }
388
+
389
+ impl FilterColumnProps {
390
+ /// Does this filter item get a "suggestions" auto-complete modal?
391
+ fn is_suggestable(&self) -> bool {
392
+ // TODO This needs to be moved to Features API. Or ... we just do this
393
+ // all string column type filters, or otherwise "fix" this in the UI?
394
+ (self.filter.op() == "=="
395
+ || self.filter.op() == "!="
396
+ || self.filter.op() == "in"
397
+ || self.filter.op() == "not in")
398
+ && self.get_filter_type(&self.filter) == Some(ColumnType::String)
399
+ }
400
+
401
+ fn get_current_filter_type(&self) -> Option<ColumnType> {
402
+ self.get_filter_type(&self.filter)
403
+ }
404
+
405
+ /// Get this filter's type, e.g. the type of the column.
406
+ fn get_filter_type(&self, filter: &Filter) -> Option<ColumnType> {
407
+ self.session
408
+ .metadata()
409
+ .get_column_table_type(filter.column())
410
+ }
411
+
412
+ // Get the string value, suitable for the `value` field of a `FilterColumns`'s
413
+ // `<input>`.
414
+ fn get_filter_input(&self) -> Option<String> {
415
+ let filter_type = self.get_current_filter_type()?;
416
+ match (&filter_type, &self.filter.term()) {
417
+ (ColumnType::Date, FilterTerm::Scalar(Scalar::Float(x))) => {
418
+ if *x > 0_f64 {
419
+ Some(
420
+ Utc.timestamp_opt(*x as i64 / 1000, (*x as u32 % 1000) * 1000)
421
+ .earliest()?
422
+ .format("%Y-%m-%d")
423
+ .to_string(),
424
+ )
425
+ } else {
426
+ None
427
+ }
428
+ },
429
+ (ColumnType::Datetime, FilterTerm::Scalar(Scalar::Float(x))) => {
430
+ posix_to_utc_str(*x).ok()
431
+ },
432
+ (ColumnType::Boolean, FilterTerm::Scalar(Scalar::Bool(x))) => {
433
+ Some((if *x { "true" } else { "false" }).to_owned())
434
+ },
435
+ (ColumnType::Boolean, _) => Some("true".to_owned()),
436
+ (_, x) => Some(format!("{x}")),
437
+ }
438
+ }
439
+
440
+ /// Update the filter comparison operator.
441
+ ///
442
+ /// # Arguments
443
+ /// - `op` The new `FilterOp`.
444
+ fn update_filter_op(&self, idx: usize, op: String) {
445
+ let mut filter = self.session.get_view_config().filter.clone();
446
+ let filter_column = &mut filter.get_mut(idx).expect("Filter on no column");
447
+ *filter_column.op_mut() = op;
448
+ let update = ViewConfigUpdate {
449
+ filter: Some(filter),
450
+ ..ViewConfigUpdate::default()
451
+ };
452
+
453
+ self.update_and_render(update)
454
+ .map(ApiFuture::spawn)
455
+ .unwrap_or_log();
456
+ }
457
+
458
+ /// Update the filter value from the string input read from the DOM.
459
+ ///
460
+ /// # Arguments
461
+ /// - `val` The new filter value.
462
+ fn update_filter_input(&self, val: String) {
463
+ let mut filters = self.session.get_view_config().filter.clone();
464
+ let filter_column = &mut filters.get_mut(self.idx).expect("Filter on no column");
465
+
466
+ // TODO This belongs in the Features API.
467
+ let filter_input = if filter_column.op() == "in" || filter_column.op() == "not in" {
468
+ Some(FilterTerm::Array(
469
+ val.split(',')
470
+ .map(|x| Scalar::String(x.trim().to_owned()))
471
+ .collect(),
472
+ ))
473
+ } else {
474
+ match self.get_current_filter_type() {
475
+ Some(ColumnType::String) => Some(FilterTerm::Scalar(Scalar::String(val))),
476
+ Some(ColumnType::Integer) => {
477
+ if val.is_empty() {
478
+ None
479
+ } else if let Ok(num) = val.parse::<f64>() {
480
+ Some(FilterTerm::Scalar(Scalar::Float(num.floor())))
481
+ } else {
482
+ None
483
+ }
484
+ },
485
+ Some(ColumnType::Float) => {
486
+ if val.is_empty() {
487
+ None
488
+ } else if let Ok(num) = val.parse::<f64>() {
489
+ Some(FilterTerm::Scalar(Scalar::Float(num)))
490
+ } else {
491
+ None
492
+ }
493
+ },
494
+ Some(ColumnType::Date) => match NaiveDate::parse_from_str(&val, "%Y-%m-%d") {
495
+ Ok(ref posix) => Some(FilterTerm::Scalar(Scalar::String(format!(
496
+ "{:0>4}-{:0>2}-{:0>2}",
497
+ posix.year(),
498
+ posix.month(),
499
+ posix.day(),
500
+ )))),
501
+ _ => None,
502
+ },
503
+ Some(ColumnType::Datetime) => match str_to_utc_posix(&val) {
504
+ Ok(x) => Some(FilterTerm::Scalar(Scalar::Float(x))),
505
+ _ => None,
506
+ },
507
+ Some(ColumnType::Boolean) => Some(FilterTerm::Scalar(match val.as_str() {
508
+ "true" => Scalar::Bool(true),
509
+ _ => Scalar::Bool(false),
510
+ })),
511
+
512
+ // shouldn't be reachable ..
513
+ _ => None,
514
+ }
515
+ };
516
+
517
+ if let Some(input) = filter_input {
518
+ if &input != filter_column.term() {
519
+ *filter_column.term_mut() = input;
520
+ let update = ViewConfigUpdate {
521
+ filter: Some(filters),
522
+ ..ViewConfigUpdate::default()
523
+ };
524
+
525
+ self.update_and_render(update)
526
+ .map(ApiFuture::spawn)
527
+ .unwrap_or_log();
528
+ }
529
+ }
530
+ }
531
+ }
@@ -11,35 +11,50 @@
11
11
  // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
12
 
13
13
  use itertools::Itertools;
14
- use perspective_client::config::{ColumnType, *};
14
+ use perspective_client::config::*;
15
15
  use perspective_client::utils::PerspectiveResultExt;
16
+ use perspective_js::utils::ApiFuture;
16
17
  use web_sys::*;
17
18
  use yew::prelude::*;
18
19
 
19
- use super::expression_toolbar::*;
20
+ use super::expr_edit_button::*;
20
21
  use crate::components::type_icon::TypeIcon;
21
- use crate::components::viewer::ColumnLocator;
22
22
  use crate::dragdrop::*;
23
23
  use crate::js::plugin::*;
24
24
  use crate::model::*;
25
- use crate::presentation::Presentation;
25
+ use crate::presentation::ColumnLocator;
26
26
  use crate::renderer::*;
27
27
  use crate::session::*;
28
+ use crate::utils::*;
28
29
  use crate::*;
29
30
 
30
- #[derive(Properties, Clone)]
31
+ #[derive(Clone, Properties, PerspectiveProperties!)]
31
32
  pub struct InactiveColumnProps {
33
+ /// This column's index in its list.
32
34
  pub idx: usize,
35
+
36
+ /// Is this column visible?
33
37
  pub visible: bool,
38
+
39
+ /// Column name
34
40
  pub name: String,
35
- pub dragdrop: DragDrop,
36
- pub session: Session,
37
- pub renderer: Renderer,
38
- pub presentation: Presentation,
41
+
42
+ /// Is the expression/config panel open for this column?
39
43
  pub is_editing: bool,
44
+
45
+ /// `dragend` event`.
40
46
  pub ondragend: Callback<()>,
47
+
48
+ /// Fires when this column's select button is sclicked.
41
49
  pub onselect: Callback<()>,
50
+
51
+ /// Fires when this column's expression/config button is clicked.
42
52
  pub on_open_expr_panel: Callback<ColumnLocator>,
53
+
54
+ // State
55
+ pub dragdrop: DragDrop,
56
+ pub session: Session,
57
+ pub renderer: Renderer,
43
58
  }
44
59
 
45
60
  impl PartialEq for InactiveColumnProps {
@@ -51,61 +66,6 @@ impl PartialEq for InactiveColumnProps {
51
66
  }
52
67
  }
53
68
 
54
- derive_model!(Renderer, Session for InactiveColumnProps);
55
-
56
- impl InactiveColumnProps {
57
- /// Add a column to the active columns, which corresponds to the `columns`
58
- /// field of the `JsPerspectiveViewConfig`.
59
- ///
60
- /// # Arguments
61
- /// - `name` The name of the column to de-activate, which is a unique ID
62
- /// with respect to `columns`.
63
- /// - `shift` whether to toggle or select this column.
64
- pub fn activate_column(&self, name: String, shift: bool) {
65
- let mut columns = self.session.get_view_config().columns.clone();
66
- let max_cols = self
67
- .renderer
68
- .metadata()
69
- .names
70
- .as_ref()
71
- .map_or(0, |x| x.len());
72
-
73
- // Don't treat `None` at the end of the column list as columns, we'll refill
74
- // these later
75
- if let Some(last_filled) = columns.iter().rposition(|x| !x.is_none()) {
76
- columns.truncate(last_filled + 1);
77
-
78
- let mode = self.renderer.metadata().mode;
79
- if (mode == ColumnSelectMode::Select) ^ shift {
80
- columns.clear();
81
- } else {
82
- columns.retain(|x| x.as_ref() != Some(&name));
83
- }
84
-
85
- columns.push(Some(name));
86
- }
87
-
88
- // Do this outside the loop so errors dont just become noops
89
- self.apply_columns(
90
- columns
91
- .into_iter()
92
- .pad_using(max_cols, |_| None)
93
- .collect::<Vec<_>>(),
94
- );
95
- }
96
-
97
- fn apply_columns(&self, columns: Vec<Option<String>>) {
98
- let config = ViewConfigUpdate {
99
- columns: Some(columns),
100
- ..ViewConfigUpdate::default()
101
- };
102
-
103
- self.update_and_render(config)
104
- .map(ApiFuture::spawn)
105
- .unwrap_or_log();
106
- }
107
- }
108
-
109
69
  pub enum InactiveColumnMsg {
110
70
  ActivateColumn(bool),
111
71
  MouseEnter(bool),
@@ -114,7 +74,6 @@ pub enum InactiveColumnMsg {
114
74
 
115
75
  use InactiveColumnMsg::*;
116
76
 
117
- #[derive(Default)]
118
77
  pub struct InactiveColumn {
119
78
  add_expression_ref: NodeRef,
120
79
  mouseover: bool,
@@ -125,7 +84,10 @@ impl Component for InactiveColumn {
125
84
  type Properties = InactiveColumnProps;
126
85
 
127
86
  fn create(_ctx: &Context<Self>) -> Self {
128
- Self::default()
87
+ Self {
88
+ add_expression_ref: NodeRef::default(),
89
+ mouseover: false,
90
+ }
129
91
  }
130
92
 
131
93
  fn update(&mut self, ctx: &Context<Self>, msg: InactiveColumnMsg) -> bool {
@@ -195,9 +157,9 @@ impl Component for InactiveColumn {
195
157
  <div {class} {onmouseover} {onmouseout} data-index={ctx.props().idx.to_string()}>
196
158
  <span class={is_active_class} onmousedown={add_column} />
197
159
  <div
160
+ ref={&self.add_expression_ref}
198
161
  class="column-selector-draggable column-selector-column-title"
199
162
  draggable="true"
200
- ref={&self.add_expression_ref}
201
163
  {ondragstart}
202
164
  {ondragend}
203
165
  >
@@ -219,3 +181,56 @@ impl Component for InactiveColumn {
219
181
  }
220
182
  }
221
183
  }
184
+
185
+ impl InactiveColumnProps {
186
+ /// Add a column to the active columns, which corresponds to the `columns`
187
+ /// field of the `JsPerspectiveViewConfig`.
188
+ ///
189
+ /// # Arguments
190
+ /// - `name` The name of the column to de-activate, which is a unique ID
191
+ /// with respect to `columns`.
192
+ /// - `shift` whether to toggle or select this column.
193
+ pub fn activate_column(&self, name: String, shift: bool) {
194
+ let mut columns = self.session.get_view_config().columns.clone();
195
+ let max_cols = self
196
+ .renderer
197
+ .metadata()
198
+ .names
199
+ .as_ref()
200
+ .map_or(0, |x| x.len());
201
+
202
+ // Don't treat `None` at the end of the column list as columns, we'll refill
203
+ // these later
204
+ if let Some(last_filled) = columns.iter().rposition(|x| !x.is_none()) {
205
+ columns.truncate(last_filled + 1);
206
+
207
+ let mode = self.renderer.metadata().mode;
208
+ if (mode == ColumnSelectMode::Select) ^ shift {
209
+ columns.clear();
210
+ } else {
211
+ columns.retain(|x| x.as_ref() != Some(&name));
212
+ }
213
+
214
+ columns.push(Some(name));
215
+ }
216
+
217
+ // Do this outside the loop so errors dont just become noops
218
+ self.apply_columns(
219
+ columns
220
+ .into_iter()
221
+ .pad_using(max_cols, |_| None)
222
+ .collect::<Vec<_>>(),
223
+ );
224
+ }
225
+
226
+ fn apply_columns(&self, columns: Vec<Option<String>>) {
227
+ let config = ViewConfigUpdate {
228
+ columns: Some(columns),
229
+ ..ViewConfigUpdate::default()
230
+ };
231
+
232
+ self.update_and_render(config)
233
+ .map(ApiFuture::spawn)
234
+ .unwrap_or_log();
235
+ }
236
+ }