@perspective-dev/viewer 4.5.2 → 5.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (227) hide show
  1. package/dist/cdn/perspective-viewer.js +20 -1
  2. package/dist/cdn/perspective-viewer.js.map +4 -4
  3. package/dist/css/botanical.css +1 -1
  4. package/dist/css/dracula.css +1 -1
  5. package/dist/css/gruvbox-dark.css +1 -1
  6. package/dist/css/gruvbox.css +1 -1
  7. package/dist/css/icons.css +1 -1
  8. package/dist/css/intl/de.css +1 -1
  9. package/dist/css/intl/es.css +1 -1
  10. package/dist/css/intl/fr.css +1 -1
  11. package/dist/css/intl/ja.css +1 -1
  12. package/dist/css/intl/pt.css +1 -1
  13. package/dist/css/intl/zh.css +1 -1
  14. package/dist/css/intl.css +1 -1
  15. package/dist/css/monokai.css +1 -1
  16. package/dist/css/phosphor.css +1 -1
  17. package/dist/css/pro-dark.css +1 -1
  18. package/dist/css/pro.css +1 -1
  19. package/dist/css/solarized-dark.css +1 -1
  20. package/dist/css/solarized.css +1 -1
  21. package/dist/css/themes.css +1 -1
  22. package/dist/css/vaporwave.css +1 -1
  23. package/dist/esm/extensions.d.ts +22 -3
  24. package/dist/esm/perspective-viewer.d.ts +9 -0
  25. package/dist/esm/perspective-viewer.inline.js +20 -1
  26. package/dist/esm/perspective-viewer.inline.js.map +4 -4
  27. package/dist/esm/perspective-viewer.js +20 -1
  28. package/dist/esm/perspective-viewer.js.map +4 -4
  29. package/dist/esm/plugin.d.ts +71 -5
  30. package/dist/esm/ts-rs/ClientOptions.d.ts +7 -0
  31. package/dist/esm/ts-rs/ExportMethod.d.ts +1 -0
  32. package/dist/esm/ts-rs/ExportOptions.d.ts +9 -0
  33. package/dist/esm/ts-rs/GetClientOptions.d.ts +8 -0
  34. package/dist/esm/ts-rs/GetTableOptions.d.ts +8 -0
  35. package/dist/esm/ts-rs/Layout.d.ts +11 -0
  36. package/dist/esm/ts-rs/Orientation.d.ts +4 -0
  37. package/dist/esm/ts-rs/PanelOptions.d.ts +7 -0
  38. package/dist/esm/ts-rs/PanelViewerConfig.d.ts +41 -0
  39. package/dist/esm/ts-rs/SplitLayout.d.ts +7 -0
  40. package/dist/esm/ts-rs/TabLayout.d.ts +10 -0
  41. package/dist/esm/ts-rs/ViewerConfig.d.ts +8 -2
  42. package/dist/esm/ts-rs/ViewerConfigUpdate.d.ts +9 -0
  43. package/dist/esm/ts-rs/WindowAggregate.d.ts +1 -0
  44. package/dist/esm/ts-rs/WindowSort.d.ts +7 -0
  45. package/dist/esm/ts-rs/WindowSortDir.d.ts +6 -0
  46. package/dist/esm/ts-rs/WindowSpec.d.ts +46 -0
  47. package/dist/esm/ts-rs/Windows.d.ts +10 -0
  48. package/dist/esm/ts-rs/WorkspaceConfig.d.ts +40 -0
  49. package/dist/esm/ts-rs/WorkspaceConfigUpdate.d.ts +28 -0
  50. package/dist/wasm/perspective-viewer.d.ts +299 -78
  51. package/dist/wasm/perspective-viewer.js +544 -186
  52. package/dist/wasm/perspective-viewer.wasm +0 -0
  53. package/dist/wasm/perspective-viewer.wasm.d.ts +34 -26
  54. package/dist/wasm/snippets/perspective-viewer-8a8c7d5d696b43ae/inline0.js +6 -0
  55. package/dist/wasm/snippets/{perspective-viewer-93064d0d79855d97/inline3.js → perspective-viewer-8a8c7d5d696b43ae/inline4.js} +5 -0
  56. package/package.json +3 -2
  57. package/src/css/_context_menu.css +14 -0
  58. package/src/css/column-selector.css +1 -0
  59. package/src/css/column-settings-panel.css +68 -3
  60. package/src/css/column-style.css +4 -3
  61. package/src/css/config-selector.css +22 -1
  62. package/src/css/containers/context-menu.css +107 -0
  63. package/src/css/containers/tabs.css +2 -2
  64. package/src/css/panel-tab.css +193 -0
  65. package/src/css/plugin-selector.css +7 -10
  66. package/src/css/status-bar.css +75 -81
  67. package/src/css/viewer.css +188 -70
  68. package/src/rust/components/column_dropdown.rs +40 -30
  69. package/src/rust/components/column_selector/active_column.rs +28 -45
  70. package/src/rust/components/column_selector/aggregate_selector.rs +4 -7
  71. package/src/rust/components/column_selector/column_selector_column_row.rs +73 -0
  72. package/src/rust/components/column_selector/config_selector.rs +40 -82
  73. package/src/rust/components/column_selector/expr_edit_button.rs +7 -1
  74. package/src/rust/components/column_selector/filter_column.rs +5 -15
  75. package/src/rust/components/column_selector/inactive_column.rs +11 -9
  76. package/src/rust/components/column_selector/sort_column.rs +3 -6
  77. package/src/rust/components/column_selector.rs +25 -14
  78. package/src/rust/components/column_settings_sidebar/style_tab.rs +2 -1
  79. package/src/rust/components/column_settings_sidebar/window_tab.rs +34 -0
  80. package/src/rust/components/column_settings_sidebar.rs +163 -16
  81. package/src/rust/components/containers/dragdrop_list.rs +73 -3
  82. package/src/rust/components/containers/sidebar.rs +99 -22
  83. package/src/rust/components/containers/split_panel.rs +181 -62
  84. package/src/rust/components/context_menu.rs +130 -0
  85. package/src/rust/components/editable_header.rs +2 -0
  86. package/src/rust/components/form/debug.rs +3 -0
  87. package/src/rust/components/global_filter_bar.rs +76 -0
  88. package/src/rust/components/main_panel/frame_theme.rs +186 -0
  89. package/src/rust/components/main_panel/msg.rs +62 -0
  90. package/src/rust/components/main_panel/presize.rs +220 -0
  91. package/src/rust/components/main_panel/reconcile.rs +224 -0
  92. package/src/rust/components/main_panel/render.rs +385 -0
  93. package/src/rust/components/main_panel/update.rs +184 -0
  94. package/src/rust/components/main_panel.rs +316 -71
  95. package/src/rust/components/mod.rs +5 -0
  96. package/src/rust/components/panel_menu.rs +493 -0
  97. package/src/rust/components/panel_tab.rs +510 -0
  98. package/src/rust/components/plugin_tab.rs +1 -1
  99. package/src/rust/components/portal.rs +7 -0
  100. package/src/rust/components/settings_panel.rs +13 -5
  101. package/src/rust/components/status_bar.rs +110 -178
  102. package/src/rust/components/status_indicator.rs +6 -9
  103. package/src/rust/components/style/mod.rs +5 -0
  104. package/src/rust/components/style/style_provider.rs +2 -0
  105. package/src/rust/components/viewer/filters.rs +133 -0
  106. package/src/rust/components/viewer/msg.rs +182 -0
  107. package/src/rust/components/viewer/panels.rs +326 -0
  108. package/src/rust/components/viewer/render.rs +255 -0
  109. package/src/rust/components/viewer/settings.rs +434 -0
  110. package/src/rust/components/viewer/snapshots.rs +134 -0
  111. package/src/rust/components/viewer/wiring.rs +505 -0
  112. package/src/rust/components/viewer.rs +245 -756
  113. package/src/rust/components/window_editor.rs +1013 -0
  114. package/src/rust/config/export_method.rs +1 -1
  115. package/src/rust/config/mod.rs +4 -0
  116. package/src/rust/config/options.rs +71 -0
  117. package/src/rust/config/viewer_config.rs +24 -2
  118. package/src/rust/config/workspace_config.rs +92 -0
  119. package/src/rust/custom_elements/copy_dropdown.rs +3 -1
  120. package/src/rust/custom_elements/debug_plugin.rs +20 -1
  121. package/src/rust/custom_elements/export_dropdown.rs +3 -1
  122. package/src/rust/custom_elements/viewer.rs +1015 -286
  123. package/src/rust/custom_events.rs +212 -61
  124. package/src/rust/js/mod.rs +2 -0
  125. package/src/rust/js/plugin.rs +297 -10
  126. package/src/rust/js/regular_layout.rs +258 -0
  127. package/src/rust/lib.rs +24 -2
  128. package/src/rust/presentation/column_locator.rs +16 -1
  129. package/src/rust/presentation.rs +66 -33
  130. package/src/rust/queries/columns_iter_set.rs +7 -1
  131. package/src/rust/queries/get_viewer_config.rs +29 -11
  132. package/src/rust/queries/plugin_column_styles.rs +2 -2
  133. package/src/rust/renderer/activate.rs +41 -14
  134. package/src/rust/renderer/dispatch.rs +463 -0
  135. package/src/rust/renderer/plugin_config.rs +495 -0
  136. package/src/rust/renderer/registry.rs +18 -6
  137. package/src/rust/renderer/render_timer.rs +29 -10
  138. package/src/rust/renderer.rs +384 -838
  139. package/src/rust/session/column_defaults_update.rs +26 -8
  140. package/src/rust/session/drag_drop_update.rs +18 -0
  141. package/src/rust/session/metadata.rs +66 -9
  142. package/src/rust/session/replace_expression_update.rs +1 -0
  143. package/src/rust/session/view_subscription.rs +30 -15
  144. package/src/rust/session.rs +755 -264
  145. package/src/rust/tasks/apply_global_filters.rs +89 -0
  146. package/src/rust/tasks/auto_pause.rs +171 -0
  147. package/src/rust/tasks/copy_export.rs +29 -19
  148. package/src/rust/tasks/create_panel.rs +243 -0
  149. package/src/rust/tasks/dismiss_render_warning.rs +3 -1
  150. package/src/rust/tasks/edit_expression.rs +4 -4
  151. package/src/rust/tasks/edit_window.rs +155 -0
  152. package/src/rust/tasks/eject.rs +64 -23
  153. package/src/rust/tasks/mod.rs +30 -4
  154. package/src/rust/tasks/pipeline.rs +411 -0
  155. package/src/rust/tasks/presize_panels.rs +324 -0
  156. package/src/rust/tasks/reset_all.rs +22 -12
  157. package/src/rust/tasks/resize_observer.rs +124 -87
  158. package/src/rust/tasks/restore_and_render.rs +80 -86
  159. package/src/rust/tasks/restore_panel.rs +145 -0
  160. package/src/rust/tasks/send_column_config.rs +5 -2
  161. package/src/rust/tasks/send_plugin_config.rs +5 -2
  162. package/src/rust/tasks/set_edit_mode.rs +59 -0
  163. package/src/rust/tasks/sync_update_panels.rs +152 -0
  164. package/src/rust/tasks/table_lifecycle.rs +158 -0
  165. package/src/rust/tasks/update_theme.rs +56 -12
  166. package/src/rust/utils/browser/dragdrop.rs +12 -0
  167. package/src/rust/utils/browser/tests/debounce.rs +49 -0
  168. package/src/rust/utils/completion.rs +56 -0
  169. package/src/rust/utils/custom_element.rs +5 -0
  170. package/src/rust/utils/debounce.rs +189 -17
  171. package/src/rust/utils/mod.rs +6 -0
  172. package/src/rust/utils/pending_effects.rs +62 -0
  173. package/src/rust/utils/spawn.rs +33 -0
  174. package/src/rust/utils/weak_scope.rs +0 -10
  175. package/src/rust/workspace.rs +846 -0
  176. package/src/svg/active-indicator.svg +3 -0
  177. package/src/svg/pin-icon.svg +5 -0
  178. package/src/themes/botanical.css +5 -2
  179. package/src/themes/defaults.css +54 -2
  180. package/src/themes/dracula.css +6 -3
  181. package/src/themes/gruvbox-dark.css +5 -2
  182. package/src/themes/gruvbox.css +5 -2
  183. package/src/themes/icons.css +5 -2
  184. package/src/themes/intl/de.css +7 -0
  185. package/src/themes/intl/es.css +7 -0
  186. package/src/themes/intl/fr.css +7 -0
  187. package/src/themes/intl/ja.css +7 -0
  188. package/src/themes/intl/pt.css +7 -0
  189. package/src/themes/intl/zh.css +7 -0
  190. package/src/themes/intl.css +10 -1
  191. package/src/themes/monokai.css +5 -2
  192. package/src/themes/phosphor.css +5 -2
  193. package/src/themes/pro-dark.css +5 -2
  194. package/src/themes/pro.css +2 -1
  195. package/src/themes/solarized-dark.css +5 -2
  196. package/src/themes/solarized.css +5 -2
  197. package/src/themes/vaporwave.css +5 -2
  198. package/src/ts/bootstrap.ts +8 -7
  199. package/src/ts/extensions.ts +30 -2
  200. package/src/ts/perspective-viewer.ts +10 -0
  201. package/src/ts/plugin.ts +72 -5
  202. package/src/ts/ts-rs/ClientOptions.ts +7 -0
  203. package/src/ts/ts-rs/ExportMethod.ts +3 -0
  204. package/src/ts/ts-rs/ExportOptions.ts +8 -0
  205. package/src/ts/ts-rs/GetClientOptions.ts +7 -0
  206. package/src/ts/ts-rs/GetTableOptions.ts +7 -0
  207. package/src/ts/ts-rs/Layout.ts +9 -0
  208. package/src/ts/ts-rs/Orientation.ts +6 -0
  209. package/src/ts/ts-rs/PanelOptions.ts +7 -0
  210. package/src/ts/ts-rs/PanelViewerConfig.ts +16 -0
  211. package/src/ts/ts-rs/SplitLayout.ts +5 -0
  212. package/src/ts/ts-rs/TabLayout.ts +11 -0
  213. package/src/ts/ts-rs/ViewerConfig.ts +7 -2
  214. package/src/ts/ts-rs/ViewerConfigUpdate.ts +9 -0
  215. package/src/ts/ts-rs/WindowAggregate.ts +3 -0
  216. package/src/ts/ts-rs/WindowSort.ts +9 -0
  217. package/src/ts/ts-rs/WindowSortDir.ts +8 -0
  218. package/src/ts/ts-rs/WindowSpec.ts +46 -0
  219. package/src/ts/ts-rs/Windows.ts +10 -0
  220. package/src/ts/ts-rs/WorkspaceConfig.ts +36 -0
  221. package/src/ts/ts-rs/WorkspaceConfigUpdate.ts +24 -0
  222. package/src/rust/tasks/intersection_observer.rs +0 -98
  223. package/src/rust/tasks/update_and_render.rs +0 -110
  224. /package/dist/wasm/snippets/{perspective-viewer-93064d0d79855d97/inline0.js → perspective-viewer-8a8c7d5d696b43ae/inline1.js} +0 -0
  225. /package/dist/wasm/snippets/{perspective-viewer-93064d0d79855d97/inline1.js → perspective-viewer-8a8c7d5d696b43ae/inline2.js} +0 -0
  226. /package/dist/wasm/snippets/{perspective-viewer-93064d0d79855d97/inline2.js → perspective-viewer-8a8c7d5d696b43ae/inline3.js} +0 -0
  227. /package/dist/wasm/snippets/{perspective-viewer-93064d0d79855d97/inline4.js → perspective-viewer-8a8c7d5d696b43ae/inline5.js} +0 -0
@@ -11,7 +11,6 @@
11
11
  // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
12
 
13
13
  use perspective_client::config::*;
14
- use perspective_js::utils::ApiFuture;
15
14
  use web_sys::*;
16
15
  use yew::prelude::*;
17
16
 
@@ -20,6 +19,7 @@ use crate::components::type_icon::TypeIcon;
20
19
  use crate::presentation::Presentation;
21
20
  use crate::renderer::*;
22
21
  use crate::session::*;
22
+ use crate::tasks::apply_and_render;
23
23
  use crate::utils::*;
24
24
 
25
25
  #[derive(Properties)]
@@ -86,11 +86,8 @@ impl Component for SortColumn {
86
86
 
87
87
  let session = ctx.props().session.clone();
88
88
  let renderer = ctx.props().renderer.clone();
89
- if session.update_view_config(update).is_ok() {
90
- ApiFuture::spawn(async move {
91
- renderer.apply_pending_plugin()?;
92
- renderer.draw(session.validate().await?.create_view()).await
93
- });
89
+ if let Ok(task) = apply_and_render(&session, &renderer, update) {
90
+ spawn_owned("sort-column", task);
94
91
  }
95
92
 
96
93
  false
@@ -13,6 +13,7 @@
13
13
  mod active_column;
14
14
  mod add_expression_button;
15
15
  mod aggregate_selector;
16
+ mod column_selector_column_row;
16
17
  mod config_selector;
17
18
  mod empty_column;
18
19
  mod expr_edit_button;
@@ -25,10 +26,10 @@ mod sort_column;
25
26
  use std::iter::*;
26
27
  use std::rc::Rc;
27
28
 
29
+ pub use column_selector_column_row::*;
28
30
  pub use empty_column::*;
29
31
  pub use invalid_column::*;
30
32
  use perspective_client::config::ViewConfig;
31
- use perspective_js::utils::ApiFuture;
32
33
  pub use pivot_column::*;
33
34
  use web_sys::*;
34
35
  use yew::prelude::*;
@@ -46,6 +47,7 @@ use crate::queries::{ActiveColumnState, ActiveColumnStateData, ColumnsIteratorSe
46
47
  use crate::renderer::*;
47
48
  use crate::session::drag_drop_update::*;
48
49
  use crate::session::*;
50
+ use crate::tasks::apply_and_render;
49
51
  use crate::utils::*;
50
52
 
51
53
  #[derive(Properties)]
@@ -218,11 +220,8 @@ impl Component for ColumnSelector {
218
220
 
219
221
  let session = ctx.props().session.clone();
220
222
  let renderer = ctx.props().renderer.clone();
221
- if session.update_view_config(update).is_ok() {
222
- ApiFuture::spawn(async move {
223
- renderer.apply_pending_plugin()?;
224
- renderer.draw(session.validate().await?.create_view()).await
225
- });
223
+ if let Ok(task) = apply_and_render(&session, &renderer, update) {
224
+ spawn_owned("column-selector", task);
226
225
  }
227
226
  }
228
227
 
@@ -246,11 +245,8 @@ impl Component for ColumnSelector {
246
245
 
247
246
  let session = ctx.props().session.clone();
248
247
  let renderer = ctx.props().renderer.clone();
249
- if session.update_view_config(update).is_ok() {
250
- ApiFuture::spawn(async move {
251
- renderer.apply_pending_plugin()?;
252
- renderer.draw(session.validate().await?.create_view()).await
253
- });
248
+ if let Ok(task) = apply_and_render(&session, &renderer, update) {
249
+ spawn_owned("column-selector", task);
254
250
  }
255
251
 
256
252
  true
@@ -373,7 +369,9 @@ impl Component for ColumnSelector {
373
369
  let column_dropdown = self.column_dropdown.clone();
374
370
  let is_editing = matches!(
375
371
  &ctx.props().selected_column,
376
- Some(ColumnLocator::Table(x)) | Some(ColumnLocator::Expression(x))
372
+ Some(ColumnLocator::Table(x))
373
+ | Some(ColumnLocator::Expression(x))
374
+ | Some(ColumnLocator::Window(x))
377
375
  if x == &key );
378
376
 
379
377
  // Compute metadata-derived props here so that changes to
@@ -398,10 +396,15 @@ impl Component for ColumnSelector {
398
396
  .map(|n| metadata.is_column_expression(n))
399
397
  .unwrap_or(false);
400
398
 
399
+ let is_window = name
400
+ .get_name()
401
+ .map(|n| metadata.is_column_window(n))
402
+ .unwrap_or(false);
403
+
401
404
  let can_render_styles =
402
405
  name.get_name().is_some() && renderer.can_render_column_styles();
403
406
 
404
- let show_edit_btn = is_expression || can_render_styles;
407
+ let show_edit_btn = is_expression || is_window || can_render_styles;
405
408
  let on_open_expr_panel = &ctx.props().on_open_expr_panel;
406
409
  html_nested! {
407
410
  <ScrollPanelItem {key} {size_hint}>
@@ -411,6 +414,7 @@ impl Component for ColumnSelector {
411
414
  {is_aggregated}
412
415
  {is_editing}
413
416
  {is_expression}
417
+ {is_window}
414
418
  {show_edit_btn}
415
419
  {col_type}
416
420
  view_config={config.clone()}
@@ -431,12 +435,18 @@ impl Component for ColumnSelector {
431
435
 
432
436
  let mut inactive_children: Vec<_> = columns_iter
433
437
  .expression()
438
+ .chain(columns_iter.window())
434
439
  .chain(columns_iter.inactive())
435
440
  .enumerate()
436
441
  .map(|(idx, vc)| {
437
442
  let selected_column = ctx.props().selected_column.as_ref();
438
- let is_editing = matches!(selected_column, Some(ColumnLocator::Expression(x)) if x.as_str() == vc.name);
443
+ let is_editing = matches!(
444
+ selected_column,
445
+ Some(ColumnLocator::Expression(x)) | Some(ColumnLocator::Window(x))
446
+ if x.as_str() == vc.name
447
+ );
439
448
  let is_expression = metadata.is_column_expression(vc.name);
449
+ let is_window = metadata.is_column_window(vc.name);
440
450
  html_nested! {
441
451
  <ScrollPanelItem key={vc.name} size_hint=28.0>
442
452
  <InactiveColumn
@@ -445,6 +455,7 @@ impl Component for ColumnSelector {
445
455
  name={vc.name.to_owned()}
446
456
  {is_editing}
447
457
  {is_expression}
458
+ {is_window}
448
459
  view_config={config.clone()}
449
460
  metadata={metadata.clone()}
450
461
  onselect={&onselect}
@@ -326,7 +326,8 @@ pub fn StyleTab(props: &StyleTabProps) -> Html {
326
326
  },
327
327
  };
328
328
 
329
- Some(html! { <fieldset class="style-control">{ component }</fieldset> })
329
+ let key = format!("{}::{}", props.column_name, keys.join("+"));
330
+ Some(html! { <fieldset class="style-control" {key}>{ component }</fieldset> })
330
331
  })
331
332
  .collect_vec()
332
333
  })
@@ -0,0 +1,34 @@
1
+ // ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
2
+ // ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
3
+ // ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
4
+ // ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
5
+ // ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
6
+ // ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
7
+ // ┃ Copyright (c) 2017, the Perspective Authors. ┃
8
+ // ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
9
+ // ┃ This file is part of the Perspective library, distributed under the terms ┃
10
+ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11
+ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
+
13
+ use yew::prelude::*;
14
+
15
+ use super::save_settings::{SaveSettings, SaveSettingsProps};
16
+ use crate::components::window_editor::{WindowEditor, WindowEditorProps};
17
+
18
+ #[derive(PartialEq, Properties, Clone)]
19
+ pub struct WindowTabProps {
20
+ pub editor: WindowEditorProps,
21
+ pub save_section: SaveSettingsProps,
22
+ }
23
+
24
+ #[function_component]
25
+ pub fn WindowTab(p: &WindowTabProps) -> Html {
26
+ html! {
27
+ <div id="window-tab">
28
+ <div class="tab-section" id="window-editor-section">
29
+ <WindowEditor ..p.editor.clone() />
30
+ </div>
31
+ <div class="tab-section"><SaveSettings ..p.save_section.clone() /></div>
32
+ </div>
33
+ }
34
+ }
@@ -13,12 +13,13 @@ mod attributes_tab;
13
13
 
14
14
  mod save_settings;
15
15
  pub(crate) mod style_tab;
16
+ mod window_tab;
16
17
 
17
18
  use std::rc::Rc;
18
19
 
19
20
  use derivative::Derivative;
20
21
  use itertools::Itertools;
21
- use perspective_client::config::{ColumnType, Expression, ViewConfig};
22
+ use perspective_client::config::{ColumnType, Expression, ViewConfig, WindowSpec};
22
23
  use perspective_client::utils::PerspectiveResultExt;
23
24
  use yew::{Callback, Component, Html, Properties, html, props};
24
25
 
@@ -27,15 +28,19 @@ use self::style_tab::StyleTabProps;
27
28
  use crate::components::column_settings_sidebar::attributes_tab::AttributesTab;
28
29
  use crate::components::column_settings_sidebar::save_settings::SaveSettingsProps;
29
30
  use crate::components::column_settings_sidebar::style_tab::StyleTab;
31
+ use crate::components::column_settings_sidebar::window_tab::{WindowTab, WindowTabProps};
30
32
  use crate::components::containers::sidebar::Sidebar;
31
33
  use crate::components::containers::tab_list::TabList;
32
34
  use crate::components::editable_header::EditableHeaderProps;
33
35
  use crate::components::expression_editor::ExpressionEditorProps;
34
36
  use crate::components::type_icon::TypeIconType;
37
+ use crate::components::window_editor::WindowEditorProps;
35
38
  use crate::presentation::{ColumnLocator, ColumnSettingsTab, Presentation};
36
39
  use crate::renderer::Renderer;
37
40
  use crate::session::{Session, SessionMetadataRc};
38
- use crate::tasks::{delete_expr, save_expr, update_expr};
41
+ use crate::tasks::{
42
+ delete_expr, delete_window, save_expr, save_window, update_expr, update_window,
43
+ };
39
44
  use crate::utils::PtrEqRc;
40
45
 
41
46
  #[derive(Clone, Derivative, Properties)]
@@ -47,9 +52,22 @@ pub struct ColumnSettingsPanelProps {
47
52
  pub width_override: Option<i32>,
48
53
  pub on_select_tab: Callback<ColumnSettingsTab>,
49
54
 
50
- /// Active plugin name threaded as a value prop so that plugin changes
51
- /// trigger re-initialization via `changed()` rather than a PubSub
52
- /// `render_limits_changed` subscription.
55
+ /// Shared trap-door width across the drawer's Style/Attributes/Window
56
+ /// tabs.
57
+ #[prop_or_default]
58
+ pub auto_width: f64,
59
+
60
+ #[prop_or_default]
61
+ pub on_auto_width: Callback<f64>,
62
+
63
+ /// Whether the drawer is pinned into the layout.
64
+ #[prop_or_default]
65
+ pub is_pinned: bool,
66
+
67
+ #[prop_or_default]
68
+ pub on_toggle_pin: Callback<()>,
69
+
70
+ /// Active plugin name.
53
71
  pub plugin_name: Option<String>,
54
72
 
55
73
  /// Session metadata snapshot — threaded from `SessionProps`.
@@ -75,8 +93,10 @@ pub struct ColumnSettingsPanelProps {
75
93
  pub session: Session,
76
94
  }
77
95
 
78
- impl PartialEq for ColumnSettingsPanelProps {
79
- fn eq(&self, other: &Self) -> bool {
96
+ impl ColumnSettingsPanelProps {
97
+ /// Everything EXCEPT the trap-door `auto_width`: the props whose change
98
+ /// invalidates the drafts that `initialize` rebuilds.
99
+ fn identity_eq(&self, other: &Self) -> bool {
80
100
  self.selected_column == other.selected_column
81
101
  && self.selected_tab == other.selected_tab
82
102
  && self.plugin_name == other.plugin_name
@@ -87,12 +107,21 @@ impl PartialEq for ColumnSettingsPanelProps {
87
107
  }
88
108
  }
89
109
 
110
+ impl PartialEq for ColumnSettingsPanelProps {
111
+ fn eq(&self, other: &Self) -> bool {
112
+ self.identity_eq(other)
113
+ && self.auto_width == other.auto_width
114
+ && self.is_pinned == other.is_pinned
115
+ }
116
+ }
117
+
90
118
  #[derive(Debug)]
91
119
  pub enum ColumnSettingsPanelMsg {
92
120
  SetExprValue(Rc<String>),
93
121
  SetExprValid(bool),
94
122
  SetHeaderValue(Option<String>),
95
123
  SetHeaderValid(bool),
124
+ SetWindowValue(Option<WindowSpec>),
96
125
  SetSelectedTab((usize, ColumnSettingsTab)),
97
126
  OnSaveAttributes(()),
98
127
  OnResetAttributes(()),
@@ -117,6 +146,8 @@ pub struct ColumnSettingsPanel {
117
146
  reset_enabled: bool,
118
147
  save_count: u8,
119
148
  save_enabled: bool,
149
+ initial_window_value: Option<WindowSpec>,
150
+ window_value: Option<WindowSpec>,
120
151
  tabs: Vec<ColumnSettingsTab>,
121
152
  }
122
153
 
@@ -138,6 +169,8 @@ impl Component for ColumnSettingsPanel {
138
169
  reset_count: 0,
139
170
  column_name: "".to_owned(),
140
171
  maybe_ty: None,
172
+ initial_window_value: None,
173
+ window_value: None,
141
174
  tabs: vec![],
142
175
  on_input: Callback::default(),
143
176
  on_save: Callback::default(),
@@ -149,12 +182,15 @@ impl Component for ColumnSettingsPanel {
149
182
  }
150
183
 
151
184
  fn changed(&mut self, ctx: &yew::prelude::Context<Self>, old_props: &Self::Properties) -> bool {
152
- if ctx.props() != old_props {
185
+ // Only reached when props are unequal. Re-`initialize` (which wipes
186
+ // in-progress expression/window drafts) only on IDENTITY changes -
187
+ // a trap-door `auto_width` change re-renders the `Sidebar` sizer
188
+ // alone.
189
+ if !ctx.props().identity_eq(old_props) {
153
190
  self.initialize(ctx);
154
- true
155
- } else {
156
- false
157
191
  }
192
+
193
+ true
158
194
  }
159
195
 
160
196
  fn update(&mut self, ctx: &yew::prelude::Context<Self>, msg: Self::Message) -> bool {
@@ -187,6 +223,15 @@ impl Component for ColumnSettingsPanel {
187
223
  self.save_enabled_effect();
188
224
  true
189
225
  },
226
+ ColumnSettingsPanelMsg::SetWindowValue(val) => {
227
+ if self.window_value != val {
228
+ self.window_value = val;
229
+ self.reset_enabled = true;
230
+ true
231
+ } else {
232
+ false
233
+ }
234
+ },
190
235
  ColumnSettingsPanelMsg::SetSelectedTab((_, val)) => {
191
236
  let rerender = ctx.props().selected_tab != Some(val);
192
237
  ctx.props().on_select_tab.emit(val);
@@ -195,19 +240,58 @@ impl Component for ColumnSettingsPanel {
195
240
  ColumnSettingsPanelMsg::OnResetAttributes(()) => {
196
241
  self.header_value.clone_from(&self.initial_header_value);
197
242
  self.expr_value.clone_from(&self.initial_expr_value);
243
+ self.window_value.clone_from(&self.initial_window_value);
198
244
  self.save_enabled = false;
199
245
  self.reset_enabled = false;
200
246
  self.reset_count += 1;
201
247
  true
202
248
  },
203
249
  ColumnSettingsPanelMsg::OnSaveAttributes(()) => {
250
+ if matches!(ctx.props().selected_tab, Some(ColumnSettingsTab::Window)) {
251
+ if let Some(spec) = self.window_value.clone() {
252
+ let name = self
253
+ .header_value
254
+ .clone()
255
+ .unwrap_or_else(|| self.column_name.clone());
256
+ match &ctx.props().selected_column {
257
+ ColumnLocator::Window(old_name) => update_window(
258
+ &ctx.props().session,
259
+ &ctx.props().renderer,
260
+ &ctx.props().presentation,
261
+ old_name.clone(),
262
+ name,
263
+ spec.clone(),
264
+ ),
265
+ _ => {
266
+ if let Err(err) = save_window(
267
+ &ctx.props().session,
268
+ &ctx.props().renderer,
269
+ &ctx.props().presentation,
270
+ name,
271
+ spec.clone(),
272
+ ) {
273
+ tracing::warn!("{}", err);
274
+ }
275
+ },
276
+ }
277
+
278
+ self.initial_window_value = Some(spec);
279
+ self.initial_header_value.clone_from(&self.header_value);
280
+ self.save_enabled = false;
281
+ self.reset_enabled = false;
282
+ self.save_count += 1;
283
+ }
284
+
285
+ return true;
286
+ }
287
+
204
288
  let new_expr = Expression::new(
205
289
  self.header_value.clone().map(|s| s.into()),
206
290
  (*(self.expr_value)).clone().into(),
207
291
  );
208
292
 
209
293
  match &ctx.props().selected_column {
210
- ColumnLocator::Table(_) => {
294
+ ColumnLocator::Table(_) | ColumnLocator::Window(_) => {
211
295
  tracing::error!("Tried to save non-expression column!")
212
296
  },
213
297
  ColumnLocator::Expression(name) => update_expr(
@@ -244,6 +328,13 @@ impl Component for ColumnSettingsPanel {
244
328
  &self.column_name,
245
329
  )
246
330
  .unwrap_or_log();
331
+ } else if ctx.props().selected_column.is_saved_window() {
332
+ delete_window(
333
+ &ctx.props().session,
334
+ &ctx.props().renderer,
335
+ &self.column_name,
336
+ )
337
+ .unwrap_or_log();
247
338
  }
248
339
 
249
340
  ctx.props().on_close.emit(());
@@ -253,15 +344,24 @@ impl Component for ColumnSettingsPanel {
253
344
  }
254
345
 
255
346
  fn view(&self, ctx: &yew::prelude::Context<Self>) -> Html {
347
+ let is_window_tab = matches!(ctx.props().selected_tab, Some(ColumnSettingsTab::Window));
348
+
349
+ let header_placeholder = if is_window_tab {
350
+ Rc::new(self.column_name.clone())
351
+ } else {
352
+ self.expr_value.clone()
353
+ };
354
+
256
355
  let header_props = props!(EditableHeaderProps {
257
356
  initial_value: self.initial_header_value.clone(),
258
- placeholder: self.expr_value.clone(),
357
+ placeholder: header_placeholder,
259
358
  reset_count: self.reset_count,
260
- editable: ctx.props().selected_column.is_expr()
359
+ editable: (ctx.props().selected_column.is_expr()
261
360
  && matches!(
262
361
  ctx.props().selected_tab,
263
362
  Some(ColumnSettingsTab::Attributes)
264
- ),
363
+ ))
364
+ || (ctx.props().selected_column.is_window_editable() && is_window_tab),
265
365
  update_on_input: true,
266
366
  icon_type: self
267
367
  .maybe_ty
@@ -324,7 +424,26 @@ impl Component for ColumnSettingsPanel {
324
424
 
325
425
  let attrs_tab = AttributesTabProps {
326
426
  expr_editor,
327
- save_section,
427
+ save_section: save_section.clone(),
428
+ };
429
+
430
+ let window_changed = self.window_value != self.initial_window_value
431
+ || self.header_value != self.initial_header_value;
432
+ let window_tab = WindowTabProps {
433
+ editor: WindowEditorProps {
434
+ metadata: ctx.props().metadata.clone(),
435
+ initial: self.initial_window_value.clone(),
436
+ on_change: ctx.link().callback(ColumnSettingsPanelMsg::SetWindowValue),
437
+ reset_count: self.reset_count,
438
+ presentation: ctx.props().presentation.clone(),
439
+ session: ctx.props().session.clone(),
440
+ selected_theme: ctx.props().selected_theme.clone(),
441
+ },
442
+ save_section: SaveSettingsProps {
443
+ save_enabled: self.window_value.is_some() && window_changed && self.header_valid,
444
+ show_danger_zone: ctx.props().selected_column.is_saved_window(),
445
+ ..save_section
446
+ },
328
447
  };
329
448
 
330
449
  let style_tab = StyleTabProps {
@@ -342,6 +461,7 @@ impl Component for ColumnSettingsPanel {
342
461
 
343
462
  let tab_children = self.tabs.iter().map(|tab| match tab {
344
463
  ColumnSettingsTab::Attributes => html! { <AttributesTab ..attrs_tab.clone() /> },
464
+ ColumnSettingsTab::Window => html! { <WindowTab ..window_tab.clone() /> },
345
465
  ColumnSettingsTab::Style => html! { <StyleTab ..style_tab.clone() /> },
346
466
  });
347
467
 
@@ -358,6 +478,10 @@ impl Component for ColumnSettingsPanel {
358
478
  on_close={ctx.props().on_close.clone()}
359
479
  id_prefix="column_settings"
360
480
  width_override={ctx.props().width_override}
481
+ auto_width={ctx.props().auto_width}
482
+ on_auto_width={ctx.props().on_auto_width.clone()}
483
+ is_pinned={ctx.props().is_pinned}
484
+ on_toggle_pin={Some(ctx.props().on_toggle_pin.clone())}
361
485
  selected_tab={selected_tab_idx}
362
486
  {header_props}
363
487
  >
@@ -403,6 +527,16 @@ impl ColumnSettingsPanel {
403
527
  .metadata
404
528
  .locator_view_type(&ctx.props().selected_column);
405
529
 
530
+ // Specs are unnamed (the `windows` map key names them, and the
531
+ // editable header owns naming in the UI), so drafts and saved specs
532
+ // compare directly.
533
+ let initial_window_value = ctx
534
+ .props()
535
+ .selected_column
536
+ .name()
537
+ .and_then(|name| ctx.props().view_config.windows.get(name))
538
+ .cloned();
539
+
406
540
  let tabs = {
407
541
  let mut tabs = vec![];
408
542
  let is_new_expr = ctx.props().selected_column.is_new_expr();
@@ -424,6 +558,17 @@ impl ColumnSettingsPanel {
424
558
  tabs.push(ColumnSettingsTab::Attributes);
425
559
  }
426
560
 
561
+ let supports_windows = ctx
562
+ .props()
563
+ .metadata
564
+ .get_features()
565
+ .map(|x| x.has_window_aggregates())
566
+ .unwrap_or_default();
567
+
568
+ if ctx.props().selected_column.is_window_editable() && supports_windows {
569
+ tabs.push(ColumnSettingsTab::Window);
570
+ }
571
+
427
572
  tabs
428
573
  };
429
574
 
@@ -440,6 +585,8 @@ impl ColumnSettingsPanel {
440
585
  header_value: initial_header_value.clone(),
441
586
  initial_header_value,
442
587
  maybe_ty,
588
+ window_value: initial_window_value.clone(),
589
+ initial_window_value,
443
590
  tabs,
444
591
  header_valid: true,
445
592
  on_input,
@@ -52,6 +52,21 @@ where
52
52
 
53
53
  #[prop_or_default]
54
54
  pub allow_duplicates: bool,
55
+
56
+ /// Single-slot mode: the list holds at most one item, a dragover
57
+ /// preview REPLACES the current item rather than inserting beside it,
58
+ /// and the trailing `EmptyColumn` autocomplete renders only while the
59
+ /// slot is empty.
60
+ #[prop_or_default]
61
+ pub single_slot: bool,
62
+
63
+ /// The in-flight drag is INVALID for this list (a parent-defined rule,
64
+ /// e.g. the window editor's Table-columns-only slots): the dragover
65
+ /// preview is suppressed and the invalid-X overlay renders instead,
66
+ /// like a duplicate drag over `group_by`/`split_by`. The parent's drop
67
+ /// handler is still responsible for ignoring the drop itself.
68
+ #[prop_or_default]
69
+ pub is_invalid: bool,
55
70
  }
56
71
 
57
72
  impl<T, U> PartialEq for DragDropListProps<T, U>
@@ -66,6 +81,8 @@ where
66
81
  && self.allow_duplicates == other.allow_duplicates
67
82
  && self.is_dragover == other.is_dragover
68
83
  && self.disabled == other.disabled
84
+ && self.single_slot == other.single_slot
85
+ && self.is_invalid == other.is_invalid
69
86
  }
70
87
  }
71
88
 
@@ -202,7 +219,51 @@ where
202
219
  let invalid_drag: bool;
203
220
  let mut valid_duplicate_drag = false;
204
221
 
205
- let columns_html = {
222
+ let columns_html = if ctx.props().single_slot {
223
+ invalid_drag = ctx.props().is_invalid && ctx.props().is_dragover.is_some();
224
+
225
+ // Dragging the slot's own pill over its own slot is a no-op
226
+ // move - keep showing the pill instead of the drop preview
227
+ // (mirrors the multi-column branch's `is_self_move` handling).
228
+ let is_self_move = ctx
229
+ .props()
230
+ .presentation
231
+ .get_drag_target()
232
+ .map(|x| V::is_self_move(x))
233
+ .unwrap_or_default();
234
+
235
+ let close = ctx.props().parent.callback(|_| V::close(0));
236
+ let dragenter = ctx.props().parent.callback({
237
+ let container_noderef = container_noderef.clone();
238
+ move |event: DragEvent| {
239
+ event.stop_propagation();
240
+ event.prevent_default();
241
+ if event.related_target().is_none()
242
+ && let Some(elem) = container_noderef.cast::<HtmlElement>()
243
+ {
244
+ let _ = elem.dataset().set("safaridragleave", "true");
245
+ }
246
+ V::dragenter(0)
247
+ }
248
+ });
249
+
250
+ if ctx.props().is_dragover.is_some() && !is_self_move && !invalid_drag {
251
+ html! {
252
+ <div class="pivot-column" ondragenter={dragenter}>
253
+ <div class="config-drop" />
254
+ </div>
255
+ }
256
+ } else if let Some(column) = ctx.props().children.iter().next() {
257
+ html! {
258
+ <div class="pivot-column" ondragenter={dragenter}>
259
+ { Html::from(column) }
260
+ <span class="row_close" onmousedown={close} />
261
+ </div>
262
+ }
263
+ } else {
264
+ html! {}
265
+ }
266
+ } else {
206
267
  let mut columns = ctx
207
268
  .props()
208
269
  .children
@@ -211,7 +272,10 @@ where
211
272
  .enumerate()
212
273
  .collect::<Vec<_>>();
213
274
 
214
- invalid_drag = if let Some((x, column)) = &ctx.props().is_dragover {
275
+ invalid_drag = if ctx.props().is_invalid && ctx.props().is_dragover.is_some() {
276
+ // Parent-defined invalidity: no preview, X overlay only.
277
+ true
278
+ } else if let Some((x, column)) = &ctx.props().is_dragover {
215
279
  let index = *x;
216
280
  let is_append = index == columns.len();
217
281
  let is_self_move = ctx
@@ -314,6 +378,12 @@ where
314
378
  .collect::<Html>()
315
379
  };
316
380
 
381
+ let show_empty = if ctx.props().single_slot {
382
+ ctx.props().children.is_empty() && ctx.props().is_dragover.is_none()
383
+ } else {
384
+ ctx.props().is_dragover.is_none() | (!invalid_drag && valid_duplicate_drag)
385
+ };
386
+
317
387
  let column_dropdown = ctx.props().column_dropdown.clone();
318
388
  let exclude = ctx.props().exclude.clone();
319
389
  let on_select = ctx.props().parent.callback(V::create);
@@ -345,7 +415,7 @@ where
345
415
  onmousedown={ctx.props().parent.callback(move |_| V::close(0))}
346
416
  />
347
417
  </div>
348
- } else if ctx.props().is_dragover.is_none() | (!invalid_drag && valid_duplicate_drag) {
418
+ } else if show_empty {
349
419
  <EmptyColumn {column_dropdown} {exclude} {on_select} />
350
420
  } else if invalid_drag {
351
421
  <InvalidColumn />