@perspective-dev/viewer 5.5.1 → 5.6.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 (194) hide show
  1. package/README.md +82 -3
  2. package/dist/cdn/perspective-viewer.js +9 -9
  3. package/dist/cdn/perspective-viewer.js.map +3 -3
  4. package/dist/css/blueprint.css +1 -1
  5. package/dist/css/botanical.css +1 -1
  6. package/dist/css/dracula.css +1 -1
  7. package/dist/css/eggplant.css +1 -1
  8. package/dist/css/gruvbox-dark.css +1 -1
  9. package/dist/css/gruvbox.css +1 -1
  10. package/dist/css/icons.css +1 -1
  11. package/dist/css/intl/ar.css +1 -1
  12. package/dist/css/intl/de.css +1 -1
  13. package/dist/css/intl/es.css +1 -1
  14. package/dist/css/intl/fr.css +1 -1
  15. package/dist/css/intl/ja.css +1 -1
  16. package/dist/css/intl/pt.css +1 -1
  17. package/dist/css/intl/zh.css +1 -1
  18. package/dist/css/intl.css +1 -1
  19. package/dist/css/ledger.css +1 -1
  20. package/dist/css/monokai.css +1 -1
  21. package/dist/css/nord.css +1 -1
  22. package/dist/css/phosphor.css +1 -1
  23. package/dist/css/pro-dark.css +1 -1
  24. package/dist/css/pro.css +1 -1
  25. package/dist/css/solarized-dark.css +1 -1
  26. package/dist/css/solarized.css +1 -1
  27. package/dist/css/themes.css +1 -1
  28. package/dist/css/vaporwave.css +1 -1
  29. package/dist/css/velvet.css +1 -1
  30. package/dist/docs/perspective-docs.json +1 -1
  31. package/dist/esm/column-format.js +2 -0
  32. package/dist/esm/column-format.js.map +7 -0
  33. package/dist/esm/extensions.d.ts +3 -13
  34. package/dist/esm/perspective-viewer.d.ts +1 -1
  35. package/dist/esm/perspective-viewer.js +9 -9
  36. package/dist/esm/perspective-viewer.js.map +3 -3
  37. package/dist/esm/plugin.d.ts +4 -1
  38. package/dist/esm/select-detail.d.ts +15 -0
  39. package/dist/esm/select-detail.js +2 -0
  40. package/dist/esm/select-detail.js.map +7 -0
  41. package/dist/esm/ts-rs/DerivedTableConfig.d.ts +35 -0
  42. package/dist/esm/ts-rs/PanelViewerConfig.d.ts +7 -6
  43. package/dist/esm/ts-rs/RestoreOptions.d.ts +3 -7
  44. package/dist/esm/ts-rs/TableRef.d.ts +9 -0
  45. package/dist/esm/ts-rs/ViewerConfig.d.ts +7 -6
  46. package/dist/esm/ts-rs/ViewerConfigUpdate.d.ts +7 -5
  47. package/dist/esm/ts-rs/WorkspaceConfig.d.ts +9 -1
  48. package/dist/esm/ts-rs/WorkspaceConfigUpdate.d.ts +9 -0
  49. package/dist/wasm/perspective-viewer.d.ts +115 -35
  50. package/dist/wasm/perspective-viewer.js +314 -190
  51. package/dist/wasm/perspective-viewer.wasm +0 -0
  52. package/dist/wasm/perspective-viewer.wasm.d.ts +21 -17
  53. package/package.json +31 -18
  54. package/src/css/column-selector.css +205 -50
  55. package/src/css/ui/containers/context-menu.css +12 -1
  56. package/src/css/ui/containers/dropdown-menu-items.css +29 -0
  57. package/src/css/ui/containers/scroll-panel.css +4 -1
  58. package/src/rust/agent/client/protocol.rs +0 -89
  59. package/src/rust/agent/markdown/mod.rs +0 -238
  60. package/src/rust/agent/preamble.md +1 -0
  61. package/src/rust/agent/tools.rs +2 -16
  62. package/src/rust/components/column_selector/active_column.rs +15 -5
  63. package/src/rust/components/column_selector/add_expression_button.rs +9 -26
  64. package/src/rust/components/column_selector/config_selector.rs +34 -6
  65. package/src/rust/components/column_selector/derived_control.rs +229 -0
  66. package/src/rust/components/column_selector/expr_edit_button.rs +3 -2
  67. package/src/rust/components/column_selector/filter_column.rs +0 -2
  68. package/src/rust/components/column_selector/inactive_column.rs +4 -5
  69. package/src/rust/components/column_selector/pivot_column.rs +21 -1
  70. package/src/rust/components/column_selector/sort_column.rs +4 -3
  71. package/src/rust/components/column_selector.rs +21 -1
  72. package/src/rust/components/column_settings_sidebar/save_settings.rs +5 -3
  73. package/src/rust/components/column_settings_sidebar/style_tab.rs +1 -0
  74. package/src/rust/components/column_settings_sidebar.rs +25 -19
  75. package/src/rust/components/debug_panel.rs +38 -29
  76. package/src/rust/components/dragdrop_list.rs +8 -9
  77. package/src/rust/components/main_panel/msg.rs +6 -4
  78. package/src/rust/components/main_panel/reconcile.rs +6 -1
  79. package/src/rust/components/main_panel/render.rs +70 -51
  80. package/src/rust/components/main_panel/update.rs +39 -1
  81. package/src/rust/components/main_panel.rs +97 -59
  82. package/src/rust/components/new_panel_menu.rs +69 -1
  83. package/src/rust/components/panel_menu.rs +63 -12
  84. package/src/rust/components/plugin_tab.rs +3 -2
  85. package/src/rust/components/render_warning.rs +2 -1
  86. package/src/rust/components/settings_panel.rs +4 -4
  87. package/src/rust/components/status_bar.rs +74 -24
  88. package/src/rust/components/status_indicator.rs +8 -1
  89. package/src/rust/components/viewer/filters.rs +13 -1
  90. package/src/rust/components/viewer/msg.rs +4 -0
  91. package/src/rust/components/viewer/panels.rs +41 -1
  92. package/src/rust/components/viewer/render.rs +1 -0
  93. package/src/rust/components/viewer/settings.rs +0 -22
  94. package/src/rust/components/viewer/snapshots.rs +2 -2
  95. package/src/rust/components/viewer/wiring.rs +14 -0
  96. package/src/rust/components/viewer.rs +45 -25
  97. package/src/rust/components/window_editor.rs +5 -4
  98. package/src/rust/config/column_config_schema.rs +38 -377
  99. package/src/rust/config/number_string_format.rs +0 -94
  100. package/src/rust/config/options.rs +3 -7
  101. package/src/rust/config/plugin_static_config.rs +0 -42
  102. package/src/rust/config/viewer_config.rs +47 -17
  103. package/src/rust/config/workspace_config.rs +158 -128
  104. package/src/rust/custom_elements/viewer.rs +187 -131
  105. package/src/rust/custom_events.rs +33 -18
  106. package/src/rust/derived.rs +266 -0
  107. package/src/rust/exprtk/tokenize.rs +0 -54
  108. package/src/rust/js/plugin.rs +1 -1
  109. package/src/rust/lib.rs +3 -0
  110. package/src/rust/presentation.rs +23 -4
  111. package/src/rust/queries/column_locator.rs +55 -48
  112. package/src/rust/queries/get_viewer_config.rs +1 -1
  113. package/src/rust/queries/plugin_column_styles.rs +22 -23
  114. package/src/rust/renderer/dispatch.rs +2 -0
  115. package/src/rust/renderer/plugin_config.rs +652 -487
  116. package/src/rust/renderer/props.rs +4 -0
  117. package/src/rust/renderer/registry.rs +2 -6
  118. package/src/rust/renderer/state.rs +205 -0
  119. package/src/rust/renderer.rs +201 -59
  120. package/src/rust/session/metadata.rs +30 -9
  121. package/src/rust/session/op_queue.rs +430 -0
  122. package/src/rust/session/panel_state.rs +327 -0
  123. package/src/rust/session/view_subscription.rs +42 -3
  124. package/src/rust/session.rs +900 -395
  125. package/src/rust/tasks/apply_global_filters.rs +49 -35
  126. package/src/rust/tasks/auto_pause.rs +22 -12
  127. package/src/rust/tasks/copy_export.rs +5 -0
  128. package/src/rust/tasks/create_panel.rs +34 -9
  129. package/src/rust/tasks/derive.rs +312 -0
  130. package/src/rust/tasks/mod.rs +6 -2
  131. package/src/rust/tasks/pipeline.rs +145 -89
  132. package/src/rust/tasks/reset_all.rs +31 -20
  133. package/src/rust/tasks/restore_derived.rs +200 -0
  134. package/src/rust/tasks/restore_panel.rs +153 -141
  135. package/src/rust/tasks/send_column_config.rs +56 -29
  136. package/src/rust/tasks/send_plugin_config.rs +52 -23
  137. package/src/rust/tasks/set_edit_mode.rs +33 -25
  138. package/src/rust/tasks/sync_update_panels.rs +39 -12
  139. package/src/rust/tasks/table_lifecycle.rs +70 -34
  140. package/src/rust/tasks/transactional_restore.rs +387 -0
  141. package/src/rust/tasks/update_theme.rs +45 -6
  142. package/src/rust/ui/containers/scroll_panel.rs +17 -10
  143. package/src/rust/ui/containers/split_panel.rs +0 -41
  144. package/src/rust/ui/context_menu.rs +157 -18
  145. package/src/rust/ui/font_loader.rs +23 -35
  146. package/src/rust/ui/form/mirrored_textarea.rs +0 -12
  147. package/src/rust/ui/form/multi_stop_gradient_selector.rs +0 -89
  148. package/src/rust/ui/form/palette_selector.rs +0 -51
  149. package/src/rust/ui/portal.rs +4 -10
  150. package/src/rust/utils/browser/mod.rs +2 -3
  151. package/src/{ts/perspective-viewer.inline.ts → rust/utils/browser/mouse.rs} +11 -10
  152. package/src/rust/utils/browser/request_animation_frame.rs +27 -8
  153. package/src/rust/utils/css.rs +0 -360
  154. package/src/rust/utils/local_fonts.rs +0 -36
  155. package/src/rust/utils/mod.rs +2 -3
  156. package/src/rust/utils/modal_position.rs +15 -21
  157. package/src/rust/utils/{browser/tests/mod.rs → plugin_host_state.rs} +31 -1
  158. package/src/rust/utils/pubsub.rs +0 -1
  159. package/src/rust/workspace.rs +187 -140
  160. package/src/svg/derived.svg +6 -0
  161. package/src/themes/icons.css +1 -0
  162. package/src/themes/intl/ar.css +10 -0
  163. package/src/themes/intl/de.css +10 -0
  164. package/src/themes/intl/es.css +10 -0
  165. package/src/themes/intl/fr.css +10 -0
  166. package/src/themes/intl/ja.css +10 -0
  167. package/src/themes/intl/pt.css +10 -0
  168. package/src/themes/intl/zh.css +10 -0
  169. package/src/themes/intl.css +10 -0
  170. package/src/ts/extensions.ts +3 -36
  171. package/src/ts/perspective-viewer.ts +1 -1
  172. package/src/ts/plugin.ts +4 -1
  173. package/src/{rust/utils/tests/request_animation_frame.rs → ts/select-detail.ts} +32 -26
  174. package/src/ts/ts-rs/DerivedTableConfig.ts +22 -0
  175. package/src/ts/ts-rs/PanelViewerConfig.ts +7 -6
  176. package/src/ts/ts-rs/RestoreOptions.ts +3 -7
  177. package/src/ts/ts-rs/TableRef.ts +9 -0
  178. package/src/ts/ts-rs/ViewerConfig.ts +7 -6
  179. package/src/ts/ts-rs/ViewerConfigUpdate.ts +7 -5
  180. package/src/ts/ts-rs/WorkspaceConfig.ts +7 -1
  181. package/src/ts/ts-rs/WorkspaceConfigUpdate.ts +7 -0
  182. package/dist/esm/perspective-viewer.inline.d.ts +0 -3
  183. package/dist/esm/perspective-viewer.inline.js +0 -21
  184. package/dist/esm/perspective-viewer.inline.js.map +0 -7
  185. package/src/rust/tasks/restore_and_render.rs +0 -107
  186. package/src/rust/utils/browser/tests/debounce.rs +0 -163
  187. package/src/rust/utils/tests/mod.rs +0 -14
  188. package/src/rust/utils/tests/pubsub.rs +0 -95
  189. /package/dist/wasm/snippets/{perspective-viewer-6813b568219c6763 → perspective-viewer-d0d525977a1acba2}/inline0.js +0 -0
  190. /package/dist/wasm/snippets/{perspective-viewer-6813b568219c6763 → perspective-viewer-d0d525977a1acba2}/inline1.js +0 -0
  191. /package/dist/wasm/snippets/{perspective-viewer-6813b568219c6763 → perspective-viewer-d0d525977a1acba2}/inline2.js +0 -0
  192. /package/dist/wasm/snippets/{perspective-viewer-6813b568219c6763 → perspective-viewer-d0d525977a1acba2}/inline3.js +0 -0
  193. /package/dist/wasm/snippets/{perspective-viewer-6813b568219c6763 → perspective-viewer-d0d525977a1acba2}/inline4.js +0 -0
  194. /package/dist/wasm/snippets/{perspective-viewer-6813b568219c6763 → perspective-viewer-d0d525977a1acba2}/inline5.js +0 -0
@@ -103,8 +103,8 @@ fn dispatch_column_settings_open_changed(
103
103
  event_init.set_detail(&JsValue::from(
104
104
  json!({"open": open, "column_name": column_name}),
105
105
  ));
106
- // Bubble so the event still reaches the host (and any public listeners) even
107
- // though it's dispatched on a panel's plugin below it.
106
+ // Bubble so the event still reaches the host (and any public listeners)
107
+ // even though it's dispatched on a panel's plugin below it.
108
108
  event_init.set_bubbles(true);
109
109
  let event = web_sys::CustomEvent::new_with_event_init_dict(
110
110
  "perspective-toggle-column-settings",
@@ -116,8 +116,8 @@ fn dispatch_column_settings_open_changed(
116
116
  // viewer hosts every panel's plugin under one element, and each datagrid
117
117
  // listens for this event on itself — so dispatching on the host (which the
118
118
  // event bubbles to) would highlight EVERY datagrid. The column settings
119
- // apply to the active panel, so scope the highlight to its plugin; fall back
120
- // to the host if none is drawn.
119
+ // apply to the active panel, so scope the highlight to its plugin; fall
120
+ // back to the host if none is drawn.
121
121
  let target: web_sys::EventTarget = workspace
122
122
  .active_id()
123
123
  .and_then(|id| workspace.panel(&id))
@@ -149,19 +149,21 @@ fn dispatch_config_update(
149
149
  session: &Session,
150
150
  renderer: &Renderer,
151
151
  presentation: &Presentation,
152
+ workspace: &Workspace,
152
153
  ) {
153
154
  if session.get_table().is_none() && session.pending_table().is_none() {
154
155
  return;
155
156
  }
156
157
 
157
- clone!(session, renderer, presentation);
158
+ clone!(session, renderer, presentation, workspace);
158
159
  let elem = elem.clone();
159
160
  let tracker = session.clone();
160
161
  tracker.track_dispatch(async move {
161
162
  let viewer_config = renderer
162
163
  .clone()
163
164
  .with_lock(async { get_viewer_config(&session, &renderer, &presentation).await })
164
- .await?;
165
+ .await?
166
+ .map_table(|name| workspace.inline_table_ref(&name));
165
167
 
166
168
  if viewer_config.view_config != Default::default()
167
169
  && Some(&viewer_config) != session.last_dispatched_config.borrow().as_deref()
@@ -215,7 +217,13 @@ pub fn wire_element_events(
215
217
  clone!(elem, presentation, workspace);
216
218
  move |_| {
217
219
  if let Some(panel) = workspace.active_panel() {
218
- dispatch_config_update(&elem, &panel.session, &panel.renderer, &presentation);
220
+ dispatch_config_update(
221
+ &elem,
222
+ &panel.session,
223
+ &panel.renderer,
224
+ &presentation,
225
+ &workspace,
226
+ );
219
227
  }
220
228
  }
221
229
  });
@@ -234,7 +242,13 @@ pub fn wire_element_events(
234
242
 
235
243
  dispatch_event(&elem, "toggle-settings", open).unwrap();
236
244
  if let Some(panel) = workspace.active_panel() {
237
- dispatch_config_update(&elem, &panel.session, &panel.renderer, &presentation);
245
+ dispatch_config_update(
246
+ &elem,
247
+ &panel.session,
248
+ &panel.renderer,
249
+ &presentation,
250
+ &workspace,
251
+ );
238
252
  }
239
253
  }
240
254
  });
@@ -328,12 +342,13 @@ pub fn wire_panel_events(
328
342
  session: &Session,
329
343
  renderer: &Renderer,
330
344
  presentation: &Presentation,
345
+ workspace: &Workspace,
331
346
  ) -> Vec<Subscription> {
332
347
  let plugin_sub = renderer.plugin_changed.add_listener({
333
- clone!(elem, session, renderer, presentation);
348
+ clone!(elem, session, renderer, presentation, workspace);
334
349
  move |plugin| {
335
350
  dispatch_plugin_changed(&elem, &renderer, &plugin);
336
- dispatch_config_update(&elem, &session, &renderer, &presentation);
351
+ dispatch_config_update(&elem, &session, &renderer, &presentation, &workspace);
337
352
  }
338
353
  });
339
354
 
@@ -343,13 +358,13 @@ pub fn wire_panel_events(
343
358
  // paused-`load()` case). The dispatcher dedups, so REBUILD's pairing
344
359
  // with `view_created` can't double-fire it.
345
360
  let view_sub = session.commit_reconciled.add_listener({
346
- clone!(elem, session, renderer, presentation);
347
- move |_| dispatch_config_update(&elem, &session, &renderer, &presentation)
361
+ clone!(elem, session, renderer, presentation, workspace);
362
+ move |_| dispatch_config_update(&elem, &session, &renderer, &presentation, &workspace)
348
363
  });
349
364
 
350
365
  let title_sub = session.title_changed.add_listener({
351
- clone!(elem, session, renderer, presentation);
352
- move |_| dispatch_config_update(&elem, &session, &renderer, &presentation)
366
+ clone!(elem, session, renderer, presentation, workspace);
367
+ move |_| dispatch_config_update(&elem, &session, &renderer, &presentation, &workspace)
353
368
  });
354
369
 
355
370
  let unload_sub = session.table_unloaded.add_listener({
@@ -365,22 +380,22 @@ pub fn wire_panel_events(
365
380
  });
366
381
 
367
382
  let select_sub = renderer.selection_changed.add_listener({
368
- clone!(elem, session, renderer, presentation);
383
+ clone!(elem, session, renderer, presentation, workspace);
369
384
  move |window: Option<ViewWindow>| {
370
385
  let detail = JsValue::from_serde_ext(&window).unwrap();
371
386
  annotate_panel(&detail, &renderer);
372
387
  let _ = dispatch_panel_event(&elem, &renderer, "select", &detail);
373
- dispatch_config_update(&elem, &session, &renderer, &presentation);
388
+ dispatch_config_update(&elem, &session, &renderer, &presentation, &workspace);
374
389
  }
375
390
  });
376
391
 
377
392
  let column_style_sub = renderer.column_style_changed.add_listener({
378
- clone!(elem, session, renderer, presentation);
393
+ clone!(elem, session, renderer, presentation, workspace);
379
394
  move |cfg: ColumnConfigMap| {
380
395
  let detail = JsValue::from_serde_ext(&cfg).unwrap();
381
396
  annotate_panel(&detail, &renderer);
382
397
  let _ = dispatch_panel_event(&elem, &renderer, "column-style-change", &detail);
383
- dispatch_config_update(&elem, &session, &renderer, &presentation);
398
+ dispatch_config_update(&elem, &session, &renderer, &presentation, &workspace);
384
399
  }
385
400
  });
386
401
 
@@ -0,0 +1,266 @@
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 std::collections::{BTreeMap, BTreeSet};
14
+
15
+ use perspective_client::config::ViewConfig;
16
+ use perspective_client::{DeleteOptions, Table, View};
17
+ use perspective_js::utils::*;
18
+
19
+ use crate::config::DerivedTableConfig;
20
+ use crate::session::ViewSubscription;
21
+ use crate::utils::PtrEqRc;
22
+
23
+ /// The `View` a derived `Table` reads, which is a panel's own subscription when
24
+ /// the derivation consumed it and a bare `View` when a restore rebuilt it.
25
+ pub enum DerivedSource {
26
+ Panel(ViewSubscription),
27
+ Restored(View),
28
+ }
29
+
30
+ impl DerivedSource {
31
+ async fn delete(self) -> ApiResult<()> {
32
+ match self {
33
+ Self::Panel(sub) => sub.delete().await,
34
+ Self::Restored(view) => Ok(view.delete().await?),
35
+ }
36
+ }
37
+ }
38
+
39
+ /// A live derived `Table` and the `View` it reads.
40
+ pub struct DerivedTable {
41
+ parent: String,
42
+ view_config: ViewConfig,
43
+ source: DerivedSource,
44
+ table: Table,
45
+ }
46
+
47
+ impl DerivedTable {
48
+ pub fn new(
49
+ parent: String,
50
+ view_config: ViewConfig,
51
+ source: DerivedSource,
52
+ table: Table,
53
+ ) -> Self {
54
+ Self {
55
+ parent,
56
+ view_config,
57
+ source,
58
+ table,
59
+ }
60
+ }
61
+
62
+ pub fn parent(&self) -> &str {
63
+ &self.parent
64
+ }
65
+
66
+ pub fn view_config(&self) -> &ViewConfig {
67
+ &self.view_config
68
+ }
69
+
70
+ pub fn table(&self) -> &Table {
71
+ &self.table
72
+ }
73
+
74
+ /// The persisted definition of this derivation.
75
+ pub fn config(&self) -> DerivedTableConfig {
76
+ DerivedTableConfig {
77
+ table: self.parent.clone(),
78
+ view_config: self.view_config.clone(),
79
+ }
80
+ }
81
+
82
+ /// Whether this derivation was built from `config`, and so may be reused.
83
+ pub fn matches(&self, config: &DerivedTableConfig) -> bool {
84
+ self.parent == config.table && self.view_config == config.view_config
85
+ }
86
+
87
+ /// Delete the `Table` before the `View` it reads, which the engine
88
+ /// requires in that order.
89
+ pub async fn delete(self) -> ApiResult<()> {
90
+ self.table.delete(DeleteOptions::default()).await?;
91
+ self.source.delete().await
92
+ }
93
+ }
94
+
95
+ /// The workspace's derived `Table`s: those live, and those whose parent is not
96
+ /// yet hosted.
97
+ #[derive(Default)]
98
+ pub struct DerivedRegistry {
99
+ live: BTreeMap<String, DerivedTable>,
100
+ pending: BTreeMap<String, DerivedTableConfig>,
101
+ }
102
+
103
+ impl DerivedRegistry {
104
+ pub fn get(&self, name: &str) -> Option<&DerivedTable> {
105
+ self.live.get(name)
106
+ }
107
+
108
+ pub fn contains(&self, name: &str) -> bool {
109
+ self.live.contains_key(name)
110
+ }
111
+
112
+ pub fn insert(&mut self, name: String, table: DerivedTable) {
113
+ self.pending.remove(&name);
114
+ self.live.insert(name, table);
115
+ }
116
+
117
+ pub fn remove(&mut self, name: &str) -> Option<DerivedTable> {
118
+ self.pending.remove(name);
119
+ self.live.remove(name)
120
+ }
121
+
122
+ pub fn names(&self) -> Vec<String> {
123
+ self.live.keys().cloned().collect()
124
+ }
125
+
126
+ /// The name of a live derivation built from `config`, regardless of what
127
+ /// it is named, for inline-definition restores to rebind to.
128
+ pub fn find_structural(&self, config: &DerivedTableConfig) -> Option<String> {
129
+ self.live
130
+ .iter()
131
+ .find(|(_, table)| table.matches(config))
132
+ .map(|(name, _)| name.clone())
133
+ }
134
+
135
+ /// The definitions waiting on a parent that is not yet hosted.
136
+ pub fn pending(&self) -> BTreeMap<String, DerivedTableConfig> {
137
+ self.pending.clone()
138
+ }
139
+
140
+ pub fn set_pending(&mut self, pending: BTreeMap<String, DerivedTableConfig>) {
141
+ self.pending = pending;
142
+ }
143
+
144
+ /// Every derivation's persisted definition, live or pending, for
145
+ /// `saveWorkspace`.
146
+ pub fn configs(&self) -> BTreeMap<String, DerivedTableConfig> {
147
+ let mut out = self.pending.clone();
148
+ out.extend(
149
+ self.live
150
+ .iter()
151
+ .map(|(name, table)| (name.clone(), table.config())),
152
+ );
153
+
154
+ out
155
+ }
156
+
157
+ /// The names whose `parent` is `name`.
158
+ pub fn children(&self, name: &str) -> Vec<String> {
159
+ self.live
160
+ .iter()
161
+ .filter(|(_, table)| table.parent == name)
162
+ .map(|(child, _)| child.clone())
163
+ .chain(
164
+ self.pending
165
+ .iter()
166
+ .filter(|(_, config)| config.table == name)
167
+ .map(|(child, _)| child.clone()),
168
+ )
169
+ .collect()
170
+ }
171
+
172
+ /// `name`'s ancestors, nearest first, stopping at the first non-derived
173
+ /// `Table`.
174
+ pub fn ancestors(&self, name: &str) -> Vec<String> {
175
+ let mut out = vec![];
176
+ let mut seen = BTreeSet::new();
177
+ let mut cursor = name.to_owned();
178
+ while let Some(entry) = self.live.get(&cursor) {
179
+ if !seen.insert(cursor.clone()) {
180
+ break;
181
+ }
182
+
183
+ out.push(entry.parent.clone());
184
+ cursor = entry.parent.clone();
185
+ }
186
+
187
+ out
188
+ }
189
+ }
190
+
191
+ /// Order `configs` parents-first, or report the names forming a cycle.
192
+ pub fn topo_order(configs: &BTreeMap<String, DerivedTableConfig>) -> Result<Vec<String>, String> {
193
+ let mut pending = configs.keys().cloned().collect::<BTreeSet<_>>();
194
+ let mut out = Vec::with_capacity(configs.len());
195
+ while !pending.is_empty() {
196
+ let ready = pending
197
+ .iter()
198
+ .filter(|name| {
199
+ let parent = &configs[*name].table;
200
+ !pending.contains(parent)
201
+ })
202
+ .cloned()
203
+ .collect::<Vec<_>>();
204
+
205
+ if ready.is_empty() {
206
+ let cycle = pending.into_iter().collect::<Vec<_>>().join("`, `");
207
+ return Err(format!(
208
+ "`derived_tables` has a dependency cycle among `{cycle}`"
209
+ ));
210
+ }
211
+
212
+ for name in ready {
213
+ pending.remove(&name);
214
+ out.push(name);
215
+ }
216
+ }
217
+
218
+ Ok(out)
219
+ }
220
+
221
+ /// The subset of `order` that may be reused from `registry`, which requires
222
+ /// every ancestor within `configs` to be reusable too.
223
+ pub fn reusable(
224
+ order: &[String],
225
+ configs: &BTreeMap<String, DerivedTableConfig>,
226
+ registry: &DerivedRegistry,
227
+ ) -> BTreeSet<String> {
228
+ let mut out = BTreeSet::new();
229
+ for name in order {
230
+ let config = &configs[name];
231
+ let parent_ok = !configs.contains_key(&config.table) || out.contains(&config.table);
232
+ if parent_ok
233
+ && registry
234
+ .get(name)
235
+ .is_some_and(|table| table.matches(config))
236
+ {
237
+ out.insert(name.clone());
238
+ }
239
+ }
240
+
241
+ out
242
+ }
243
+
244
+ /// A `<table> GROUP BY <cols>`-style name that collides with nothing in
245
+ /// `taken`.
246
+ pub fn suggest_name(
247
+ taken: &BTreeSet<String>,
248
+ config: &PtrEqRc<ViewConfig>,
249
+ table: &Option<String>,
250
+ ) -> String {
251
+ let table_name = table.as_deref().unwrap_or("...");
252
+ let name = if config.group_by.is_empty() {
253
+ table_name.to_owned()
254
+ } else {
255
+ format!("{} GROUP BY {}", table_name, config.group_by.join(", "))
256
+ };
257
+
258
+ if taken.contains(&name) {
259
+ (1..)
260
+ .map(|n| format!("{name} {n}"))
261
+ .find(|name| !taken.contains(name))
262
+ .unwrap()
263
+ } else {
264
+ name
265
+ }
266
+ }
@@ -151,57 +151,3 @@ pub fn tokenize(input: &str) -> Vec<Token<'_>> {
151
151
 
152
152
  tokens
153
153
  }
154
-
155
- #[cfg(test)]
156
- mod tests {
157
- use wasm_bindgen_test::*;
158
-
159
- use super::*;
160
-
161
- #[wasm_bindgen_test]
162
- fn test_simple() {
163
- let s = "123 abc 'hello' \"Sales\"";
164
- assert_eq!(tokenize(s), vec![
165
- Literal("123"),
166
- Whitespace(" "),
167
- Symbol("abc"),
168
- Whitespace(" "),
169
- Column("'hello'"),
170
- Whitespace(" "),
171
- Literal("\"Sales\"")
172
- ]);
173
- }
174
-
175
- #[wasm_bindgen_test]
176
- fn test_complex_string() {
177
- let s = "'this is 'a \"test of\" strings";
178
- assert_eq!(tokenize(s), vec![
179
- Column("'this is '"),
180
- Symbol("a"),
181
- Whitespace(" "),
182
- Literal("\"test of\""),
183
- Whitespace(" "),
184
- Symbol("strings"),
185
- ]);
186
- }
187
-
188
- #[wasm_bindgen_test]
189
- fn test_comment_newline() {
190
- let s = "// Title\n1 + 2";
191
- assert_eq!(tokenize(s), vec![
192
- Comment("// Title"),
193
- Break("\n"),
194
- Literal("1"),
195
- Whitespace(" "),
196
- Operator("+"),
197
- Whitespace(" "),
198
- Literal("2"),
199
- ]);
200
- }
201
-
202
- #[wasm_bindgen_test]
203
- fn test_escape_strings() {
204
- let s = "'test\\/'";
205
- assert_eq!(tokenize(s), vec![Literal("'test\\/'"),]);
206
- }
207
- }
@@ -51,7 +51,7 @@ extern "C" {
51
51
  /// call — the view re-renders and re-queries the schema once the
52
52
  /// async fetch resolves.
53
53
  #[wasm_bindgen(method, catch, js_name = column_config_schema)]
54
- pub fn _column_config_schema(this: &JsPerspectiveViewerPlugin, view_type: &str, group: Option<&str>, column_name: &str, current_value: &JsValue, view_config: &JsValue, column_stats: &JsValue) -> ApiResult<JsValue>;
54
+ pub fn _column_config_schema(this: &JsPerspectiveViewerPlugin, view_type: &str, group: Option<&str>, column_name: &str, current_value: &JsValue, view_config: &JsValue, plugin_config: &JsValue, role: &str) -> ApiResult<JsValue>;
55
55
 
56
56
  /// The plugin-level settings schema as it applies to `current_value`, the
57
57
  /// `plugin_config` state the schema should describe.
package/src/rust/lib.rs CHANGED
@@ -34,6 +34,7 @@ pub mod components;
34
34
  pub mod config;
35
35
  pub mod custom_elements;
36
36
  mod custom_events;
37
+ mod derived;
37
38
  pub mod exprtk;
38
39
  mod js;
39
40
  mod presentation;
@@ -89,6 +90,8 @@ export type * from "../../src/ts/ts-rs/ViewerConfigUpdate.d.ts";
89
90
  export type * from "../../src/ts/ts-rs/ViewerConfigInitial.d.ts";
90
91
  export type * from "../../src/ts/ts-rs/PluginStaticConfig.d.ts";
91
92
  export type * from "../../src/ts/ts-rs/WorkspaceConfig.d.ts";
93
+ export type * from "../../src/ts/ts-rs/TableRef.d.ts";
94
+ export type * from "../../src/ts/ts-rs/DerivedTableConfig.d.ts";
92
95
  export type * from "../../src/ts/ts-rs/WorkspaceConfigUpdate.d.ts";
93
96
  export type * from "../../src/ts/ts-rs/ExportMethod.d.ts";
94
97
  export type * from "../../src/ts/ts-rs/PanelOptions.d.ts";
@@ -233,6 +233,8 @@ impl Presentation {
233
233
  self.viewer_elem
234
234
  .toggle_attribute_with_force("settings", opt)
235
235
  .unwrap();
236
+
237
+ crate::utils::stamp_plugin_host_state_all(&self.viewer_elem);
236
238
  }
237
239
 
238
240
  pub fn is_settings_open(&self) -> bool {
@@ -652,10 +654,12 @@ impl Presentation {
652
654
  _ => None,
653
655
  };
654
656
 
655
- self.end_drag();
657
+ self.reset_drag_state();
656
658
  if let Some(action) = action {
657
659
  self.drop_received.emit(action);
658
660
  }
661
+
662
+ self.emit_drag_end();
659
663
  }
660
664
 
661
665
  /// Start the drag/drop action with the name of the column being dragged.
@@ -681,11 +685,26 @@ impl Presentation {
681
685
  }
682
686
 
683
687
  fn end_drag(&self) {
688
+ self.reset_drag_state();
689
+ self.emit_drag_end();
690
+ }
691
+
692
+ fn reset_drag_state(&self) {
684
693
  self.drag_target.borrow_mut().take();
685
694
  *self.drag_state.borrow_mut() = DragState::NoDrag;
686
- if let Some(cb) = self.on_dragend.borrow().as_ref() {
687
- cb.emit(());
688
- }
695
+ }
696
+
697
+ /// Notify the root one microtask late, so a drop's config edit (itself
698
+ /// notified through a microtask) lands in the same render batch.
699
+ fn emit_drag_end(&self) {
700
+ let emit = self.on_dragend.borrow().clone();
701
+ ApiFuture::spawn(async move {
702
+ if let Some(cb) = emit {
703
+ cb.emit(());
704
+ }
705
+
706
+ Ok(())
707
+ });
689
708
  }
690
709
 
691
710
  /// Register a `dragend` listener on the host `<perspective-viewer>`
@@ -10,7 +10,7 @@
10
10
  // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11
11
  // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
12
 
13
- use perspective_client::config::ViewConfig;
13
+ use perspective_client::config::{ColumnType, ViewConfig};
14
14
 
15
15
  use crate::presentation::{ColumnLocator, ColumnSettingsTarget, OpenColumnSettings};
16
16
  use crate::renderer::Renderer;
@@ -37,11 +37,62 @@ pub fn classify_column(
37
37
  }
38
38
  }
39
39
 
40
+ /// Whether `name` is one of `view_config`'s pivots, whose own raw values a
41
+ /// plugin draws as a header label or axis tick and therefore formats with its
42
+ /// TABLE type rather than an aggregate's view type.
43
+ pub fn is_pivot_column(name: &str, view_config: &ViewConfig) -> bool {
44
+ view_config.group_by.iter().any(|col| col == name)
45
+ || view_config.split_by.iter().any(|col| col == name)
46
+ }
47
+
48
+ /// Whether `name` is configured AS a pivot: a pivot that is also in `columns`
49
+ /// is configured as that aggregate instead, so its pivot copy is not
50
+ /// separately configurable.
51
+ pub fn is_pivot_only_column(name: &str, view_config: &ViewConfig) -> bool {
52
+ is_pivot_column(name, view_config)
53
+ && !view_config
54
+ .columns
55
+ .iter()
56
+ .any(|col| col.as_deref() == Some(name))
57
+ }
58
+
59
+ /// Whether the active plugin offers `name` any column config, gating every
60
+ /// affordance that opens the Style tab.
61
+ // TODO This hardcodes the set of types a plugin offers a pivot a format control
62
+ // for, instead of asking it. `get_column_config_schema` answers exactly
63
+ // (`!schema.leaf_fields().is_empty()`) but costs a plugin call per pivot pill
64
+ // per render; switch to it if pills ever outgrow a handful, or if a plugin's
65
+ // pivot controls stop being format-only.
66
+ pub fn has_column_config(
67
+ name: &str,
68
+ view_config: &ViewConfig,
69
+ metadata: &SessionMetadata,
70
+ renderer: &Renderer,
71
+ ) -> bool {
72
+ if !renderer.can_render_column_styles() {
73
+ return false;
74
+ }
75
+
76
+ if view_config
77
+ .columns
78
+ .iter()
79
+ .any(|col| col.as_deref() == Some(name))
80
+ {
81
+ return true;
82
+ }
83
+
84
+ is_pivot_column(name, view_config)
85
+ && matches!(
86
+ metadata.get_column_table_type(name),
87
+ Some(ColumnType::Integer | ColumnType::Float | ColumnType::Date | ColumnType::Datetime)
88
+ )
89
+ }
90
+
40
91
  /// Gets a [`ColumnLocator`] for the current UI's column settings state,
41
92
  /// or [`None`] if it is not currently active.
42
93
  ///
43
- /// Table columns only have a useful sidebar (the Style tab)
44
- /// when they're in `view_config.columns`.
94
+ /// Table columns only have a useful sidebar (the Style tab) when they're in
95
+ /// `view_config.columns` or are a pivot.
45
96
  pub fn get_current_column_locator(
46
97
  open_column_settings: &OpenColumnSettings,
47
98
  renderer: &Renderer,
@@ -54,54 +105,10 @@ pub fn get_current_column_locator(
54
105
  let locator = classify_column(name, view_config, metadata)?;
55
106
  match locator {
56
107
  ColumnLocator::Table(_) => {
57
- let in_columns = view_config.columns.iter().any(|maybe_col| {
58
- maybe_col
59
- .as_ref()
60
- .map(|col| col == name)
61
- .unwrap_or_default()
62
- });
63
-
64
- (in_columns && renderer.can_render_column_styles()).then_some(locator)
108
+ has_column_config(name, view_config, metadata, renderer).then_some(locator)
65
109
  },
66
110
  locator => Some(locator),
67
111
  }
68
112
  },
69
113
  }
70
114
  }
71
-
72
- #[cfg(test)]
73
- mod tests {
74
- use std::collections::HashMap;
75
-
76
- use perspective_client::config::Expressions;
77
-
78
- use super::*;
79
-
80
- fn config_with_expression(name: &str, expr: &str) -> ViewConfig {
81
- ViewConfig {
82
- expressions: Expressions(HashMap::from([(name.to_owned(), expr.to_owned())])),
83
- ..ViewConfig::default()
84
- }
85
- }
86
-
87
- #[test]
88
- fn classifies_from_the_config_before_metadata_catches_up() {
89
- let config = config_with_expression("x", "1 + 1");
90
- let metadata = SessionMetadata::default();
91
- assert_eq!(
92
- classify_column("x", &config, &metadata),
93
- Some(ColumnLocator::Expression("x".to_owned()))
94
- );
95
- assert_eq!(classify_column("nope", &config, &metadata), None);
96
- }
97
-
98
- #[test]
99
- fn unnamed_expressions_are_keyed_by_their_text() {
100
- let config = config_with_expression("\"Sales\" + 1", "\"Sales\" + 1");
101
- let metadata = SessionMetadata::default();
102
- assert_eq!(
103
- classify_column("\"Sales\" + 1", &config, &metadata),
104
- Some(ColumnLocator::Expression("\"Sales\" + 1".to_owned()))
105
- );
106
- }
107
- }
@@ -26,7 +26,7 @@ pub async fn get_viewer_config(
26
26
  session: &Session,
27
27
  renderer: &Renderer,
28
28
  presentation: &Presentation,
29
- ) -> ApiResult<ViewerConfig> {
29
+ ) -> ApiResult<ViewerConfig<String, String>> {
30
30
  let version = config::API_VERSION.to_string();
31
31
  let mut view_config = if let Some(rendered) = session.get_rendered_view_config() {
32
32
  (*rendered).clone()