@perspective-dev/viewer 4.2.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 (240) 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 -0
  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/extensions.d.ts +32 -1
  23. package/dist/esm/perspective-viewer.d.ts +1 -0
  24. package/dist/esm/perspective-viewer.inline.js +2 -2
  25. package/dist/esm/perspective-viewer.inline.js.map +4 -4
  26. package/dist/esm/perspective-viewer.js +2 -2
  27. package/dist/esm/perspective-viewer.js.map +4 -4
  28. package/dist/esm/ts-rs/GroupRollupMode.d.ts +1 -0
  29. package/dist/esm/ts-rs/ViewerConfigUpdate.d.ts +2 -0
  30. package/dist/wasm/perspective-viewer.d.ts +57 -53
  31. package/dist/wasm/perspective-viewer.js +197 -164
  32. package/dist/wasm/perspective-viewer.wasm +0 -0
  33. package/dist/wasm/perspective-viewer.wasm.d.ts +17 -18
  34. package/package.json +9 -6
  35. package/src/{less/aggregate-selector.less → css/aggregate-selector.css} +23 -20
  36. package/src/css/column-dropdown.css +109 -0
  37. package/src/{less/column-selector.less → css/column-selector.css} +161 -159
  38. package/src/{less/column-settings-panel.less → css/column-settings-panel.css} +69 -59
  39. package/src/{less/column-style.less → css/column-style.css} +52 -66
  40. package/src/{less/column-symbol-attributes.less → css/column-symbol-attributes.css} +15 -14
  41. package/src/css/config-selector.css +441 -0
  42. package/src/{less/containers/dropdown-menu.less → css/containers/dropdown-menu.css} +20 -19
  43. package/src/{less/containers/pairs-list.less → css/containers/pairs-list.css} +13 -12
  44. package/src/{themes/variables.less → css/containers/scroll-panel.css} +25 -22
  45. package/src/{less/containers/split-panel.less → css/containers/split-panel.css} +15 -14
  46. package/src/{less/containers/tabs.less → css/containers/tabs.css} +17 -19
  47. package/src/css/dom/checkbox.css +102 -0
  48. package/src/css/dom/scrollbar.css +35 -0
  49. package/src/{less/dom/select.less → css/dom/select.css} +17 -18
  50. package/src/{less/empty-column.less → css/empty-column.css} +19 -18
  51. package/src/{less/expression-editor.less → css/expression-editor.css} +19 -18
  52. package/src/{less/filter-dropdown.less → css/filter-dropdown.css} +12 -11
  53. package/src/{less/filter-item.less → css/filter-item.css} +16 -15
  54. package/src/{less/form/code-editor.less → css/form/code-editor.css} +26 -30
  55. package/src/{less/form/debug.less → css/form/debug.css} +19 -18
  56. package/src/{less/function-dropdown.less → css/function-dropdown.css} +12 -11
  57. package/src/css/plugin-selector.css +261 -0
  58. package/src/{less/render-warning.less → css/render-warning.css} +18 -17
  59. package/src/{less/status-bar.less → css/status-bar.css} +156 -144
  60. package/src/css/type-icon.css +116 -0
  61. package/src/{less/viewer.less → css/viewer.css} +112 -146
  62. package/src/rust/components/column_dropdown.rs +229 -119
  63. package/src/rust/components/column_selector/active_column.rs +81 -62
  64. package/src/rust/components/column_selector/add_expression_button.rs +1 -0
  65. package/src/rust/components/column_selector/aggregate_selector.rs +25 -15
  66. package/src/rust/components/column_selector/config_selector.rs +374 -185
  67. package/src/rust/components/column_selector/empty_column.rs +2 -2
  68. package/src/rust/components/column_selector/expr_edit_button.rs +8 -2
  69. package/src/rust/components/column_selector/filter_column.rs +37 -26
  70. package/src/rust/components/column_selector/inactive_column.rs +41 -29
  71. package/src/rust/components/column_selector/invalid_column.rs +7 -18
  72. package/src/rust/components/column_selector/pivot_column.rs +21 -10
  73. package/src/rust/components/column_selector/sort_column.rs +23 -13
  74. package/src/rust/components/column_selector.rs +189 -100
  75. package/src/rust/components/column_settings_sidebar/style_tab/symbol/row_selector.rs +1 -1
  76. package/src/rust/components/column_settings_sidebar/style_tab/symbol/symbol_pairs.rs +3 -2
  77. package/src/rust/components/column_settings_sidebar/style_tab/symbol/symbol_pairs_item.rs +3 -2
  78. package/src/rust/components/column_settings_sidebar/style_tab/symbol/symbol_selector.rs +2 -3
  79. package/src/rust/components/column_settings_sidebar/style_tab/symbol.rs +7 -1
  80. package/src/rust/components/column_settings_sidebar/style_tab.rs +153 -112
  81. package/src/rust/components/column_settings_sidebar.rs +91 -53
  82. package/src/rust/components/containers/dragdrop_list.rs +29 -7
  83. package/src/rust/components/containers/scroll_panel.rs +8 -1
  84. package/src/rust/components/containers/select.rs +3 -3
  85. package/src/rust/components/containers/sidebar_close_button.rs +1 -1
  86. package/src/rust/components/containers/split_panel.rs +3 -2
  87. package/src/rust/components/containers/tab_list.rs +1 -1
  88. package/src/rust/components/copy_dropdown.rs +7 -28
  89. package/src/rust/components/datetime_column_style/custom.rs +2 -2
  90. package/src/rust/components/datetime_column_style/simple.rs +2 -2
  91. package/src/rust/components/datetime_column_style.rs +4 -2
  92. package/src/rust/components/editable_header.rs +7 -4
  93. package/src/rust/components/empty_row.rs +1 -1
  94. package/src/rust/components/export_dropdown.rs +4 -30
  95. package/src/rust/components/expression_editor.rs +19 -10
  96. package/src/rust/components/filter_dropdown.rs +246 -102
  97. package/src/rust/components/font_loader.rs +11 -28
  98. package/src/rust/components/form/code_editor.rs +17 -2
  99. package/src/rust/components/form/color_range_selector.rs +19 -6
  100. package/src/rust/components/form/debug.rs +30 -13
  101. package/src/rust/components/function_dropdown.rs +186 -113
  102. package/src/rust/components/main_panel.rs +71 -89
  103. package/src/rust/components/mod.rs +1 -1
  104. package/src/rust/components/modal.rs +7 -1
  105. package/src/rust/components/number_column_style.rs +22 -7
  106. package/src/rust/components/plugin_selector.rs +34 -92
  107. package/src/rust/components/portal.rs +274 -0
  108. package/src/rust/components/render_warning.rs +72 -123
  109. package/src/rust/components/settings_panel.rs +115 -11
  110. package/src/rust/components/status_bar.rs +222 -98
  111. package/src/rust/components/status_bar_counter.rs +8 -20
  112. package/src/rust/components/status_indicator.rs +64 -111
  113. package/src/rust/components/string_column_style.rs +2 -2
  114. package/src/rust/components/style/style_cache.rs +5 -1
  115. package/src/rust/components/viewer.rs +391 -39
  116. package/src/rust/custom_elements/copy_dropdown.rs +102 -21
  117. package/src/rust/custom_elements/export_dropdown.rs +102 -20
  118. package/src/rust/custom_elements/mod.rs +0 -7
  119. package/src/rust/custom_elements/modal.rs +7 -103
  120. package/src/rust/custom_elements/viewer.rs +99 -35
  121. package/src/rust/custom_events.rs +23 -2
  122. package/src/rust/dragdrop.rs +149 -10
  123. package/src/{less/containers/scroll-panel.less → rust/engines.rs} +15 -13
  124. package/src/rust/js/plugin.rs +20 -1
  125. package/src/rust/lib.rs +5 -4
  126. package/src/rust/presentation/props.rs +39 -0
  127. package/src/rust/presentation/sheets.rs +3 -3
  128. package/src/rust/presentation.rs +44 -8
  129. package/src/rust/renderer/limits.rs +32 -3
  130. package/src/{less/dom/scrollbar.less → rust/renderer/props.rs} +18 -19
  131. package/src/rust/renderer/registry.rs +8 -1
  132. package/src/rust/renderer.rs +83 -9
  133. package/src/rust/session/column_defaults_update.rs +18 -0
  134. package/src/rust/session/metadata.rs +23 -2
  135. package/src/rust/session/props.rs +178 -0
  136. package/src/rust/session/replace_expression_update.rs +1 -0
  137. package/src/rust/session.rs +124 -117
  138. package/src/rust/tasks/column_locator.rs +133 -0
  139. package/src/rust/{model → tasks}/columns_iter_set.rs +14 -23
  140. package/src/rust/{model → tasks}/edit_expression.rs +34 -10
  141. package/src/rust/{model → tasks}/eject.rs +2 -2
  142. package/src/rust/{model → tasks}/get_viewer_config.rs +0 -11
  143. package/src/rust/{model → tasks}/intersection_observer.rs +22 -4
  144. package/src/{less/containers/radio-list.less → rust/tasks/is_invalid_drop.rs} +21 -14
  145. package/src/rust/tasks/mod.rs +52 -0
  146. package/src/rust/{model → tasks}/plugin_column_styles.rs +69 -46
  147. package/src/rust/{model → tasks}/resize_observer.rs +39 -6
  148. package/src/rust/{model → tasks}/send_plugin_config.rs +1 -1
  149. package/src/rust/tasks/structural.rs +53 -0
  150. package/src/rust/utils/mod.rs +4 -0
  151. package/src/rust/utils/modal_position.rs +110 -0
  152. package/src/rust/utils/ptr_eq_rc.rs +74 -0
  153. package/src/rust/utils/pubsub.rs +11 -1
  154. package/src/svg/bg-pattern.png +0 -0
  155. package/src/svg/close-icon.svg +1 -1
  156. package/src/svg/expression.svg +1 -1
  157. package/src/svg/mega-menu-icons-candlestick.svg +1 -1
  158. package/src/svg/mega-menu-icons-datagrid.svg +1 -2
  159. package/src/svg/mega-menu-icons-heatmap.svg +1 -1
  160. package/src/svg/mega-menu-icons-map-scatter.svg +1 -1
  161. package/src/svg/mega-menu-icons-ohlc.svg +1 -1
  162. package/src/svg/mega-menu-icons-sunburst.svg +1 -1
  163. package/src/svg/mega-menu-icons-treemap.svg +1 -1
  164. package/src/svg/mega-menu-icons-x-bar.svg +1 -1
  165. package/src/svg/mega-menu-icons-x-y-line.svg +1 -1
  166. package/src/svg/mega-menu-icons-x-y-scatter.svg +1 -1
  167. package/src/svg/mega-menu-icons-y-area.svg +1 -1
  168. package/src/svg/mega-menu-icons-y-bar.svg +1 -1
  169. package/src/svg/mega-menu-icons-y-line.svg +1 -1
  170. package/src/svg/mega-menu-icons-y-scatter.svg +1 -1
  171. package/src/svg/radio-hover.svg +1 -1
  172. package/src/svg/radio-off.svg +1 -1
  173. package/src/svg/radio-on.svg +1 -1
  174. package/src/themes/botanical.css +157 -0
  175. package/src/themes/defaults.css +139 -0
  176. package/src/themes/dracula.css +233 -0
  177. package/src/themes/gruvbox-dark.css +255 -0
  178. package/src/themes/gruvbox.css +134 -0
  179. package/src/themes/icons.css +124 -0
  180. package/src/themes/intl/de.css +102 -0
  181. package/src/themes/intl/es.css +102 -0
  182. package/src/themes/intl/fr.css +102 -0
  183. package/src/themes/intl/ja.css +102 -0
  184. package/src/themes/intl/pt.css +102 -0
  185. package/src/themes/intl/zh.css +102 -0
  186. package/src/themes/intl.css +102 -0
  187. package/src/themes/monokai.css +233 -0
  188. package/src/themes/pro-dark.css +158 -0
  189. package/src/themes/{themes.less → pro.css} +17 -20
  190. package/src/themes/solarized-dark.css +135 -0
  191. package/src/themes/solarized.css +95 -0
  192. package/src/themes/themes.css +22 -0
  193. package/src/themes/vaporwave.css +256 -0
  194. package/src/ts/extensions.ts +73 -2
  195. package/src/ts/perspective-viewer.ts +1 -0
  196. package/src/ts/ts-rs/GroupRollupMode.ts +3 -0
  197. package/src/ts/ts-rs/ViewerConfigUpdate.ts +2 -1
  198. package/tsconfig.json +1 -0
  199. package/dist/css/variables.css +0 -0
  200. package/src/less/column-dropdown.less +0 -95
  201. package/src/less/config-selector.less +0 -363
  202. package/src/less/dom/checkbox.less +0 -100
  203. package/src/less/plugin-selector.less +0 -183
  204. package/src/less/type-icon.less +0 -68
  205. package/src/rust/components/error_message.rs +0 -56
  206. package/src/rust/custom_elements/column_dropdown.rs +0 -123
  207. package/src/rust/custom_elements/filter_dropdown.rs +0 -179
  208. package/src/rust/custom_elements/function_dropdown.rs +0 -115
  209. package/src/rust/model/column_locator.rs +0 -82
  210. package/src/rust/model/is_invalid_drop.rs +0 -36
  211. package/src/rust/model/mod.rs +0 -100
  212. package/src/rust/model/reset_all.rs +0 -38
  213. package/src/rust/model/structural.rs +0 -244
  214. package/src/themes/dracula.less +0 -101
  215. package/src/themes/gruvbox-dark.less +0 -116
  216. package/src/themes/gruvbox.less +0 -152
  217. package/src/themes/icons.less +0 -130
  218. package/src/themes/intl/de.less +0 -102
  219. package/src/themes/intl/es.less +0 -102
  220. package/src/themes/intl/fr.less +0 -102
  221. package/src/themes/intl/ja.less +0 -102
  222. package/src/themes/intl/pt.less +0 -102
  223. package/src/themes/intl/zh.less +0 -102
  224. package/src/themes/intl.less +0 -102
  225. package/src/themes/monokai.less +0 -107
  226. package/src/themes/pro-dark.less +0 -147
  227. package/src/themes/pro.less +0 -186
  228. package/src/themes/solarized-dark.less +0 -78
  229. package/src/themes/solarized.less +0 -102
  230. package/src/themes/vaporwave.less +0 -145
  231. /package/dist/wasm/snippets/{perspective-viewer-1586156e058be573 → perspective-viewer-68fef752754ffbc6}/inline0.js +0 -0
  232. /package/dist/wasm/snippets/{perspective-viewer-1586156e058be573 → perspective-viewer-68fef752754ffbc6}/inline1.js +0 -0
  233. /package/dist/wasm/snippets/{perspective-viewer-1586156e058be573 → perspective-viewer-68fef752754ffbc6}/inline2.js +0 -0
  234. /package/dist/wasm/snippets/{perspective-viewer-1586156e058be573 → perspective-viewer-68fef752754ffbc6}/inline3.js +0 -0
  235. /package/dist/wasm/snippets/{perspective-viewer-1586156e058be573 → perspective-viewer-68fef752754ffbc6}/inline4.js +0 -0
  236. /package/src/rust/{model → tasks}/copy_export.rs +0 -0
  237. /package/src/rust/{model → tasks}/export_app.rs +0 -0
  238. /package/src/rust/{model → tasks}/export_method.rs +0 -0
  239. /package/src/rust/{model → tasks}/restore_and_render.rs +0 -0
  240. /package/src/rust/{model → tasks}/update_and_render.rs +0 -0
@@ -27,7 +27,9 @@ use std::rc::Rc;
27
27
 
28
28
  pub use empty_column::*;
29
29
  pub use invalid_column::*;
30
+ use perspective_client::config::ViewConfig;
30
31
  use perspective_js::utils::ApiFuture;
32
+ pub use pivot_column::*;
31
33
  use web_sys::*;
32
34
  use yew::prelude::*;
33
35
 
@@ -38,17 +40,20 @@ use self::inactive_column::*;
38
40
  use super::containers::scroll_panel::*;
39
41
  use super::containers::split_panel::{Orientation, SplitPanel};
40
42
  use super::style::LocalStyle;
43
+ use crate::components::column_dropdown::{ColumnDropDownElement, ColumnDropDownPortal};
41
44
  use crate::components::containers::scroll_panel_item::ScrollPanelItem;
42
- use crate::custom_elements::ColumnDropDownElement;
45
+ use crate::css;
43
46
  use crate::dragdrop::*;
44
- use crate::model::*;
45
47
  use crate::presentation::ColumnLocator;
46
48
  use crate::renderer::*;
49
+ use crate::session::drag_drop_update::*;
47
50
  use crate::session::*;
51
+ use crate::tasks::{
52
+ ActiveColumnState, ActiveColumnStateData, ColumnsIteratorSet, can_render_column_styles,
53
+ };
48
54
  use crate::utils::*;
49
- use crate::*;
50
55
 
51
- #[derive(Properties, PerspectiveProperties!)]
56
+ #[derive(Properties)]
52
57
  pub struct ColumnSelectorProps {
53
58
  /// Fires when the expression/config column is open.
54
59
  pub on_open_expr_panel: Callback<ColumnLocator>,
@@ -56,29 +61,48 @@ pub struct ColumnSelectorProps {
56
61
  /// This is passed to the add_expression_button for styling.
57
62
  pub selected_column: Option<ColumnLocator>,
58
63
 
59
- /// Fires when this component is resized via the UI.
60
- #[prop_or_default]
61
- pub on_resize: Option<Rc<PubSub<()>>>,
64
+ /// Value props threaded from root's `SessionProps` / `RendererProps`.
65
+ pub has_table: Option<TableLoadState>,
66
+ pub named_column_count: usize,
67
+ pub view_config: PtrEqRc<ViewConfig>,
68
+ pub drag_column: Option<String>,
69
+
70
+ /// Cloned session metadata snapshot — threaded from `SessionProps`
71
+ /// so that metadata changes trigger re-renders via prop diffing.
72
+ pub metadata: SessionMetadataRc,
73
+
74
+ /// Selected theme name, threaded for PortalModal consumers.
75
+ pub selected_theme: Option<String>,
62
76
 
63
77
  // State
64
78
  pub session: Session,
65
79
  pub renderer: Renderer,
66
80
  pub dragdrop: DragDrop,
81
+
82
+ /// Fires when this component is resized via the UI.
83
+ #[prop_or_default]
84
+ pub on_resize: Option<Rc<PubSub<()>>>,
67
85
  }
68
86
 
69
87
  impl PartialEq for ColumnSelectorProps {
70
88
  fn eq(&self, rhs: &Self) -> bool {
71
89
  self.selected_column == rhs.selected_column
90
+ && self.has_table == rhs.has_table
91
+ && self.named_column_count == rhs.named_column_count
92
+ && self.view_config == rhs.view_config
93
+ && self.drag_column == rhs.drag_column
94
+ && self.metadata == rhs.metadata
95
+ && self.selected_theme == rhs.selected_theme
72
96
  }
73
97
  }
74
98
 
75
99
  #[derive(Debug)]
76
100
  pub enum ColumnSelectorMsg {
77
- TableLoaded,
78
- ViewCreated,
101
+ /// Triggers a plain re-render; used as `onselect`/`ondragenter` callbacks
102
+ /// from `ConfigSelector` after it mutates the view config.
103
+ Redraw,
79
104
  HoverActiveIndex(Option<usize>),
80
- Drag(DragEffect),
81
- DragEnd,
105
+ SetWidth(f64),
82
106
  Drop((String, DragTarget, DragEffect, usize)),
83
107
  }
84
108
 
@@ -87,10 +111,10 @@ use ColumnSelectorMsg::*;
87
111
  /// A `ColumnSelector` controls the `columns` field of the `ViewConfig`,
88
112
  /// deriving its options from the table columns and `ViewConfig` expressions.
89
113
  pub struct ColumnSelector {
90
- _subscriptions: [Subscription; 5],
91
- named_row_count: usize,
114
+ _subscriptions: [Subscription; 1],
92
115
  drag_container: DragDropContainer,
93
116
  column_dropdown: ColumnDropDownElement,
117
+ viewport_width: f64,
94
118
  on_reset: Rc<PubSub<()>>,
95
119
  }
96
120
 
@@ -100,44 +124,14 @@ impl Component for ColumnSelector {
100
124
 
101
125
  fn create(ctx: &Context<Self>) -> Self {
102
126
  let ColumnSelectorProps {
103
- dragdrop,
104
- renderer,
105
- session,
106
- ..
127
+ dragdrop, session, ..
107
128
  } = 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
129
 
118
130
  let drop_sub = {
119
131
  let cb = ctx.link().callback(ColumnSelectorMsg::Drop);
120
132
  dragdrop.drop_received.add_listener(cb)
121
133
  };
122
134
 
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
135
  let drag_container = DragDropContainer::new(|| {}, {
142
136
  let link = ctx.link().clone();
143
137
  move || link.send_message(ColumnSelectorMsg::HoverActiveIndex(None))
@@ -145,8 +139,8 @@ impl Component for ColumnSelector {
145
139
 
146
140
  let column_dropdown = ColumnDropDownElement::new(session.clone());
147
141
  Self {
148
- _subscriptions: [table_sub, view_sub, drop_sub, drag_sub, dragend_sub],
149
- named_row_count,
142
+ _subscriptions: [drop_sub],
143
+ viewport_width: 0f64,
150
144
  drag_container,
151
145
  column_dropdown,
152
146
  on_reset: Default::default(),
@@ -155,18 +149,10 @@ impl Component for ColumnSelector {
155
149
 
156
150
  fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
157
151
  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
152
+ Redraw => true,
153
+ SetWidth(w) => {
154
+ self.viewport_width = w;
155
+ false
170
156
  },
171
157
  HoverActiveIndex(Some(to_index)) => ctx
172
158
  .props()
@@ -177,33 +163,77 @@ impl Component for ColumnSelector {
177
163
  true
178
164
  },
179
165
  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(
166
+ let is_invalid = {
167
+ let config = &ctx.props().view_config;
168
+ let from_index = config
169
+ .columns
170
+ .iter()
171
+ .position(|x| x.as_ref() == Some(&column));
172
+
173
+ let min_cols = ctx.props().renderer.metadata().min;
174
+ let is_to_empty = !config
175
+ .columns
176
+ .get(index)
177
+ .map(|x| x.is_some())
178
+ .unwrap_or_default();
179
+
180
+ min_cols
181
+ .and_then(|x| from_index.map(|fi| fi < x))
182
+ .unwrap_or_default()
183
+ && is_to_empty
184
+ };
185
+ if !is_invalid {
186
+ let col_type = ctx
187
+ .props()
188
+ .metadata
189
+ .get_column_table_type(column.as_str())
190
+ .unwrap();
191
+
192
+ let update = ctx.props().view_config.create_drag_drop_update(
182
193
  column,
194
+ col_type,
183
195
  index,
184
196
  DragTarget::Active,
185
197
  DragEffect::Move(DragTarget::Active),
186
198
  &ctx.props().renderer.metadata(),
199
+ ctx.props().metadata.get_features().unwrap(),
187
200
  );
188
201
 
189
- if let Ok(task) = ctx.props().update_and_render(update) {
190
- ApiFuture::spawn(task);
202
+ let session = ctx.props().session.clone();
203
+ let renderer = ctx.props().renderer.clone();
204
+ if session.update_view_config(update).is_ok() {
205
+ ApiFuture::spawn(async move {
206
+ renderer.apply_pending_plugin()?;
207
+ renderer.draw(session.validate().await?.create_view()).await
208
+ });
191
209
  }
192
210
  }
193
211
 
194
212
  true
195
213
  },
196
214
  Drop((column, DragTarget::Active, effect, index)) => {
197
- let update = ctx.props().session.create_drag_drop_update(
215
+ let col_type = ctx
216
+ .props()
217
+ .metadata
218
+ .get_column_table_type(column.as_str())
219
+ .unwrap();
220
+ let update = ctx.props().view_config.create_drag_drop_update(
198
221
  column,
222
+ col_type,
199
223
  index,
200
224
  DragTarget::Active,
201
225
  effect,
202
226
  &ctx.props().renderer.metadata(),
227
+ ctx.props().metadata.get_features().unwrap(),
203
228
  );
204
229
 
205
- if let Ok(task) = ctx.props().update_and_render(update) {
206
- ApiFuture::spawn(task);
230
+ let session = ctx.props().session.clone();
231
+ let renderer = ctx.props().renderer.clone();
232
+ if session.update_view_config(update).is_ok() {
233
+ ApiFuture::spawn(async move {
234
+ renderer.apply_pending_plugin()?;
235
+ renderer.draw(session.validate().await?.create_view()).await
236
+ });
207
237
  }
208
238
 
209
239
  true
@@ -220,10 +250,29 @@ impl Component for ColumnSelector {
220
250
  dragdrop,
221
251
  ..
222
252
  } = ctx.props();
223
- let config = session.get_view_config();
253
+ let metadata = &ctx.props().metadata;
254
+
255
+ // When `config.columns` is empty but the table has columns (transient
256
+ // state during `load()` after `reset()` clears the config), fill in
257
+ // all table columns as active — matching `validate_view_config()`.
258
+ let prop_config = &ctx.props().view_config;
259
+ let config = if prop_config.columns.is_empty() {
260
+ if let Some(table_cols) = metadata.get_table_columns() {
261
+ ViewConfig {
262
+ columns: table_cols.iter().map(|c| Some(c.clone())).collect(),
263
+ ..(**prop_config).clone()
264
+ }
265
+ .into()
266
+ } else {
267
+ prop_config.clone()
268
+ }
269
+ } else {
270
+ prop_config.clone()
271
+ };
272
+
224
273
  let is_aggregated = config.is_aggregated();
225
- let columns_iter = ctx.props().column_selector_iter_set(&config);
226
- let onselect = ctx.link().callback(|()| ViewCreated);
274
+ let columns_iter = ColumnsIteratorSet::new(&config, metadata, renderer, dragdrop);
275
+ let onselect = ctx.link().callback(|()| Redraw);
227
276
  let ondragenter = ctx.link().callback(HoverActiveIndex);
228
277
  let ondragover = Callback::from(|_event: DragEvent| _event.prevent_default());
229
278
  let ondrop = Callback::from({
@@ -236,8 +285,8 @@ impl Component for ColumnSelector {
236
285
  move |_| dragdrop.notify_drag_end()
237
286
  });
238
287
 
239
- let mut active_classes = classes!();
240
- if ctx.props().dragdrop.get_drag_column().is_some() {
288
+ let mut active_classes = classes!("scrollable");
289
+ if ctx.props().drag_column.is_some() {
241
290
  active_classes.push("dragdrop-highlight");
242
291
  };
243
292
 
@@ -250,8 +299,7 @@ impl Component for ColumnSelector {
250
299
  + config.split_by.len()
251
300
  + config.filter.len()
252
301
  + config.sort.len()) as f64,
253
- session
254
- .metadata()
302
+ metadata
255
303
  .get_features()
256
304
  .map(|x| {
257
305
  let mut y = 0.0;
@@ -280,7 +328,11 @@ impl Component for ColumnSelector {
280
328
  <ScrollPanelItem key="config_selector" {size_hint}>
281
329
  <ConfigSelector
282
330
  onselect={onselect.clone()}
283
- ondragenter={ctx.link().callback(|()| ViewCreated)}
331
+ ondragenter={ctx.link().callback(|()| Redraw)}
332
+ view_config={ctx.props().view_config.clone()}
333
+ drag_column={ctx.props().drag_column.clone()}
334
+ metadata={metadata.clone()}
335
+ selected_theme={ctx.props().selected_theme.clone()}
284
336
  {dragdrop}
285
337
  {renderer}
286
338
  {session}
@@ -288,11 +340,11 @@ impl Component for ColumnSelector {
288
340
  </ScrollPanelItem>
289
341
  };
290
342
 
291
- let mut named_count = self.named_row_count;
343
+ let mut named_count = ctx.props().named_column_count;
292
344
  let mut active_columns: Vec<_> = columns_iter
293
345
  .active()
294
346
  .enumerate()
295
- .map(|(idx, name)| {
347
+ .map(|(idx, name): (usize, ActiveColumnState)| {
296
348
  let ondragenter = ondragenter.reform(move |_| Some(idx));
297
349
  let size_hint = if named_count > 0 { 50.0 } else { 28.0 };
298
350
  named_count = named_count.saturating_sub(1);
@@ -307,6 +359,34 @@ impl Component for ColumnSelector {
307
359
  Some(ColumnLocator::Table(x)) | Some(ColumnLocator::Expression(x))
308
360
  if x == &key );
309
361
 
362
+ // Compute metadata-derived props here so that changes to
363
+ // session metadata propagate via prop diffing.
364
+ // For DragOver placeholders, resolve the type from the
365
+ // dragged column (since `get_name()` returns `None`).
366
+ let col_type = name
367
+ .get_name()
368
+ .and_then(|n| metadata.get_column_table_type(n))
369
+ .or_else(|| {
370
+ if matches!(name.state, ActiveColumnStateData::DragOver) {
371
+ dragdrop
372
+ .get_drag_column()
373
+ .and_then(|c| metadata.get_column_table_type(&c))
374
+ } else {
375
+ None
376
+ }
377
+ });
378
+
379
+ let is_expression = name
380
+ .get_name()
381
+ .map(|n| metadata.is_column_expression(n))
382
+ .unwrap_or(false);
383
+
384
+ let can_render_styles = name
385
+ .get_name()
386
+ .and_then(|n| can_render_column_styles(renderer, &config, metadata, n).ok())
387
+ .unwrap_or(false);
388
+
389
+ let show_edit_btn = is_expression || can_render_styles;
310
390
  let on_open_expr_panel = &ctx.props().on_open_expr_panel;
311
391
  html_nested! {
312
392
  <ScrollPanelItem {key} {size_hint}>
@@ -315,6 +395,11 @@ impl Component for ColumnSelector {
315
395
  {idx}
316
396
  {is_aggregated}
317
397
  {is_editing}
398
+ {is_expression}
399
+ {show_edit_btn}
400
+ {col_type}
401
+ view_config={config.clone()}
402
+ metadata={metadata.clone()}
318
403
  {name}
319
404
  {on_open_expr_panel}
320
405
  {ondragenter}
@@ -336,6 +421,7 @@ impl Component for ColumnSelector {
336
421
  .map(|(idx, vc)| {
337
422
  let selected_column = ctx.props().selected_column.as_ref();
338
423
  let is_editing = matches!(selected_column, Some(ColumnLocator::Expression(x)) if x.as_str() == vc.name);
424
+ let is_expression = metadata.is_column_expression(vc.name);
339
425
  html_nested! {
340
426
  <ScrollPanelItem key={vc.name} size_hint=28.0>
341
427
  <InactiveColumn
@@ -343,6 +429,9 @@ impl Component for ColumnSelector {
343
429
  visible={vc.is_visible}
344
430
  name={vc.name.to_owned()}
345
431
  {is_editing}
432
+ {is_expression}
433
+ view_config={config.clone()}
434
+ metadata={metadata.clone()}
346
435
  onselect={&onselect}
347
436
  ondragend={&ondragend}
348
437
  on_open_expr_panel={&ctx.props().on_open_expr_panel}
@@ -355,20 +444,9 @@ impl Component for ColumnSelector {
355
444
  })
356
445
  .collect();
357
446
 
358
- let size = if !inactive_children.is_empty() {
359
- 56.0
360
- } else {
361
- 28.0
362
- };
447
+ let size = 28.0;
363
448
 
364
- let add_column = if ctx
365
- .props()
366
- .session
367
- .metadata()
368
- .get_features()
369
- .unwrap()
370
- .expressions
371
- {
449
+ let add_column = if metadata.get_features().unwrap().expressions {
372
450
  html_nested! {
373
451
  <ScrollPanelItem key="__add_expression__" size_hint={size}>
374
452
  <AddExpressionButton
@@ -389,8 +467,8 @@ impl Component for ColumnSelector {
389
467
  inactive_children.insert(0, add_column);
390
468
  }
391
469
 
392
- let selected_columns = html! {
393
- <div id="selected-columns">
470
+ let mut selected_columns = vec![html! {
471
+ <div id="selected-columns" key="__active_columns__">
394
472
  <ScrollPanel
395
473
  id="active-columns"
396
474
  class={active_classes}
@@ -398,13 +476,28 @@ impl Component for ColumnSelector {
398
476
  dragenter={&self.drag_container.dragenter}
399
477
  dragleave={&self.drag_container.dragleave}
400
478
  viewport_ref={&self.drag_container.noderef}
479
+ initial_width={self.viewport_width}
480
+ on_auto_width={ctx.link().callback(ColumnSelectorMsg::SetWidth)}
401
481
  drop={ondrop}
402
482
  on_resize={&ctx.props().on_resize}
403
483
  on_dimensions_reset={&self.on_reset}
404
484
  children={std::iter::once(config_selector).chain(active_columns).collect::<Vec<_>>()}
405
485
  />
406
486
  </div>
407
- };
487
+ }];
488
+
489
+ if !inactive_children.is_empty() {
490
+ selected_columns.push(html! {
491
+ <ScrollPanel
492
+ id="sub-columns"
493
+ key="__sub_columns__"
494
+ class={classes!("scrollable")}
495
+ on_resize={&ctx.props().on_resize}
496
+ on_dimensions_reset={&self.on_reset}
497
+ children={inactive_children}
498
+ />
499
+ })
500
+ }
408
501
 
409
502
  html! {
410
503
  <>
@@ -415,16 +508,12 @@ impl Component for ColumnSelector {
415
508
  skip_empty=true
416
509
  orientation={Orientation::Vertical}
417
510
  >
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
- }
511
+ { for selected_columns }
427
512
  </SplitPanel>
513
+ <ColumnDropDownPortal
514
+ element={self.column_dropdown.clone()}
515
+ theme={ctx.props().selected_theme.clone().unwrap_or_default()}
516
+ />
428
517
  </>
429
518
  }
430
519
  }
@@ -18,8 +18,8 @@ use perspective_client::clone;
18
18
  use yew::{Html, Properties, function_component, html};
19
19
 
20
20
  use crate::components::empty_row::EmptyRow;
21
+ use crate::components::filter_dropdown::FilterDropDownElement;
21
22
  use crate::config::SymbolKVPair;
22
- use crate::custom_elements::FilterDropDownElement;
23
23
 
24
24
  #[derive(Properties, PartialEq)]
25
25
  pub struct RowSelectorProps {
@@ -16,10 +16,11 @@ use itertools::Itertools;
16
16
  use yew::{Callback, Html, Properties, html};
17
17
 
18
18
  use crate::components::column_settings_sidebar::style_tab::symbol::symbol_pairs_item::PairsListItem;
19
+ use crate::components::filter_dropdown::FilterDropDownElement;
19
20
  use crate::components::style::LocalStyle;
20
21
  use crate::config::SymbolKVPair;
21
22
  use crate::css;
22
- use crate::custom_elements::FilterDropDownElement;
23
+ use crate::utils::PtrEqRc;
23
24
 
24
25
  #[derive(Properties, PartialEq)]
25
26
  pub struct PairsListProps {
@@ -28,7 +29,7 @@ pub struct PairsListProps {
28
29
  pub update_pairs: Callback<Vec<SymbolKVPair>>,
29
30
  pub id: Option<String>,
30
31
  pub row_dropdown: Rc<FilterDropDownElement>,
31
- pub values: Rc<Vec<String>>,
32
+ pub values: PtrEqRc<Vec<String>>,
32
33
  pub column_name: String,
33
34
  }
34
35
 
@@ -16,8 +16,9 @@ use yew::{Callback, Html, Properties, html};
16
16
 
17
17
  use crate::components::column_settings_sidebar::style_tab::symbol::row_selector::RowSelector;
18
18
  use crate::components::column_settings_sidebar::style_tab::symbol::symbol_selector::SymbolSelector;
19
+ use crate::components::filter_dropdown::FilterDropDownElement;
19
20
  use crate::config::SymbolKVPair;
20
- use crate::custom_elements::FilterDropDownElement;
21
+ use crate::utils::PtrEqRc;
21
22
 
22
23
  #[derive(Properties, PartialEq)]
23
24
  pub struct PairsListItemProps {
@@ -26,7 +27,7 @@ pub struct PairsListItemProps {
26
27
  pub pairs: Vec<SymbolKVPair>,
27
28
  pub update_pairs: Callback<Vec<SymbolKVPair>>,
28
29
  pub row_dropdown: Rc<FilterDropDownElement>,
29
- pub values: Rc<Vec<String>>,
30
+ pub values: PtrEqRc<Vec<String>>,
30
31
  pub focused: bool,
31
32
  pub set_focused_index: Callback<Option<usize>>,
32
33
  pub column_name: String,
@@ -10,18 +10,17 @@
10
10
  // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11
11
  // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
12
 
13
- use std::rc::Rc;
14
-
15
13
  use itertools::Itertools;
16
14
  use yew::{Callback, Html, Properties, function_component, html};
17
15
 
18
16
  use crate::components::containers::select::{Select, SelectItem};
17
+ use crate::utils::PtrEqRc;
19
18
 
20
19
  #[derive(Properties, PartialEq)]
21
20
  pub struct SymbolSelectorProps {
22
21
  pub index: usize,
23
22
  pub selected_value: Option<String>,
24
- pub values: Rc<Vec<String>>,
23
+ pub values: PtrEqRc<Vec<String>>,
25
24
  pub callback: Callback<String>,
26
25
  }
27
26
 
@@ -23,10 +23,10 @@ use itertools::Itertools;
23
23
  use yew::{Callback, Html, Properties, html};
24
24
 
25
25
  use crate::components::column_settings_sidebar::style_tab::symbol::symbol_pairs::PairsList;
26
+ use crate::components::filter_dropdown::{FilterDropDownElement, FilterDropDownPortal};
26
27
  use crate::components::style::LocalStyle;
27
28
  use crate::config::{ColumnConfigValueUpdate, KeyValueOpts, SymbolKVPair};
28
29
  use crate::css;
29
- use crate::custom_elements::FilterDropDownElement;
30
30
  use crate::session::Session;
31
31
 
32
32
  #[derive(Properties, PartialEq, Clone)]
@@ -36,6 +36,8 @@ pub struct SymbolAttrProps {
36
36
  pub restored_config: Option<HashMap<String, String>>,
37
37
  pub on_change: Callback<ColumnConfigValueUpdate>,
38
38
  pub default_config: KeyValueOpts,
39
+ /// Selected theme name, threaded for PortalModal consumers.
40
+ pub selected_theme: Option<String>,
39
41
  }
40
42
  impl SymbolAttrProps {
41
43
  pub fn next_default_symbol(&self, pairs_len: usize) -> String {
@@ -125,6 +127,10 @@ impl yew::Component for SymbolStyle {
125
127
  values={Rc::new(ctx.props().default_config.values.clone())}
126
128
  {update_pairs}
127
129
  />
130
+ <FilterDropDownPortal
131
+ element={(*self.row_dropdown).clone()}
132
+ theme={ctx.props().selected_theme.clone().unwrap_or_default()}
133
+ />
128
134
  </>
129
135
  }
130
136
  }