@perspective-dev/viewer 5.4.0 → 5.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/dist/cdn/perspective-viewer.js +4 -4
  2. package/dist/cdn/perspective-viewer.js.map +2 -2
  3. package/dist/css/blueprint.css +1 -1
  4. package/dist/css/botanical.css +1 -1
  5. package/dist/css/dracula.css +1 -1
  6. package/dist/css/eggplant.css +1 -1
  7. package/dist/css/gruvbox-dark.css +1 -1
  8. package/dist/css/gruvbox.css +1 -1
  9. package/dist/css/icons.css +1 -1
  10. package/dist/css/intl/ar.css +1 -1
  11. package/dist/css/intl/de.css +1 -1
  12. package/dist/css/intl/es.css +1 -1
  13. package/dist/css/intl/fr.css +1 -1
  14. package/dist/css/intl/ja.css +1 -1
  15. package/dist/css/intl/pt.css +1 -1
  16. package/dist/css/intl/zh.css +1 -1
  17. package/dist/css/intl.css +1 -1
  18. package/dist/css/ledger.css +1 -1
  19. package/dist/css/monokai.css +1 -1
  20. package/dist/css/nord.css +1 -1
  21. package/dist/css/phosphor.css +1 -1
  22. package/dist/css/pro-dark.css +1 -1
  23. package/dist/css/pro.css +1 -1
  24. package/dist/css/solarized-dark.css +1 -1
  25. package/dist/css/solarized.css +1 -1
  26. package/dist/css/themes.css +1 -1
  27. package/dist/css/vaporwave.css +1 -1
  28. package/dist/css/velvet.css +1 -1
  29. package/dist/docs/perspective-docs.json +1 -1
  30. package/dist/esm/perspective-viewer.d.ts +2 -0
  31. package/dist/esm/perspective-viewer.inline.js +9 -9
  32. package/dist/esm/perspective-viewer.inline.js.map +3 -3
  33. package/dist/esm/perspective-viewer.js +4 -4
  34. package/dist/esm/perspective-viewer.js.map +2 -2
  35. package/dist/esm/plugin.d.ts +9 -0
  36. package/dist/esm/ts-rs/AddPanelOptions.d.ts +9 -0
  37. package/dist/esm/ts-rs/ExportMethod.d.ts +1 -1
  38. package/dist/esm/ts-rs/GetViewMode.d.ts +4 -0
  39. package/dist/esm/ts-rs/GetViewOptions.d.ts +8 -0
  40. package/dist/esm/ts-rs/RestoreOptions.d.ts +6 -0
  41. package/dist/esm/ts-rs/RestoreWorkspaceOptions.d.ts +9 -0
  42. package/dist/esm/ts-rs/WorkspaceConfigUpdate.d.ts +30 -22
  43. package/dist/wasm/perspective-viewer.d.ts +102 -47
  44. package/dist/wasm/perspective-viewer.js +233 -142
  45. package/dist/wasm/perspective-viewer.wasm +0 -0
  46. package/dist/wasm/perspective-viewer.wasm.d.ts +18 -18
  47. package/package.json +7 -7
  48. package/src/css/column-selector.css +11 -3
  49. package/src/css/column-style.css +162 -0
  50. package/src/css/config-selector.css +1 -1
  51. package/src/css/form/debug.css +9 -0
  52. package/src/css/status-bar.css +11 -0
  53. package/src/css/ui/containers/tabs.css +2 -1
  54. package/src/rust/agent/tools.rs +80 -7
  55. package/src/rust/components/column_selector/active_column.rs +9 -0
  56. package/src/rust/components/column_selector.rs +11 -0
  57. package/src/rust/components/column_settings_sidebar/style_tab/alignment_field.rs +90 -0
  58. package/src/rust/components/column_settings_sidebar/style_tab/font_field.rs +267 -0
  59. package/src/rust/components/column_settings_sidebar/style_tab/primitive_field.rs +1 -1
  60. package/src/rust/components/column_settings_sidebar/style_tab.rs +68 -9
  61. package/src/rust/components/debug_panel.rs +107 -50
  62. package/src/rust/components/export_dropdown.rs +6 -0
  63. package/src/rust/components/main_panel/msg.rs +4 -0
  64. package/src/rust/components/main_panel.rs +14 -0
  65. package/src/rust/components/mod.rs +0 -1
  66. package/src/rust/components/plugin_tab.rs +57 -3
  67. package/src/rust/components/settings_panel.rs +6 -0
  68. package/src/rust/components/status_bar.rs +12 -3
  69. package/src/rust/components/status_indicator.rs +4 -0
  70. package/src/rust/components/viewer/filters.rs +2 -2
  71. package/src/rust/components/viewer/msg.rs +4 -0
  72. package/src/rust/components/viewer/panels.rs +6 -3
  73. package/src/rust/components/viewer/render.rs +1 -0
  74. package/src/rust/components/viewer/wiring.rs +14 -2
  75. package/src/rust/components/viewer.rs +2 -2
  76. package/src/rust/config/column_config_schema.rs +312 -8
  77. package/src/rust/config/datetime_column_style.rs +0 -11
  78. package/src/rust/config/export_method.rs +24 -0
  79. package/src/rust/config/mod.rs +0 -2
  80. package/src/rust/config/options.rs +50 -0
  81. package/src/rust/config/workspace_config.rs +69 -24
  82. package/src/rust/custom_elements/viewer.rs +214 -113
  83. package/src/rust/js/plugin.rs +3 -1
  84. package/src/rust/lib.rs +7 -3
  85. package/src/rust/queries/plugin_column_styles.rs +6 -5
  86. package/src/rust/renderer/plugin_config.rs +200 -41
  87. package/src/rust/renderer/plugin_store.rs +2 -1
  88. package/src/rust/renderer/props.rs +5 -0
  89. package/src/rust/renderer.rs +9 -0
  90. package/src/rust/session/metadata.rs +5 -10
  91. package/src/rust/session/props.rs +4 -0
  92. package/src/rust/session.rs +273 -195
  93. package/src/rust/tasks/copy_export.rs +67 -0
  94. package/src/rust/tasks/create_panel.rs +16 -13
  95. package/src/rust/tasks/pipeline.rs +26 -8
  96. package/src/rust/tasks/restore_panel.rs +129 -30
  97. package/src/rust/tasks/send_column_config.rs +3 -0
  98. package/src/rust/tasks/sync_update_panels.rs +148 -68
  99. package/src/rust/tasks/table_lifecycle.rs +7 -1
  100. package/src/rust/ui/containers/tab_list.rs +1 -1
  101. package/src/rust/ui/form/intl_label.rs +4 -23
  102. package/src/rust/ui/portal.rs +1 -0
  103. package/src/rust/utils/css_vars.rs +13 -0
  104. package/src/rust/{config/datetime_column_style/color_mode.rs → utils/in_flight.rs} +47 -35
  105. package/src/rust/utils/local_fonts.rs +395 -0
  106. package/src/rust/utils/mod.rs +4 -0
  107. package/src/rust/utils/pending_effects.rs +6 -30
  108. package/src/rust/workspace.rs +12 -0
  109. package/src/svg/status_pending.svg +3 -0
  110. package/src/themes/icons.css +1 -0
  111. package/src/themes/intl/ar.css +20 -7
  112. package/src/themes/intl/de.css +20 -7
  113. package/src/themes/intl/es.css +20 -7
  114. package/src/themes/intl/fr.css +20 -7
  115. package/src/themes/intl/ja.css +20 -7
  116. package/src/themes/intl/pt.css +20 -7
  117. package/src/themes/intl/zh.css +20 -7
  118. package/src/themes/intl.css +20 -7
  119. package/src/themes/monokai.css +1 -1
  120. package/src/themes/vaporwave.css +1 -1
  121. package/src/ts/perspective-viewer.ts +2 -0
  122. package/src/ts/plugin.ts +9 -0
  123. package/src/ts/ts-rs/AddPanelOptions.ts +10 -0
  124. package/src/ts/ts-rs/ExportMethod.ts +1 -1
  125. package/src/ts/ts-rs/{DatetimeColorMode.ts → GetViewMode.ts} +4 -1
  126. package/src/ts/ts-rs/GetViewOptions.ts +7 -0
  127. package/src/ts/ts-rs/RestoreOptions.ts +7 -1
  128. package/src/ts/ts-rs/RestoreWorkspaceOptions.ts +10 -0
  129. package/src/ts/ts-rs/WorkspaceConfigUpdate.ts +31 -20
  130. package/dist/esm/ts-rs/DatetimeColorMode.d.ts +0 -1
  131. package/dist/esm/ts-rs/FormatMode.d.ts +0 -1
  132. package/dist/esm/ts-rs/StringColorMode.d.ts +0 -1
  133. package/src/rust/components/string_column_style.rs +0 -120
  134. package/src/rust/config/string_column_style.rs +0 -135
  135. package/src/ts/ts-rs/FormatMode.ts +0 -3
  136. package/src/ts/ts-rs/StringColorMode.ts +0 -3
  137. /package/dist/wasm/snippets/{perspective-viewer-adc6f3f79141112b → perspective-viewer-6813b568219c6763}/inline0.js +0 -0
  138. /package/dist/wasm/snippets/{perspective-viewer-adc6f3f79141112b → perspective-viewer-6813b568219c6763}/inline1.js +0 -0
  139. /package/dist/wasm/snippets/{perspective-viewer-adc6f3f79141112b → perspective-viewer-6813b568219c6763}/inline2.js +0 -0
  140. /package/dist/wasm/snippets/{perspective-viewer-adc6f3f79141112b → perspective-viewer-6813b568219c6763}/inline3.js +0 -0
  141. /package/dist/wasm/snippets/{perspective-viewer-adc6f3f79141112b → perspective-viewer-6813b568219c6763}/inline4.js +0 -0
  142. /package/dist/wasm/snippets/{perspective-viewer-adc6f3f79141112b → perspective-viewer-6813b568219c6763}/inline5.js +0 -0
@@ -38,7 +38,7 @@ use crate::js::*;
38
38
  use crate::presentation::*;
39
39
  use crate::queries::*;
40
40
  use crate::root::Root;
41
- use crate::session::{ResetOptions, TableLoadState};
41
+ use crate::session::{Disposal, MissingTable, ResetOptions, TableLoadState};
42
42
  use crate::tasks::*;
43
43
  use crate::utils::*;
44
44
  use crate::workspace::{Panel, PanelId, Workspace};
@@ -61,11 +61,18 @@ extern "C" {
61
61
  #[wasm_bindgen(typescript_type = "PanelOptions")]
62
62
  pub type JsPanelOptions;
63
63
 
64
- /// `restore()` options dict
65
- /// (`{ panel?: string, suppress_errors?: boolean }`).
64
+ /// The `restore()` options dict.
66
65
  #[wasm_bindgen(typescript_type = "RestoreOptions")]
67
66
  pub type JsRestoreOptions;
68
67
 
68
+ /// The `restoreWorkspace()` options dict.
69
+ #[wasm_bindgen(typescript_type = "RestoreWorkspaceOptions")]
70
+ pub type JsRestoreWorkspaceOptions;
71
+
72
+ /// The `addPanel()` options dict.
73
+ #[wasm_bindgen(typescript_type = "AddPanelOptions")]
74
+ pub type JsAddPanelOptions;
75
+
69
76
  /// `addPanel()` argument: a new panel's initial config — `table`
70
77
  /// REQUIRED.
71
78
  #[wasm_bindgen(typescript_type = "ViewerConfigInitial")]
@@ -84,9 +91,9 @@ extern "C" {
84
91
  #[wasm_bindgen(typescript_type = "GetClientOptions")]
85
92
  pub type JsGetClientOptions;
86
93
 
87
- /// `restoreWorkspace()` argument: a workspace config update.
88
- #[wasm_bindgen(typescript_type = "WorkspaceConfigUpdate")]
89
- pub type JsWorkspaceConfigUpdate;
94
+ /// `getView` options dict (`{ mode?: GetViewMode, panel?: string }`).
95
+ #[wasm_bindgen(typescript_type = "GetViewOptions")]
96
+ pub type JsGetViewOptions;
90
97
 
91
98
  /// `saveWorkspace()` options dict (`{ full_palette?: boolean }`).
92
99
  #[wasm_bindgen(typescript_type = "SaveWorkspaceOptions")]
@@ -121,14 +128,15 @@ struct ResizeDimensions {
121
128
  /// falling back to `Default` on absence or a malformed argument (matching the
122
129
  /// `ResizeOptions` precedent — an options bag is a best-effort convenience,
123
130
  /// not a hard-validated payload).
124
- fn parse_options<T, U>(options: Option<T>) -> U
131
+ fn parse_options<T, U>(options: Option<T>) -> ApiResult<U>
125
132
  where
126
133
  T: Into<JsValue>,
127
134
  U: Default + for<'a> serde::Deserialize<'a>,
128
135
  {
129
- options
130
- .and_then(|o| o.into_serde_ext().ok())
131
- .unwrap_or_default()
136
+ match options {
137
+ None => Ok(U::default()),
138
+ Some(x) => Ok(x.into_serde_ext()?),
139
+ }
132
140
  }
133
141
 
134
142
  /// The `<perspective-viewer>` custom element.
@@ -196,6 +204,28 @@ impl PerspectiveViewerElement {
196
204
  })
197
205
  }
198
206
 
207
+ /// Re-stamp the global-filter overlay on every panel not in `fresh` and
208
+ /// re-render those whose overlay changed.
209
+ async fn rerender_retained_overlays(&self, fresh: &[PanelId]) -> ApiResult<()> {
210
+ let retained = self
211
+ .workspace
212
+ .panels()
213
+ .into_iter()
214
+ .filter(|panel| !fresh.contains(&panel.id))
215
+ .filter(|panel| stamp_global_overlay(&self.workspace, &panel.id, &panel.session))
216
+ .collect::<Vec<_>>();
217
+
218
+ join_all(retained.iter().map(|panel| async move {
219
+ apply_and_render(&panel.session, &panel.renderer, ViewConfigUpdate::default())?.await?;
220
+ Ok(())
221
+ }))
222
+ .await
223
+ .into_iter()
224
+ .collect::<ApiResult<Vec<_>>>()?;
225
+
226
+ Ok(())
227
+ }
228
+
199
229
  fn layout_element(&self) -> Option<RegularLayout> {
200
230
  self.elem
201
231
  .shadow_root()?
@@ -518,14 +548,19 @@ impl PerspectiveViewerElement {
518
548
 
519
549
  let session = panel.session;
520
550
  let renderer = panel.renderer;
521
- let generation = session.begin_pending_load();
551
+ let load = session.begin_pending_load();
522
552
  clone!(self.workspace, self.presentation);
523
553
  Ok(ApiFuture::new_throttled(async move {
524
554
  let _effect = effect;
555
+ enum Discard {
556
+ Unclaimed(Panel),
557
+ Evicted(Panel),
558
+ }
559
+
525
560
  renderer.set_throttle(None);
526
561
  let _run_token = session.begin_config_run();
527
562
  let result = {
528
- clone!(session, renderer, workspace, notify);
563
+ clone!(session, renderer, workspace, notify, load);
529
564
  renderer
530
565
  .clone()
531
566
  .render_task(|guard| async move {
@@ -539,7 +574,7 @@ impl PerspectiveViewerElement {
539
574
  try_from_js_option::<perspective_js::Table>(jstable.clone())
540
575
  {
541
576
  tracing::warn!("{}", DEPRECATED_TABLE_MESSAGE);
542
- let Some(journal) = session.take_pending_load(generation) else {
577
+ let Some(journal) = load.claim() else {
543
578
  return Ok(None);
544
579
  };
545
580
 
@@ -600,10 +635,10 @@ impl PerspectiveViewerElement {
600
635
  // window is discarded (not replayed): a `Client`
601
636
  // performs no reset, and any racing `restore`'s
602
637
  // commits already applied live (`commit_view_config`).
603
- let owned_window = session.take_pending_load(generation).is_some();
638
+ let owned_window = load.claim().is_some();
604
639
  let discard = if owned_window && notify.is_some() {
605
640
  match workspace.take_reserved() {
606
- Some(panel) => Some((panel, None)),
641
+ Some(panel) => Some(Discard::Unclaimed(panel)),
607
642
  // The one-shot claim read: a table-less
608
643
  // `restore` claimed the reservation, and no
609
644
  // table has bound nor is pending — evict the
@@ -625,9 +660,7 @@ impl PerspectiveViewerElement {
625
660
  notify.emit(());
626
661
  }
627
662
 
628
- evicted.map(|panel| {
629
- (panel, Some(ApiError::new(CREATE_REQUIRES_TABLE)))
630
- })
663
+ evicted.map(Discard::Evicted)
631
664
  },
632
665
  None => None,
633
666
  }
@@ -638,7 +671,7 @@ impl PerspectiveViewerElement {
638
671
  workspace.set_default_client(client.get_client().clone());
639
672
  Ok(discard)
640
673
  } else {
641
- session.take_pending_load(generation);
674
+ load.close();
642
675
  Err(ApiError::new("Invalid argument"))
643
676
  }
644
677
  })
@@ -647,7 +680,7 @@ impl PerspectiveViewerElement {
647
680
 
648
681
  match result {
649
682
  Err(e) => {
650
- session.take_pending_load(generation);
683
+ load.close();
651
684
  if let Some(notify) = &notify {
652
685
  place_reserved(&workspace, notify, true);
653
686
  }
@@ -655,12 +688,13 @@ impl PerspectiveViewerElement {
655
688
  session.set_error(false, e.clone()).await?;
656
689
  Err(e)
657
690
  },
658
- Ok(Some((panel, error))) => {
659
- eject_panel(panel).await?;
660
- match error {
661
- Some(e) => Err(e),
662
- None => Ok(()),
663
- }
691
+ Ok(Some(Discard::Unclaimed(panel))) => {
692
+ eject_panel(panel, Disposal::Reject).await?;
693
+ Ok(())
694
+ },
695
+ Ok(Some(Discard::Evicted(panel))) => {
696
+ eject_panel(panel, Disposal::Resolve).await?;
697
+ Err(ApiError::new(CREATE_REQUIRES_TABLE))
664
698
  },
665
699
  Ok(None) => Ok(()),
666
700
  }
@@ -716,7 +750,11 @@ impl PerspectiveViewerElement {
716
750
  /// await viewer.eject({client: "remote"});
717
751
  /// ```
718
752
  pub fn eject(&mut self, options: Option<JsClientOptions>) -> ApiFuture<()> {
719
- let ClientOptions { client } = parse_options(options);
753
+ let ClientOptions { client } = match parse_options(options) {
754
+ Ok(x) => x,
755
+ Err(x) => return ApiFuture::new_err(x),
756
+ };
757
+
720
758
  // Default target: the active panel's client, or — when the active panel
721
759
  // is unbound (`load(Client)` is now inert) — the default client.
722
760
  let Some(target) = client
@@ -752,32 +790,59 @@ impl PerspectiveViewerElement {
752
790
  eject_client_panels(&self.workspace, &self.root, target, ids)
753
791
  }
754
792
 
755
- /// Get the underlying [`View`] for this viewer.
793
+ /// Get a [`View`] for a panel of this viewer, as currently configured by
794
+ /// the user.
756
795
  ///
757
- /// Use this method to get promgrammatic access to the [`View`] as currently
758
- /// configured by the user, for e.g. serializing as an
759
- /// [Apache Arrow](https://arrow.apache.org/) before passing to another
760
- /// library.
796
+ /// # Arguments
761
797
  ///
762
- /// The [`View`] returned by this method is owned by the
763
- /// [`PerspectiveViewerElement`] and may be _invalidated_ by
764
- /// [`View::delete`] at any time. Plugins which rely on this [`View`] for
765
- /// their [`HTMLPerspectiveViewerPluginElement::draw`] implementations
766
- /// should treat this condition as a _cancellation_ by silently aborting on
767
- /// "View already deleted" errors from method calls.
798
+ /// - `mode` - which [`View`] to return:
799
+ /// - `"live"` (default) - the panel's own [`View`], which the viewer
800
+ /// replaces on every config change and deletes when auto-paused or
801
+ /// [`PerspectiveViewerElement::delete`]d, so calls on it may fail with
802
+ /// "View already deleted", it must never be deleted by the caller, and
803
+ /// it rejects with `No View for panel "<name>"` while none is bound
804
+ /// (auto-paused or not yet rendered).
805
+ /// - `"clone"` - a new [`View`] built from the panel's effective config
806
+ /// (global filter included), independent of the render lifecycle, which
807
+ /// the caller owns and must [`View::delete`].
808
+ /// - `"auto"` - `"live"` when the panel has a bound [`View`], else
809
+ /// `"clone"`.
810
+ /// - `panel` - the target panel, or the active panel when omitted.
811
+ ///
812
+ /// Plugins which rely on the live [`View`] for their
813
+ /// [`HTMLPerspectiveViewerPluginElement::draw`] implementations should
814
+ /// treat "View already deleted" as a _cancellation_ by silently aborting.
768
815
  ///
769
816
  /// # JavaScript Examples
770
817
  ///
771
818
  /// ```javascript
772
819
  /// const view = await viewer.getView();
773
820
  /// ```
821
+ ///
822
+ /// Export every panel without borrowing the viewer's own [`View`]:
823
+ ///
824
+ /// ```javascript
825
+ /// for (const panel of viewer.getPanelNames()) {
826
+ /// const view = await viewer.getView({ panel, mode: "clone" });
827
+ /// const arrow = await view.to_arrow();
828
+ /// await view.delete();
829
+ /// }
830
+ /// ```
774
831
  #[wasm_bindgen]
775
- pub fn getView(&self, options: Option<JsPanelOptions>) -> ApiFuture<View> {
776
- let PanelOptions { panel: name } = parse_options(options);
832
+ pub fn getView(&self, options: Option<JsGetViewOptions>) -> ApiFuture<View> {
777
833
  let this = self.clone();
778
834
  ApiFuture::new(async move {
835
+ let GetViewOptions { mode, panel: name } = parse_options(options)?;
779
836
  let panel = this.resolve_panel(name)?;
780
- Ok(panel.session.get_view().ok_or("No table set")?.into())
837
+ match (mode.unwrap_or_default(), panel.session.get_view()) {
838
+ (GetViewMode::Live | GetViewMode::Auto, Some(view)) => Ok(view.into()),
839
+ (GetViewMode::Live, None) => {
840
+ Err(format!("No View for panel \"{}\"", panel.id.as_str()).into())
841
+ },
842
+ (GetViewMode::Clone, _) | (GetViewMode::Auto, None) => {
843
+ Ok(panel.session.create_detached_view().await?.into())
844
+ },
845
+ }
781
846
  })
782
847
  }
783
848
 
@@ -788,9 +853,9 @@ impl PerspectiveViewerElement {
788
853
  /// [`View::get_config`])
789
854
  #[wasm_bindgen]
790
855
  pub fn getViewConfig(&self, options: Option<JsPanelOptions>) -> ApiFuture<JsViewConfig> {
791
- let PanelOptions { panel: name } = parse_options(options);
792
856
  let this = self.clone();
793
857
  ApiFuture::new(async move {
858
+ let PanelOptions { panel: name } = parse_options(options)?;
794
859
  let panel = this.resolve_panel(name)?;
795
860
  let config = if let Some(ctx) = panel.renderer.render_context() {
796
861
  (*ctx.view_config).clone()
@@ -821,12 +886,12 @@ impl PerspectiveViewerElement {
821
886
  /// ```
822
887
  #[wasm_bindgen]
823
888
  pub fn getTable(&self, options: Option<JsGetTableOptions>) -> ApiFuture<Table> {
824
- let GetTableOptions {
825
- wait: wait_for_table,
826
- panel: name,
827
- } = parse_options(options);
828
889
  let this = self.clone();
829
890
  ApiFuture::new(async move {
891
+ let GetTableOptions {
892
+ wait: wait_for_table,
893
+ panel: name,
894
+ } = parse_options(options)?;
830
895
  let panel = this.resolve_panel(name)?;
831
896
  if !wait_for_table.unwrap_or_default()
832
897
  && let Some(ctx) = panel.renderer.render_context()
@@ -866,12 +931,12 @@ impl PerspectiveViewerElement {
866
931
  &self,
867
932
  options: Option<JsGetClientOptions>,
868
933
  ) -> ApiFuture<perspective_js::Client> {
869
- let GetClientOptions {
870
- wait: wait_for_client,
871
- panel: name,
872
- } = parse_options(options);
873
934
  let this = self.clone();
874
935
  ApiFuture::new(async move {
936
+ let GetClientOptions {
937
+ wait: wait_for_client,
938
+ panel: name,
939
+ } = parse_options(options)?;
875
940
  let panel = this.resolve_panel(name)?;
876
941
  if !wait_for_client.unwrap_or_default()
877
942
  && let Some(ctx) = panel.renderer.render_context()
@@ -902,7 +967,7 @@ impl PerspectiveViewerElement {
902
967
  /// ```
903
968
  #[wasm_bindgen]
904
969
  pub fn getRenderStats(&self, options: Option<JsPanelOptions>) -> ApiResult<JsValue> {
905
- let PanelOptions { panel: name } = parse_options(options);
970
+ let PanelOptions { panel: name } = parse_options(options)?;
906
971
  let panel = self.resolve_panel(name)?;
907
972
  Ok(JsValue::from_serde_ext(
908
973
  &panel.renderer.render_timer().get_stats(),
@@ -1000,12 +1065,13 @@ impl PerspectiveViewerElement {
1000
1065
  /// - `options.panel` - The panel to target, or the active panel when
1001
1066
  /// omitted.
1002
1067
  /// - `options.suppress_errors` - when `true`, a failed restore only rejects
1003
- /// the returned `Promise`; the error is NOT committed to the viewer's
1004
- /// visible error state and the session remains usable. The view config is
1005
- /// rolled back to its pre-call value, so a rejected patch cannot re-merge
1006
- /// into a later restore. Element-level state the call already applied
1007
- /// (theme, title, a plugin swap) is NOT undone — restore a known-good
1008
- /// config to recover those exactly.
1068
+ /// the returned `Promise` and rolls the view config back, leaving the
1069
+ /// viewer's visible error state and any element-level state the call
1070
+ /// already applied (theme, title, plugin, a successful `table` change)
1071
+ /// untouched.
1072
+ /// - `options.wait_for_table` - when `true`, a `table` no loaded client
1073
+ /// hosts yet leaves the panel empty and pending until the table is
1074
+ /// created, instead of the default error.
1009
1075
  ///
1010
1076
  /// # JavaScript Examples
1011
1077
  ///
@@ -1027,20 +1093,23 @@ impl PerspectiveViewerElement {
1027
1093
  update: JsViewerConfigUpdate,
1028
1094
  options: Option<JsRestoreOptions>,
1029
1095
  ) -> JsVoidPromise {
1030
- let RestoreOptions {
1031
- panel: name,
1032
- suppress_errors,
1033
- } = parse_options(options);
1034
-
1035
- let errors = if suppress_errors.unwrap_or_default() {
1036
- RestoreErrors::Suppress
1037
- } else {
1038
- RestoreErrors::Publish
1039
- };
1040
-
1041
1096
  let effect = self.workspace.effects().guard();
1042
1097
  let this = self.clone();
1043
1098
  let fut = ApiFuture::new_throttled(async move {
1099
+ let RestoreOptions {
1100
+ panel: name,
1101
+ suppress_errors,
1102
+ wait_for_table,
1103
+ } = parse_options(options)?;
1104
+
1105
+ let errors = if suppress_errors.unwrap_or_default() {
1106
+ RestoreErrors::Suppress
1107
+ } else {
1108
+ RestoreErrors::Publish
1109
+ };
1110
+
1111
+ let missing = MissingTable::from_wait(wait_for_table);
1112
+
1044
1113
  let _effect = effect;
1045
1114
  let id = name.map(PanelId::from);
1046
1115
  let mut update = ViewerConfigUpdate::decode(&update)?;
@@ -1074,19 +1143,6 @@ impl PerspectiveViewerElement {
1074
1143
  };
1075
1144
 
1076
1145
  if !matches!(settings, OptionalUpdate::Missing) {
1077
- // Through `ToggleSettingsInit` — the SAME full choreography
1078
- // the toolbar toggle drives (presize every visible plugin
1079
- // to its post-toggle box, then the exactness-finalizer
1080
- // resize) — NOT the bare `ToggleSettingsComplete` leaf,
1081
- // which only re-renders the pane. The pane is the outer
1082
- // `SplitPanel` (no `before-resize` event) and the host box
1083
- // is unchanged, so a leaf-only toggle left every canvas
1084
- // plugin CSS-stretched at its old backing size.
1085
- //
1086
- // No `set_settings_before_open` here: `is_settings_open` is
1087
- // Init's toggle-vs-no-op DISPATCH state, so pre-writing the
1088
- // target makes every call resolve as a no-op. Init owns the
1089
- // write, as it does for the toolbar and `toggleConfig`.
1090
1146
  let (sender, receiver) = channel::<ApiResult<JsValue>>();
1091
1147
  this.root.borrow().as_ref().into_apierror()?.send_message(
1092
1148
  PerspectiveViewerMsg::ToggleSettingsInit(Some(settings), false, Some(sender)),
@@ -1105,6 +1161,7 @@ impl PerspectiveViewerElement {
1105
1161
  RestoreMode::Existing { active },
1106
1162
  update,
1107
1163
  errors,
1164
+ missing,
1108
1165
  )
1109
1166
  .await
1110
1167
  },
@@ -1117,6 +1174,7 @@ impl PerspectiveViewerElement {
1117
1174
  RestoreMode::Existing { active: true },
1118
1175
  update,
1119
1176
  errors,
1177
+ missing,
1120
1178
  )
1121
1179
  .await
1122
1180
  },
@@ -1129,6 +1187,7 @@ impl PerspectiveViewerElement {
1129
1187
  id,
1130
1188
  *config,
1131
1189
  None,
1190
+ missing,
1132
1191
  )
1133
1192
  .await?;
1134
1193
  Ok(())
@@ -1139,25 +1198,47 @@ impl PerspectiveViewerElement {
1139
1198
  js_sys::Promise::from(fut).unchecked_into()
1140
1199
  }
1141
1200
 
1142
- /// Restore the ENTIRE element from a [`WorkspaceConfigUpdate`]
1143
- /// (`{version, active?, layout, panels, ...}`) —
1144
- /// the multi-panel counterpart of [`Self::restore`]. Every existing panel
1145
- /// is replaced by the `panels` entries, and the layout tree + master/detail
1146
- /// cross-filter state re-applied. Unlike [`Self::restore`], this never
1147
- /// falls back to the single-panel path.
1201
+ /// Apply a [`WorkspaceConfigUpdate`] to the element — the multi-panel
1202
+ /// counterpart of [`Self::restore`] and like it a field-wise update, where
1203
+ /// an absent field is unchanged, `null` resets and a value replaces.
1204
+ ///
1205
+ /// # Arguments
1206
+ ///
1207
+ /// - `update` - The workspace config update: a [`Self::saveWorkspace`]
1208
+ /// token replaces every panel (`panels` is the whole set), while a token
1209
+ /// without `panels` keeps them and `layout`, `active` and `masters` name
1210
+ /// them by their existing ids.
1211
+ /// - `options.wait_for_table` - when `true`, a panel whose `table` no
1212
+ /// loaded client hosts yet is created empty and pending until the table
1213
+ /// is created, instead of the default error.
1148
1214
  ///
1149
1215
  /// # JavaScript Examples
1150
1216
  ///
1151
1217
  /// ```javascript
1152
1218
  /// await viewer.restoreWorkspace(await otherViewer.saveWorkspace());
1153
1219
  /// ```
1154
- pub fn restoreWorkspace(&self, update: JsWorkspaceConfigUpdate) -> JsVoidPromise {
1155
- let update: JsViewerConfigUpdate = update.unchecked_into();
1220
+ ///
1221
+ /// Cross-filter the current panels without re-creating them:
1222
+ ///
1223
+ /// ```javascript
1224
+ /// await viewer.restoreWorkspace({ global_filters: [["Region", "==", "West"]] });
1225
+ /// ```
1226
+ pub fn restoreWorkspace(
1227
+ &self,
1228
+ update: JsWorkspaceConfigUpdate,
1229
+ options: Option<JsRestoreWorkspaceOptions>,
1230
+ ) -> JsVoidPromise {
1156
1231
  let effect = self.workspace.effects().guard();
1157
1232
  let this = self.clone();
1158
1233
  let fut = ApiFuture::new(async move {
1159
1234
  let _effect = effect;
1235
+ let RestoreWorkspaceOptions { wait_for_table } = parse_options(options)?;
1236
+ let missing = MissingTable::from_wait(wait_for_table);
1160
1237
  let (contents, eject_tasks) = sync_update_panels(&this, update)?;
1238
+ let fresh = contents
1239
+ .iter()
1240
+ .map(|(id, ..)| id.clone())
1241
+ .collect::<Vec<_>>();
1161
1242
  let results = join_all(contents.into_iter().map(|(id, session, renderer, config)| {
1162
1243
  let presentation = this.presentation.clone();
1163
1244
  let workspace = this.workspace.clone();
@@ -1171,6 +1252,7 @@ impl PerspectiveViewerElement {
1171
1252
  RestoreMode::Fresh,
1172
1253
  config,
1173
1254
  crate::tasks::RestoreErrors::Publish,
1255
+ missing,
1174
1256
  )
1175
1257
  .await?;
1176
1258
  if workspace.is_master(&id) {
@@ -1183,6 +1265,7 @@ impl PerspectiveViewerElement {
1183
1265
  .await;
1184
1266
 
1185
1267
  results.into_iter().collect::<ApiResult<Vec<_>>>()?;
1268
+ this.rerender_retained_overlays(&fresh).await?;
1186
1269
  join_all(eject_tasks)
1187
1270
  .await
1188
1271
  .into_iter()
@@ -1246,9 +1329,9 @@ impl PerspectiveViewerElement {
1246
1329
  /// });
1247
1330
  /// ```
1248
1331
  pub fn save(&self, options: Option<JsPanelOptions>) -> JsViewerConfigPromise {
1249
- let PanelOptions { panel: name } = parse_options(options);
1250
1332
  let this = self.clone();
1251
1333
  let fut = ApiFuture::new(async move {
1334
+ let PanelOptions { panel: name } = parse_options(options)?;
1252
1335
  this.workspace.effects().settle().await;
1253
1336
  let panel = this.resolve_panel(name)?;
1254
1337
  let viewer_config = panel
@@ -1282,9 +1365,12 @@ impl PerspectiveViewerElement {
1282
1365
  &self,
1283
1366
  options: Option<JsSaveWorkspaceOptions>,
1284
1367
  ) -> JsWorkspaceConfigPromise {
1285
- let SaveWorkspaceOptions { full_palette } = parse_options(options);
1286
1368
  let this = self.clone();
1287
- let fut = ApiFuture::new(Self::workspace_config(this, full_palette.unwrap_or(false)));
1369
+ let fut = ApiFuture::new(async move {
1370
+ let SaveWorkspaceOptions { full_palette } = parse_options(options)?;
1371
+ Self::workspace_config(this, full_palette.unwrap_or(false)).await
1372
+ });
1373
+
1288
1374
  js_sys::Promise::from(fut).unchecked_into()
1289
1375
  }
1290
1376
 
@@ -1303,13 +1389,13 @@ impl PerspectiveViewerElement {
1303
1389
  /// })
1304
1390
  /// ```
1305
1391
  pub fn download(&self, options: Option<JsExportOptions>) -> ApiFuture<()> {
1306
- let ExportOptions {
1307
- method,
1308
- panel: name,
1309
- } = parse_options(options);
1310
- let method = method.map(|m| JsString::from(m.as_str()));
1311
1392
  let this = self.clone();
1312
1393
  ApiFuture::new_throttled(async move {
1394
+ let ExportOptions {
1395
+ method,
1396
+ panel: name,
1397
+ } = parse_options(options)?;
1398
+ let method = method.map(|m| JsString::from(m.as_str()));
1313
1399
  let method = if let Some(method) = method
1314
1400
  .map(|x| x.unchecked_into())
1315
1401
  .map(serde_wasm_bindgen::from_value)
@@ -1350,13 +1436,14 @@ impl PerspectiveViewerElement {
1350
1436
  /// const data = await viewer.export("plugin");
1351
1437
  /// ```
1352
1438
  pub fn export(&self, options: Option<JsExportOptions>) -> ApiFuture<JsValue> {
1353
- let ExportOptions {
1354
- method,
1355
- panel: name,
1356
- } = parse_options(options);
1357
- let method = method.map(|m| JsString::from(m.as_str()));
1358
1439
  let this = self.clone();
1359
1440
  ApiFuture::new(async move {
1441
+ let ExportOptions {
1442
+ method,
1443
+ panel: name,
1444
+ } = parse_options(options)?;
1445
+
1446
+ let method = method.map(|m| JsString::from(m.as_str()));
1360
1447
  let method = if let Some(method) = method
1361
1448
  .map(|x| x.unchecked_into())
1362
1449
  .map(serde_wasm_bindgen::from_value)
@@ -1388,13 +1475,14 @@ impl PerspectiveViewerElement {
1388
1475
  /// })
1389
1476
  /// ```
1390
1477
  pub fn copy(&self, options: Option<JsExportOptions>) -> ApiFuture<()> {
1391
- let ExportOptions {
1392
- method,
1393
- panel: name,
1394
- } = parse_options(options);
1395
- let method = method.map(|m| JsString::from(m.as_str()));
1396
1478
  let this = self.clone();
1397
1479
  ApiFuture::new_throttled(async move {
1480
+ let ExportOptions {
1481
+ method,
1482
+ panel: name,
1483
+ } = parse_options(options)?;
1484
+
1485
+ let method = method.map(|m| JsString::from(m.as_str()));
1398
1486
  let method = if let Some(method) = method
1399
1487
  .map(|x| x.unchecked_into())
1400
1488
  .map(serde_wasm_bindgen::from_value)
@@ -1433,11 +1521,11 @@ impl PerspectiveViewerElement {
1433
1521
  /// await viewer.reset(true, {panel: "p1"}); // just "p1", + expressions
1434
1522
  /// ```
1435
1523
  pub fn reset(&self, reset_all: Option<bool>, options: Option<JsPanelOptions>) -> ApiFuture<()> {
1436
- let PanelOptions { panel: name } = parse_options(options);
1437
1524
  let effect = self.workspace.effects().guard();
1438
1525
  let this = self.clone();
1439
1526
  let all = reset_all.unwrap_or_default();
1440
1527
  ApiFuture::new_throttled(async move {
1528
+ let PanelOptions { panel: name } = parse_options(options)?;
1441
1529
  let _effect = effect;
1442
1530
  let (completion, receiver) = Completion::new();
1443
1531
  {
@@ -1615,7 +1703,7 @@ impl PerspectiveViewerElement {
1615
1703
  /// region of the named panel, or the active panel when `panel` is omitted.
1616
1704
  #[wasm_bindgen]
1617
1705
  pub fn getSelection(&self, options: Option<JsPanelOptions>) -> ApiResult<Option<JsViewWindow>> {
1618
- let PanelOptions { panel: name } = parse_options(options);
1706
+ let PanelOptions { panel: name } = parse_options(options)?;
1619
1707
  let panel = self.resolve_panel(name)?;
1620
1708
  Ok(panel.renderer.get_selection().map(|x| x.into()))
1621
1709
  }
@@ -1628,7 +1716,7 @@ impl PerspectiveViewerElement {
1628
1716
  window: Option<JsViewWindow>,
1629
1717
  options: Option<JsPanelOptions>,
1630
1718
  ) -> ApiResult<()> {
1631
- let PanelOptions { panel: name } = parse_options(options);
1719
+ let PanelOptions { panel: name } = parse_options(options)?;
1632
1720
  let window = window.map(|x| x.into_serde_ext()).transpose()?;
1633
1721
  self.resolve_panel(name)?.renderer.set_selection(window);
1634
1722
  Ok(())
@@ -1639,7 +1727,7 @@ impl PerspectiveViewerElement {
1639
1727
  /// `panel` is omitted.
1640
1728
  #[wasm_bindgen]
1641
1729
  pub fn getEditPort(&self, options: Option<JsPanelOptions>) -> ApiResult<f64> {
1642
- let PanelOptions { panel: name } = parse_options(options);
1730
+ let PanelOptions { panel: name } = parse_options(options)?;
1643
1731
  let panel = self.resolve_panel(name)?;
1644
1732
  let edit_port = if let Some(ctx) = panel.renderer.render_context() {
1645
1733
  ctx.edit_port
@@ -1855,13 +1943,25 @@ impl PerspectiveViewerElement {
1855
1943
  ///
1856
1944
  /// The element-level `settings` field does not exist on the argument
1857
1945
  /// type (it is shared across the element, not per-panel).
1946
+ ///
1947
+ /// # Arguments
1948
+ ///
1949
+ /// - `config` - The new panel's initial config, whose `table` is required.
1950
+ /// - `options.wait_for_table` - when `true`, a `table` no loaded client
1951
+ /// hosts yet creates the panel empty and pending until the table is
1952
+ /// created, instead of the default rejection.
1858
1953
  #[wasm_bindgen]
1859
- pub fn addPanel(&self, config: JsViewerConfigInitial) -> ApiFuture<JsValue> {
1954
+ pub fn addPanel(
1955
+ &self,
1956
+ config: JsViewerConfigInitial,
1957
+ options: Option<JsAddPanelOptions>,
1958
+ ) -> ApiFuture<JsValue> {
1860
1959
  clone!(self.elem, self.presentation, self.workspace);
1861
1960
  let effect = workspace.effects().guard();
1862
1961
  let notify = self.layout_changed_notify();
1863
1962
  ApiFuture::new(async move {
1864
1963
  let _effect = effect;
1964
+ let AddPanelOptions { wait_for_table } = parse_options(options)?;
1865
1965
  let config = ViewerConfigInitial::decode(&config)?;
1866
1966
  let id = create_panel(
1867
1967
  &elem,
@@ -1871,6 +1971,7 @@ impl PerspectiveViewerElement {
1871
1971
  None,
1872
1972
  config,
1873
1973
  None,
1974
+ MissingTable::from_wait(wait_for_table),
1874
1975
  )
1875
1976
  .await?;
1876
1977
  Ok(JsValue::from_str(id.as_str()))
@@ -1960,10 +2061,10 @@ impl PerspectiveViewerElement {
1960
2061
  column_name: String,
1961
2062
  options: Option<JsPanelOptions>,
1962
2063
  ) -> ApiFuture<()> {
1963
- let PanelOptions { panel: name } = parse_options(options);
1964
2064
  let effect = self.workspace.effects().guard();
1965
2065
  let this = self.clone();
1966
2066
  ApiFuture::new_throttled(async move {
2067
+ let PanelOptions { panel: name } = parse_options(options)?;
1967
2068
  let _effect = effect;
1968
2069
  let panel = this.resolve_panel(name)?;
1969
2070
  let was_active = this.workspace.active_id().as_ref() == Some(&panel.id);
@@ -53,8 +53,10 @@ extern "C" {
53
53
  #[wasm_bindgen(method, catch, js_name = column_config_schema)]
54
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>;
55
55
 
56
+ /// The plugin-level settings schema as it applies to `current_value`, the
57
+ /// `plugin_config` state the schema should describe.
56
58
  #[wasm_bindgen(method, catch, js_name = plugin_config_schema)]
57
- pub fn _plugin_config_schema(this: &JsPerspectiveViewerPlugin, view_config: &JsValue) -> ApiResult<JsValue>;
59
+ pub fn _plugin_config_schema(this: &JsPerspectiveViewerPlugin, view_config: &JsValue, current_value: &JsValue) -> ApiResult<JsValue>;
58
60
 
59
61
  /// STATE TRANSFER, not rendering (dispatch semantics, rule for
60
62
  /// `restore`/`save`): deliver a `plugin_config` + `columns_config`