@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
@@ -12,20 +12,23 @@
12
12
 
13
13
  use std::rc::Rc;
14
14
 
15
+ use perspective_client::config::{ViewConfig, ViewConfigUpdate};
16
+ use perspective_js::utils::ApiFuture;
15
17
  use yew::prelude::*;
16
18
 
17
19
  use super::column_selector::ColumnSelector;
18
20
  use super::plugin_selector::PluginSelector;
19
- use crate::PerspectiveProperties;
20
21
  use crate::components::containers::sidebar_close_button::SidebarCloseButton;
22
+ use crate::config::PluginUpdate;
21
23
  use crate::dragdrop::*;
22
- use crate::model::*;
23
- use crate::presentation::{ColumnLocator, Presentation};
24
+ use crate::presentation::{ColumnLocator, OpenColumnSettings, Presentation};
24
25
  use crate::renderer::*;
26
+ use crate::session::column_defaults_update::*;
25
27
  use crate::session::*;
28
+ use crate::tasks::can_render_column_styles;
26
29
  use crate::utils::*;
27
30
 
28
- #[derive(Clone, Properties, PerspectiveProperties!)]
31
+ #[derive(Clone, Properties)]
29
32
  pub struct SettingsPanelProps {
30
33
  pub on_close: Callback<()>,
31
34
  pub on_resize: Rc<PubSub<()>>,
@@ -33,6 +36,25 @@ pub struct SettingsPanelProps {
33
36
  pub on_debug: Callback<()>,
34
37
  pub is_debug: bool,
35
38
 
39
+ /// Value props threaded from the root's `RendererProps` / `SessionProps`.
40
+ pub plugin_name: Option<String>,
41
+ pub available_plugins: PtrEqRc<Vec<String>>,
42
+ pub has_table: Option<TableLoadState>,
43
+ pub named_column_count: usize,
44
+ pub view_config: PtrEqRc<ViewConfig>,
45
+ /// Column currently being dragged (if any) — threaded to show drag
46
+ /// highlights without per-component `DragDrop` PubSub subscriptions.
47
+ pub drag_column: Option<String>,
48
+ /// Cloned session metadata snapshot — threaded from `SessionProps`
49
+ /// so that metadata changes trigger re-renders via prop diffing.
50
+ pub metadata: SessionMetadataRc,
51
+ /// Snapshot of the column-settings sidebar state — threaded from
52
+ /// `PresentationProps` so that open/close triggers re-renders.
53
+ pub open_column_settings: OpenColumnSettings,
54
+
55
+ /// Selected theme name, threaded for PortalModal consumers.
56
+ pub selected_theme: Option<String>,
57
+
36
58
  /// State
37
59
  pub dragdrop: DragDrop,
38
60
  pub session: Session,
@@ -41,8 +63,17 @@ pub struct SettingsPanelProps {
41
63
  }
42
64
 
43
65
  impl PartialEq for SettingsPanelProps {
44
- fn eq(&self, _rhs: &Self) -> bool {
45
- false
66
+ fn eq(&self, rhs: &Self) -> bool {
67
+ self.is_debug == rhs.is_debug
68
+ && self.plugin_name == rhs.plugin_name
69
+ && self.available_plugins == rhs.available_plugins
70
+ && self.has_table == rhs.has_table
71
+ && self.named_column_count == rhs.named_column_count
72
+ && self.view_config == rhs.view_config
73
+ && self.drag_column == rhs.drag_column
74
+ && self.metadata == rhs.metadata
75
+ && self.open_column_settings == rhs.open_column_settings
76
+ && self.selected_theme == rhs.selected_theme
46
77
  }
47
78
  }
48
79
 
@@ -55,7 +86,74 @@ pub fn SettingsPanel(props: &SettingsPanelProps) -> Html {
55
86
  session,
56
87
  ..
57
88
  } = &props;
58
- let selected_column = props.get_current_column_locator();
89
+
90
+ let selected_column = {
91
+ let locator = props.open_column_settings.locator.clone();
92
+ let config = &props.view_config;
93
+ locator.filter(|locator| match locator {
94
+ ColumnLocator::Table(name) => {
95
+ locator
96
+ .name()
97
+ .map(|n| {
98
+ config.columns.iter().any(|maybe_col| {
99
+ maybe_col.as_ref().map(|col| col == n).unwrap_or_default()
100
+ }) || config.group_by.iter().any(|col| col == n)
101
+ || config.split_by.iter().any(|col| col == n)
102
+ || config.filter.iter().any(|col| col.column() == n)
103
+ || config.sort.iter().any(|col| &col.0 == n)
104
+ })
105
+ .unwrap_or_default()
106
+ && can_render_column_styles(&props.renderer, config, &props.metadata, name)
107
+ .unwrap_or_default()
108
+ },
109
+ _ => true,
110
+ })
111
+ };
112
+
113
+ let plugin_name = props.plugin_name.clone();
114
+ let available_plugins = props.available_plugins.clone();
115
+
116
+ // Dispatch callback: captures engine handles, constructs config update, renders
117
+ let on_select_plugin = {
118
+ clone!(renderer, session, presentation);
119
+ let session_metadata = props.metadata.clone();
120
+ Callback::from(move |plugin_name: String| {
121
+ if !session.is_errored() {
122
+ let metadata =
123
+ renderer.get_next_plugin_metadata(&PluginUpdate::Update(plugin_name));
124
+
125
+ let prev_metadata = renderer.metadata();
126
+ let requirements = metadata.as_ref().unwrap_or(&*prev_metadata);
127
+ let rollup_features = session_metadata
128
+ .get_features()
129
+ .map(|x| x.get_group_rollup_modes())
130
+ .unwrap();
131
+
132
+ let group_rollups = requirements.get_group_rollups(&rollup_features);
133
+ let mut update = ViewConfigUpdate {
134
+ group_rollup_mode: group_rollups.first().cloned(),
135
+ ..ViewConfigUpdate::default()
136
+ };
137
+
138
+ update.set_update_column_defaults(
139
+ &session_metadata,
140
+ &session.get_view_config().columns,
141
+ requirements,
142
+ );
143
+
144
+ if session.update_view_config(update).is_ok() {
145
+ clone!(renderer, session);
146
+ ApiFuture::spawn(async move {
147
+ renderer.apply_pending_plugin()?;
148
+ renderer.draw(session.validate().await?.create_view()).await
149
+ });
150
+ }
151
+
152
+ presentation.set_open_column_settings(None);
153
+ }
154
+ })
155
+ };
156
+
59
157
  html! {
60
158
  <div id="settings_panel" class="sidebar_column noselect split-panel orient-vertical">
61
159
  if selected_column.is_none() {
@@ -68,14 +166,20 @@ pub fn SettingsPanel(props: &SettingsPanelProps) -> Html {
68
166
  id={if props.is_debug {"debug_close_button"} else {"debug_open_button"}}
69
167
  on_close_sidebar={&props.on_debug}
70
168
  />
71
- <PluginSelector {presentation} {renderer} {session} />
169
+ <PluginSelector {plugin_name} {available_plugins} {on_select_plugin} />
72
170
  <ColumnSelector
73
171
  on_resize={&props.on_resize}
74
172
  on_open_expr_panel={&props.on_select_column}
75
- selected_column={selected_column.clone()}
173
+ {selected_column}
174
+ has_table={props.has_table.clone()}
175
+ named_column_count={props.named_column_count}
176
+ view_config={props.view_config.clone()}
177
+ drag_column={props.drag_column.clone()}
178
+ metadata={props.metadata.clone()}
179
+ selected_theme={props.selected_theme.clone()}
76
180
  {dragdrop}
77
- {renderer}
78
- {session}
181
+ renderer={renderer.clone()}
182
+ session={session.clone()}
79
183
  />
80
184
  </div>
81
185
  }
@@ -10,26 +10,27 @@
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
-
13
+ use wasm_bindgen_futures::spawn_local;
15
14
  use web_sys::*;
16
15
  use yew::prelude::*;
17
16
 
18
17
  use super::status_indicator::StatusIndicator;
19
18
  use super::style::LocalStyle;
20
19
  use crate::components::containers::select::*;
20
+ use crate::components::copy_dropdown::CopyDropDownMenu;
21
+ use crate::components::export_dropdown::ExportDropDownMenu;
22
+ use crate::components::portal::PortalModal;
21
23
  use crate::components::status_bar_counter::StatusBarRowsCounter;
22
- use crate::custom_elements::copy_dropdown::*;
23
- use crate::custom_elements::export_dropdown::*;
24
24
  use crate::custom_events::CustomEvents;
25
- use crate::model::*;
25
+ use crate::js::*;
26
26
  use crate::presentation::Presentation;
27
27
  use crate::renderer::*;
28
28
  use crate::session::*;
29
+ use crate::tasks::*;
29
30
  use crate::utils::*;
30
31
  use crate::*;
31
32
 
32
- #[derive(Properties, PerspectiveProperties!)]
33
+ #[derive(Clone, Properties)]
33
34
  pub struct StatusBarProps {
34
35
  // DOM Attribute
35
36
  pub id: String,
@@ -41,6 +42,25 @@ pub struct StatusBarProps {
41
42
  #[prop_or_default]
42
43
  pub on_settings: Option<Callback<()>>,
43
44
 
45
+ // Value props threaded from the root's `SessionProps`.
46
+ // Using these avoids PubSub subscriptions for table_loaded / table_errored.
47
+ pub has_table: Option<TableLoadState>,
48
+ pub is_errored: bool,
49
+ pub stats: Option<ViewStats>,
50
+ /// In-flight render counter and full error, threaded to `StatusIndicator`.
51
+ pub update_count: u32,
52
+ pub error: Option<TableErrorState>,
53
+ /// Title string from session — threaded to avoid title_changed
54
+ /// subscription.
55
+ pub title: Option<String>,
56
+ /// Theme state from presentation — threaded to avoid theme_config_updated /
57
+ /// visibility_changed subscriptions.
58
+ pub is_settings_open: bool,
59
+ pub selected_theme: Option<String>,
60
+ pub available_themes: PtrEqRc<Vec<String>>,
61
+ /// Whether this viewer is hosted inside a `<perspective-workspace>`.
62
+ pub is_workspace: bool,
63
+
44
64
  // State
45
65
  pub custom_events: CustomEvents,
46
66
  pub session: Session,
@@ -51,6 +71,48 @@ pub struct StatusBarProps {
51
71
  impl PartialEq for StatusBarProps {
52
72
  fn eq(&self, other: &Self) -> bool {
53
73
  self.id == other.id
74
+ && self.has_table == other.has_table
75
+ && self.is_errored == other.is_errored
76
+ && self.stats == other.stats
77
+ && self.update_count == other.update_count
78
+ && self.error == other.error
79
+ && self.title == other.title
80
+ && self.is_settings_open == other.is_settings_open
81
+ && self.selected_theme == other.selected_theme
82
+ && self.available_themes == other.available_themes
83
+ && self.is_workspace == other.is_workspace
84
+ }
85
+ }
86
+
87
+ impl HasCustomEvents for StatusBarProps {
88
+ fn custom_events(&self) -> &CustomEvents {
89
+ &self.custom_events
90
+ }
91
+ }
92
+
93
+ impl HasPresentation for StatusBarProps {
94
+ fn presentation(&self) -> &Presentation {
95
+ &self.presentation
96
+ }
97
+ }
98
+
99
+ impl HasRenderer for StatusBarProps {
100
+ fn renderer(&self) -> &Renderer {
101
+ &self.renderer
102
+ }
103
+ }
104
+
105
+ impl HasSession for StatusBarProps {
106
+ fn session(&self) -> &Session {
107
+ &self.session
108
+ }
109
+ }
110
+
111
+ impl StateProvider for StatusBarProps {
112
+ type State = StatusBarProps;
113
+
114
+ fn clone_state(&self) -> Self::State {
115
+ self.clone()
54
116
  }
55
117
  }
56
118
 
@@ -58,9 +120,10 @@ pub enum StatusBarMsg {
58
120
  Reset(MouseEvent),
59
121
  Export,
60
122
  Copy,
123
+ CloseExport,
124
+ CloseCopy,
61
125
  Noop,
62
126
  Eject,
63
- SetThemeConfig((Rc<Vec<String>>, Option<usize>)),
64
127
  SetTheme(String),
65
128
  ResetTheme,
66
129
  PointerEvent(web_sys::PointerEvent),
@@ -70,14 +133,16 @@ pub enum StatusBarMsg {
70
133
 
71
134
  /// A toolbar with buttons, and `Table` & `View` status information.
72
135
  pub struct StatusBar {
73
- _subscriptions: [Subscription; 5],
74
136
  copy_ref: NodeRef,
75
137
  export_ref: NodeRef,
76
138
  input_ref: NodeRef,
77
139
  statusbar_ref: NodeRef,
78
- theme: Option<String>,
79
- themes: Rc<Vec<String>>,
140
+ /// Local title tracks the live `<input>` value before the user commits the
141
+ /// change (blur / Enter). Reset to the prop value whenever the prop
142
+ /// changes.
80
143
  title: Option<String>,
144
+ copy_target: Option<HtmlElement>,
145
+ export_target: Option<HtmlElement>,
81
146
  }
82
147
 
83
148
  impl Component for StatusBar {
@@ -85,21 +150,26 @@ impl Component for StatusBar {
85
150
  type Properties = StatusBarProps;
86
151
 
87
152
  fn create(ctx: &Context<Self>) -> Self {
88
- fetch_initial_theme(ctx);
89
153
  Self {
90
- _subscriptions: register_listeners(ctx),
91
154
  copy_ref: NodeRef::default(),
92
155
  export_ref: NodeRef::default(),
93
156
  input_ref: NodeRef::default(),
94
157
  statusbar_ref: NodeRef::default(),
95
- theme: None,
96
- themes: vec![].into(),
97
- title: ctx.props().session().get_title().clone(),
158
+ title: ctx.props().title.clone(),
159
+ copy_target: None,
160
+ export_target: None,
98
161
  }
99
162
  }
100
163
 
101
- fn changed(&mut self, ctx: &Context<Self>, _old_props: &Self::Properties) -> bool {
102
- self._subscriptions = register_listeners(ctx);
164
+ fn changed(&mut self, ctx: &Context<Self>, old_props: &Self::Properties) -> bool {
165
+ // Keep the local title in sync with the prop whenever the session title
166
+ // changes externally (e.g. restore() call) or the settings panel opens /
167
+ // closes (which resets the input element).
168
+ if ctx.props().title != old_props.title
169
+ || ctx.props().is_settings_open != old_props.is_settings_open
170
+ {
171
+ self.title = ctx.props().title.clone();
172
+ }
103
173
  true
104
174
  }
105
175
 
@@ -111,47 +181,50 @@ impl Component for StatusBar {
111
181
  false
112
182
  },
113
183
  StatusBarMsg::ResetTheme => {
114
- let state = ctx.props().clone_state();
184
+ let presentation = ctx.props().presentation.clone();
185
+ let session = ctx.props().session.clone();
186
+ let renderer = ctx.props().renderer.clone();
115
187
  ApiFuture::spawn(async move {
116
- state.presentation.reset_theme().await?;
117
- let view = state.session.get_view().into_apierror()?;
118
- state.renderer.restyle_all(&view).await
188
+ presentation.reset_theme().await?;
189
+ let view = session.get_view().into_apierror()?;
190
+ renderer.restyle_all(&view).await
119
191
  });
120
192
  true
121
193
  },
122
- StatusBarMsg::SetThemeConfig((themes, index)) => {
123
- let new_theme = index.and_then(|x| themes.get(x)).cloned();
124
- let should_render = new_theme != self.theme || self.themes != themes;
125
- self.theme = new_theme;
126
- self.themes = themes;
127
- should_render
128
- },
129
194
  StatusBarMsg::SetTheme(theme_name) => {
130
- let state = ctx.props().clone_state();
195
+ let presentation = ctx.props().presentation.clone();
196
+ let session = ctx.props().session.clone();
197
+ let renderer = ctx.props().renderer.clone();
131
198
  ApiFuture::spawn(async move {
132
- state.presentation.set_theme_name(Some(&theme_name)).await?;
133
- let view = state.session.get_view().into_apierror()?;
134
- state.renderer.restyle_all(&view).await
199
+ presentation.set_theme_name(Some(&theme_name)).await?;
200
+ let view = session.get_view().into_apierror()?;
201
+ renderer.restyle_all(&view).await
135
202
  });
136
203
 
137
204
  false
138
205
  },
139
206
  StatusBarMsg::Export => {
140
- let target = self.export_ref.cast::<HtmlElement>().into_apierror()?;
141
- ExportDropDownMenuElement::new_from_model(ctx.props()).open(target);
142
- false
207
+ self.export_target = self.export_ref.cast::<HtmlElement>();
208
+ true
143
209
  },
144
210
  StatusBarMsg::Copy => {
145
- let target = self.copy_ref.cast::<HtmlElement>().into_apierror()?;
146
- CopyDropDownMenuElement::new_from_model(ctx.props()).open(target);
147
- false
211
+ self.copy_target = self.copy_ref.cast::<HtmlElement>();
212
+ true
213
+ },
214
+ StatusBarMsg::CloseExport => {
215
+ self.export_target = None;
216
+ true
217
+ },
218
+ StatusBarMsg::CloseCopy => {
219
+ self.copy_target = None;
220
+ true
148
221
  },
149
222
  StatusBarMsg::Eject => {
150
223
  ctx.props().presentation().on_eject.emit(());
151
224
  false
152
225
  },
153
226
  StatusBarMsg::Noop => {
154
- self.title = ctx.props().session().get_title();
227
+ self.title = ctx.props().title.clone();
155
228
  true
156
229
  },
157
230
  StatusBarMsg::TitleInputEvent => {
@@ -201,16 +274,21 @@ impl Component for StatusBar {
201
274
  ..
202
275
  } = ctx.props();
203
276
 
277
+ let has_table = ctx.props().has_table.clone();
278
+ let is_errored = ctx.props().is_errored;
279
+ let is_settings_open = ctx.props().is_settings_open;
280
+ let title = &ctx.props().title;
281
+
204
282
  let mut is_updating_class_name = classes!();
205
- if session.get_title().is_some() {
283
+ if title.is_some() {
206
284
  is_updating_class_name.push("titled");
207
285
  };
208
286
 
209
- if !presentation.is_settings_open() {
287
+ if !is_settings_open {
210
288
  is_updating_class_name.push(["settings-closed", "titled"]);
211
289
  };
212
290
 
213
- if !session.has_table() {
291
+ if !matches!(has_table, Some(TableLoadState::Loaded)) {
214
292
  is_updating_class_name.push("updating");
215
293
  }
216
294
 
@@ -229,20 +307,58 @@ impl Component for StatusBar {
229
307
  .link()
230
308
  .callback(|_: InputEvent| StatusBarMsg::TitleInputEvent);
231
309
 
232
- let is_menu = session.has_table() && ctx.props().on_settings.as_ref().is_none();
310
+ let is_menu = matches!(has_table, Some(TableLoadState::Loaded))
311
+ && ctx.props().on_settings.as_ref().is_none();
233
312
  let is_title = is_menu
234
- || presentation.get_is_workspace()
235
- || session.get_title().is_some()
236
- || session.is_errored()
313
+ || ctx.props().is_workspace
314
+ || title.is_some()
315
+ || is_errored
237
316
  || presentation.is_active(&self.input_ref.cast::<Element>());
238
317
 
239
- let is_settings = session.get_title().is_some()
240
- || presentation.get_is_workspace()
241
- || !session.has_table()
242
- || session.is_errored()
243
- || presentation.is_settings_open()
318
+ let is_settings = title.is_some()
319
+ || ctx.props().is_workspace
320
+ || !matches!(has_table, Some(TableLoadState::Loaded))
321
+ || is_errored
322
+ || is_settings_open
244
323
  || presentation.is_active(&self.input_ref.cast::<Element>());
245
324
 
325
+ let on_copy_select = {
326
+ let props = ctx.props().clone();
327
+ let link = ctx.link().clone();
328
+ Callback::from(move |x: ExportFile| {
329
+ let props = props.clone();
330
+ let link = link.clone();
331
+ spawn_local(async move {
332
+ let mime = x.method.mimetype(x.is_chart);
333
+ let task = props.export_method_to_blob(x.method);
334
+ let result = copy_to_clipboard(task, mime).await;
335
+ crate::maybe_log!({
336
+ result?;
337
+ link.send_message(StatusBarMsg::CloseCopy);
338
+ })
339
+ })
340
+ })
341
+ };
342
+
343
+ let on_export_select = {
344
+ let props = ctx.props().clone();
345
+ let link = ctx.link().clone();
346
+ Callback::from(move |x: ExportFile| {
347
+ if !x.name.is_empty() {
348
+ clone!(props, link);
349
+ spawn_local(async move {
350
+ let val = props.export_method_to_blob(x.method).await.unwrap();
351
+ let is_chart = props.renderer().is_chart();
352
+ download(&x.as_filename(is_chart), &val).unwrap();
353
+ link.send_message(StatusBarMsg::CloseExport);
354
+ })
355
+ }
356
+ })
357
+ };
358
+
359
+ let on_close_copy = ctx.link().callback(|_| StatusBarMsg::CloseCopy);
360
+ let on_close_export = ctx.link().callback(|_| StatusBarMsg::CloseExport);
361
+
246
362
  if is_settings {
247
363
  html! {
248
364
  <>
@@ -253,7 +369,15 @@ impl Component for StatusBar {
253
369
  class={is_updating_class_name}
254
370
  {onpointerdown}
255
371
  >
256
- <StatusIndicator {custom_events} {renderer} {session} />
372
+ <StatusIndicator
373
+ {custom_events}
374
+ {renderer}
375
+ {session}
376
+ update_count={ctx.props().update_count}
377
+ error={ctx.props().error.clone()}
378
+ has_table={ctx.props().has_table.clone()}
379
+ stats={ctx.props().stats.clone()}
380
+ />
257
381
  if is_title {
258
382
  <label
259
383
  class="input-sizer"
@@ -272,21 +396,22 @@ impl Component for StatusBar {
272
396
  </label>
273
397
  }
274
398
  if is_title {
275
- <StatusBarRowsCounter {session} />
399
+ <StatusBarRowsCounter stats={ctx.props().stats.clone()} />
276
400
  }
277
401
  <div id="spacer" />
278
402
  if is_menu {
279
403
  <div id="menu-bar" class="section">
280
404
  <ThemeSelector
281
- theme={self.theme.clone()}
282
- themes={self.themes.clone()}
405
+ theme={ctx.props().selected_theme.clone()}
406
+ themes={ctx.props().available_themes.clone()}
283
407
  on_change={ctx.link().callback(StatusBarMsg::SetTheme)}
284
408
  on_reset={ctx.link().callback(|_| StatusBarMsg::ResetTheme)}
285
409
  />
286
410
  <div id="plugin-settings"><slot name="statusbar-extra" /></div>
287
411
  <span class="hover-target">
288
412
  <span id="reset" class="button" onmousedown={&onreset}>
289
- <span />
413
+ <span class="icon" />
414
+ <span class="icon-label" />
290
415
  </span>
291
416
  </span>
292
417
  <span
@@ -294,14 +419,20 @@ impl Component for StatusBar {
294
419
  class="hover-target"
295
420
  onmousedown={onexport}
296
421
  >
297
- <span id="export" class="button"><span /></span>
422
+ <span id="export" class="button">
423
+ <span class="icon" />
424
+ <span class="icon-label" />
425
+ </span>
298
426
  </span>
299
427
  <span
300
428
  ref={&self.copy_ref}
301
429
  class="hover-target"
302
430
  onmousedown={oncopy}
303
431
  >
304
- <span id="copy" class="button"><span /></span>
432
+ <span id="copy" class="button">
433
+ <span class="icon" />
434
+ <span class="icon-label" />
435
+ </span>
305
436
  </span>
306
437
  </div>
307
438
  }
@@ -310,17 +441,45 @@ impl Component for StatusBar {
310
441
  id="settings_button"
311
442
  class="noselect"
312
443
  onmousedown={x.reform(|_| ())}
313
- />
314
- <div id="close_button" class="noselect" onmousedown={onclose} />
444
+ >
445
+ <span class="icon" />
446
+ </div>
447
+ <div id="close_button" class="noselect" onmousedown={onclose}>
448
+ <span class="icon" />
449
+ </div>
315
450
  }
316
451
  </div>
452
+ <PortalModal
453
+ tag_name="perspective-copy-menu"
454
+ target={self.copy_target.clone()}
455
+ own_focus=true
456
+ on_close={on_close_copy}
457
+ theme={ctx.props().selected_theme.clone().unwrap_or_default()}
458
+ >
459
+ <CopyDropDownMenu renderer={renderer.clone()} callback={on_copy_select} />
460
+ </PortalModal>
461
+ <PortalModal
462
+ tag_name="perspective-export-menu"
463
+ target={self.export_target.clone()}
464
+ own_focus=true
465
+ on_close={on_close_export}
466
+ theme={ctx.props().selected_theme.clone().unwrap_or_default()}
467
+ >
468
+ <ExportDropDownMenu
469
+ renderer={renderer.clone()}
470
+ session={session.clone()}
471
+ callback={on_export_select}
472
+ />
473
+ </PortalModal>
317
474
  </>
318
475
  }
319
476
  } else if let Some(x) = ctx.props().on_settings.as_ref() {
320
477
  let class = classes!(is_updating_class_name, "floating");
321
478
  html! {
322
479
  <div id={ctx.props().id.clone()} {class}>
323
- <div id="settings_button" class="noselect" onmousedown={x.reform(|_| ())} />
480
+ <div id="settings_button" class="noselect" onmousedown={x.reform(|_| ())}>
481
+ <span class="icon" />
482
+ </div>
324
483
  <div id="close_button" class="noselect" onmousedown={&onclose} />
325
484
  </div>
326
485
  }
@@ -330,46 +489,10 @@ impl Component for StatusBar {
330
489
  }
331
490
  }
332
491
 
333
- fn register_listeners(ctx: &Context<StatusBar>) -> [Subscription; 5] {
334
- [
335
- ctx.props()
336
- .presentation()
337
- .theme_config_updated
338
- .add_listener(ctx.link().callback(StatusBarMsg::SetThemeConfig)),
339
- ctx.props()
340
- .presentation()
341
- .visibility_changed
342
- .add_listener(ctx.link().callback(|_| StatusBarMsg::Noop)),
343
- ctx.props()
344
- .session()
345
- .title_changed
346
- .add_listener(ctx.link().callback(|_| StatusBarMsg::Noop)),
347
- ctx.props()
348
- .session()
349
- .table_loaded
350
- .add_listener(ctx.link().callback(|_| StatusBarMsg::Noop)),
351
- ctx.props()
352
- .session()
353
- .table_errored
354
- .add_listener(ctx.link().callback(|_| StatusBarMsg::Noop)),
355
- ]
356
- }
357
-
358
- fn fetch_initial_theme(ctx: &Context<StatusBar>) {
359
- ApiFuture::spawn({
360
- let on_theme = ctx.link().callback(StatusBarMsg::SetThemeConfig);
361
- clone!(ctx.props().presentation());
362
- async move {
363
- on_theme.emit(presentation.get_selected_theme_config().await?);
364
- Ok(())
365
- }
366
- });
367
- }
368
-
369
492
  #[derive(Properties, PartialEq)]
370
493
  struct ThemeSelectorProps {
371
494
  pub theme: Option<String>,
372
- pub themes: Rc<Vec<String>>,
495
+ pub themes: PtrEqRc<Vec<String>>,
373
496
  pub on_reset: Callback<()>,
374
497
  pub on_change: Callback<String>,
375
498
  }
@@ -403,6 +526,7 @@ fn ThemeSelector(props: &ThemeSelectorProps) -> Html {
403
526
  onclick={props.on_reset.reform(|_| ())}
404
527
  />
405
528
  <span id="theme" class="button">
529
+ <span class="icon" />
406
530
  <Select<String>
407
531
  id="theme_selector"
408
532
  class="invert"