@perspective-dev/viewer 5.4.0 → 5.5.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 (129) 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/GetViewMode.d.ts +4 -0
  38. package/dist/esm/ts-rs/GetViewOptions.d.ts +8 -0
  39. package/dist/esm/ts-rs/RestoreOptions.d.ts +6 -0
  40. package/dist/esm/ts-rs/RestoreWorkspaceOptions.d.ts +9 -0
  41. package/dist/esm/ts-rs/WorkspaceConfigUpdate.d.ts +30 -22
  42. package/dist/wasm/perspective-viewer.d.ts +91 -47
  43. package/dist/wasm/perspective-viewer.js +216 -136
  44. package/dist/wasm/perspective-viewer.wasm +0 -0
  45. package/dist/wasm/perspective-viewer.wasm.d.ts +18 -18
  46. package/package.json +5 -5
  47. package/src/css/column-selector.css +11 -3
  48. package/src/css/column-style.css +162 -0
  49. package/src/css/config-selector.css +1 -1
  50. package/src/css/form/debug.css +9 -0
  51. package/src/css/status-bar.css +11 -0
  52. package/src/css/ui/containers/tabs.css +2 -1
  53. package/src/rust/agent/tools.rs +80 -7
  54. package/src/rust/components/column_selector/active_column.rs +9 -0
  55. package/src/rust/components/column_selector.rs +11 -0
  56. package/src/rust/components/column_settings_sidebar/style_tab/alignment_field.rs +90 -0
  57. package/src/rust/components/column_settings_sidebar/style_tab/font_field.rs +267 -0
  58. package/src/rust/components/column_settings_sidebar/style_tab/primitive_field.rs +1 -1
  59. package/src/rust/components/column_settings_sidebar/style_tab.rs +68 -9
  60. package/src/rust/components/debug_panel.rs +107 -50
  61. package/src/rust/components/main_panel/msg.rs +4 -0
  62. package/src/rust/components/main_panel.rs +14 -0
  63. package/src/rust/components/mod.rs +0 -1
  64. package/src/rust/components/plugin_tab.rs +57 -3
  65. package/src/rust/components/settings_panel.rs +6 -0
  66. package/src/rust/components/status_indicator.rs +4 -0
  67. package/src/rust/components/viewer/panels.rs +3 -0
  68. package/src/rust/components/viewer/render.rs +1 -0
  69. package/src/rust/components/viewer/wiring.rs +10 -1
  70. package/src/rust/config/column_config_schema.rs +312 -8
  71. package/src/rust/config/datetime_column_style.rs +0 -11
  72. package/src/rust/config/mod.rs +0 -2
  73. package/src/rust/config/options.rs +50 -0
  74. package/src/rust/config/workspace_config.rs +69 -24
  75. package/src/rust/custom_elements/viewer.rs +194 -97
  76. package/src/rust/js/plugin.rs +3 -1
  77. package/src/rust/lib.rs +7 -3
  78. package/src/rust/queries/plugin_column_styles.rs +6 -5
  79. package/src/rust/renderer/plugin_config.rs +200 -41
  80. package/src/rust/renderer/plugin_store.rs +2 -1
  81. package/src/rust/renderer/props.rs +5 -0
  82. package/src/rust/renderer.rs +9 -0
  83. package/src/rust/session/props.rs +4 -0
  84. package/src/rust/session.rs +151 -80
  85. package/src/rust/tasks/create_panel.rs +4 -1
  86. package/src/rust/tasks/pipeline.rs +18 -7
  87. package/src/rust/tasks/restore_panel.rs +129 -30
  88. package/src/rust/tasks/send_column_config.rs +3 -0
  89. package/src/rust/tasks/sync_update_panels.rs +147 -67
  90. package/src/rust/tasks/table_lifecycle.rs +7 -1
  91. package/src/rust/ui/containers/tab_list.rs +1 -1
  92. package/src/rust/ui/form/intl_label.rs +4 -23
  93. package/src/rust/ui/portal.rs +1 -0
  94. package/src/rust/utils/css_vars.rs +13 -0
  95. package/src/rust/utils/local_fonts.rs +395 -0
  96. package/src/rust/utils/mod.rs +2 -0
  97. package/src/rust/workspace.rs +12 -0
  98. package/src/svg/status_pending.svg +3 -0
  99. package/src/themes/icons.css +1 -0
  100. package/src/themes/intl/ar.css +20 -7
  101. package/src/themes/intl/de.css +20 -7
  102. package/src/themes/intl/es.css +20 -7
  103. package/src/themes/intl/fr.css +20 -7
  104. package/src/themes/intl/ja.css +20 -7
  105. package/src/themes/intl/pt.css +20 -7
  106. package/src/themes/intl/zh.css +20 -7
  107. package/src/themes/intl.css +20 -7
  108. package/src/ts/perspective-viewer.ts +2 -0
  109. package/src/ts/plugin.ts +9 -0
  110. package/src/ts/ts-rs/AddPanelOptions.ts +10 -0
  111. package/src/ts/ts-rs/{DatetimeColorMode.ts → GetViewMode.ts} +4 -1
  112. package/src/ts/ts-rs/GetViewOptions.ts +7 -0
  113. package/src/ts/ts-rs/RestoreOptions.ts +7 -1
  114. package/src/ts/ts-rs/RestoreWorkspaceOptions.ts +10 -0
  115. package/src/ts/ts-rs/WorkspaceConfigUpdate.ts +31 -20
  116. package/dist/esm/ts-rs/DatetimeColorMode.d.ts +0 -1
  117. package/dist/esm/ts-rs/FormatMode.d.ts +0 -1
  118. package/dist/esm/ts-rs/StringColorMode.d.ts +0 -1
  119. package/src/rust/components/string_column_style.rs +0 -120
  120. package/src/rust/config/datetime_column_style/color_mode.rs +0 -62
  121. package/src/rust/config/string_column_style.rs +0 -135
  122. package/src/ts/ts-rs/FormatMode.ts +0 -3
  123. package/src/ts/ts-rs/StringColorMode.ts +0 -3
  124. /package/dist/wasm/snippets/{perspective-viewer-adc6f3f79141112b → perspective-viewer-ea71a3990cad35d7}/inline0.js +0 -0
  125. /package/dist/wasm/snippets/{perspective-viewer-adc6f3f79141112b → perspective-viewer-ea71a3990cad35d7}/inline1.js +0 -0
  126. /package/dist/wasm/snippets/{perspective-viewer-adc6f3f79141112b → perspective-viewer-ea71a3990cad35d7}/inline2.js +0 -0
  127. /package/dist/wasm/snippets/{perspective-viewer-adc6f3f79141112b → perspective-viewer-ea71a3990cad35d7}/inline3.js +0 -0
  128. /package/dist/wasm/snippets/{perspective-viewer-adc6f3f79141112b → perspective-viewer-ea71a3990cad35d7}/inline4.js +0 -0
  129. /package/dist/wasm/snippets/{perspective-viewer-adc6f3f79141112b → perspective-viewer-ea71a3990cad35d7}/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::{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()?
@@ -716,7 +746,11 @@ impl PerspectiveViewerElement {
716
746
  /// await viewer.eject({client: "remote"});
717
747
  /// ```
718
748
  pub fn eject(&mut self, options: Option<JsClientOptions>) -> ApiFuture<()> {
719
- let ClientOptions { client } = parse_options(options);
749
+ let ClientOptions { client } = match parse_options(options) {
750
+ Ok(x) => x,
751
+ Err(x) => return ApiFuture::new_err(x),
752
+ };
753
+
720
754
  // Default target: the active panel's client, or — when the active panel
721
755
  // is unbound (`load(Client)` is now inert) — the default client.
722
756
  let Some(target) = client
@@ -752,32 +786,59 @@ impl PerspectiveViewerElement {
752
786
  eject_client_panels(&self.workspace, &self.root, target, ids)
753
787
  }
754
788
 
755
- /// Get the underlying [`View`] for this viewer.
789
+ /// Get a [`View`] for a panel of this viewer, as currently configured by
790
+ /// the user.
756
791
  ///
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.
792
+ /// # Arguments
761
793
  ///
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.
794
+ /// - `mode` - which [`View`] to return:
795
+ /// - `"live"` (default) - the panel's own [`View`], which the viewer
796
+ /// replaces on every config change and deletes when auto-paused or
797
+ /// [`PerspectiveViewerElement::delete`]d, so calls on it may fail with
798
+ /// "View already deleted", it must never be deleted by the caller, and
799
+ /// it rejects with `No View for panel "<name>"` while none is bound
800
+ /// (auto-paused or not yet rendered).
801
+ /// - `"clone"` - a new [`View`] built from the panel's effective config
802
+ /// (global filter included), independent of the render lifecycle, which
803
+ /// the caller owns and must [`View::delete`].
804
+ /// - `"auto"` - `"live"` when the panel has a bound [`View`], else
805
+ /// `"clone"`.
806
+ /// - `panel` - the target panel, or the active panel when omitted.
807
+ ///
808
+ /// Plugins which rely on the live [`View`] for their
809
+ /// [`HTMLPerspectiveViewerPluginElement::draw`] implementations should
810
+ /// treat "View already deleted" as a _cancellation_ by silently aborting.
768
811
  ///
769
812
  /// # JavaScript Examples
770
813
  ///
771
814
  /// ```javascript
772
815
  /// const view = await viewer.getView();
773
816
  /// ```
817
+ ///
818
+ /// Export every panel without borrowing the viewer's own [`View`]:
819
+ ///
820
+ /// ```javascript
821
+ /// for (const panel of viewer.getPanelNames()) {
822
+ /// const view = await viewer.getView({ panel, mode: "clone" });
823
+ /// const arrow = await view.to_arrow();
824
+ /// await view.delete();
825
+ /// }
826
+ /// ```
774
827
  #[wasm_bindgen]
775
- pub fn getView(&self, options: Option<JsPanelOptions>) -> ApiFuture<View> {
776
- let PanelOptions { panel: name } = parse_options(options);
828
+ pub fn getView(&self, options: Option<JsGetViewOptions>) -> ApiFuture<View> {
777
829
  let this = self.clone();
778
830
  ApiFuture::new(async move {
831
+ let GetViewOptions { mode, panel: name } = parse_options(options)?;
779
832
  let panel = this.resolve_panel(name)?;
780
- Ok(panel.session.get_view().ok_or("No table set")?.into())
833
+ match (mode.unwrap_or_default(), panel.session.get_view()) {
834
+ (GetViewMode::Live | GetViewMode::Auto, Some(view)) => Ok(view.into()),
835
+ (GetViewMode::Live, None) => {
836
+ Err(format!("No View for panel \"{}\"", panel.id.as_str()).into())
837
+ },
838
+ (GetViewMode::Clone, _) | (GetViewMode::Auto, None) => {
839
+ Ok(panel.session.create_detached_view().await?.into())
840
+ },
841
+ }
781
842
  })
782
843
  }
783
844
 
@@ -788,9 +849,9 @@ impl PerspectiveViewerElement {
788
849
  /// [`View::get_config`])
789
850
  #[wasm_bindgen]
790
851
  pub fn getViewConfig(&self, options: Option<JsPanelOptions>) -> ApiFuture<JsViewConfig> {
791
- let PanelOptions { panel: name } = parse_options(options);
792
852
  let this = self.clone();
793
853
  ApiFuture::new(async move {
854
+ let PanelOptions { panel: name } = parse_options(options)?;
794
855
  let panel = this.resolve_panel(name)?;
795
856
  let config = if let Some(ctx) = panel.renderer.render_context() {
796
857
  (*ctx.view_config).clone()
@@ -821,12 +882,12 @@ impl PerspectiveViewerElement {
821
882
  /// ```
822
883
  #[wasm_bindgen]
823
884
  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
885
  let this = self.clone();
829
886
  ApiFuture::new(async move {
887
+ let GetTableOptions {
888
+ wait: wait_for_table,
889
+ panel: name,
890
+ } = parse_options(options)?;
830
891
  let panel = this.resolve_panel(name)?;
831
892
  if !wait_for_table.unwrap_or_default()
832
893
  && let Some(ctx) = panel.renderer.render_context()
@@ -866,12 +927,12 @@ impl PerspectiveViewerElement {
866
927
  &self,
867
928
  options: Option<JsGetClientOptions>,
868
929
  ) -> ApiFuture<perspective_js::Client> {
869
- let GetClientOptions {
870
- wait: wait_for_client,
871
- panel: name,
872
- } = parse_options(options);
873
930
  let this = self.clone();
874
931
  ApiFuture::new(async move {
932
+ let GetClientOptions {
933
+ wait: wait_for_client,
934
+ panel: name,
935
+ } = parse_options(options)?;
875
936
  let panel = this.resolve_panel(name)?;
876
937
  if !wait_for_client.unwrap_or_default()
877
938
  && let Some(ctx) = panel.renderer.render_context()
@@ -902,7 +963,7 @@ impl PerspectiveViewerElement {
902
963
  /// ```
903
964
  #[wasm_bindgen]
904
965
  pub fn getRenderStats(&self, options: Option<JsPanelOptions>) -> ApiResult<JsValue> {
905
- let PanelOptions { panel: name } = parse_options(options);
966
+ let PanelOptions { panel: name } = parse_options(options)?;
906
967
  let panel = self.resolve_panel(name)?;
907
968
  Ok(JsValue::from_serde_ext(
908
969
  &panel.renderer.render_timer().get_stats(),
@@ -1000,12 +1061,13 @@ impl PerspectiveViewerElement {
1000
1061
  /// - `options.panel` - The panel to target, or the active panel when
1001
1062
  /// omitted.
1002
1063
  /// - `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.
1064
+ /// the returned `Promise` and rolls the view config back, leaving the
1065
+ /// viewer's visible error state and any element-level state the call
1066
+ /// already applied (theme, title, plugin, a successful `table` change)
1067
+ /// untouched.
1068
+ /// - `options.wait_for_table` - when `true`, a `table` no loaded client
1069
+ /// hosts yet leaves the panel empty and pending until the table is
1070
+ /// created, instead of the default error.
1009
1071
  ///
1010
1072
  /// # JavaScript Examples
1011
1073
  ///
@@ -1027,20 +1089,23 @@ impl PerspectiveViewerElement {
1027
1089
  update: JsViewerConfigUpdate,
1028
1090
  options: Option<JsRestoreOptions>,
1029
1091
  ) -> 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
1092
  let effect = self.workspace.effects().guard();
1042
1093
  let this = self.clone();
1043
1094
  let fut = ApiFuture::new_throttled(async move {
1095
+ let RestoreOptions {
1096
+ panel: name,
1097
+ suppress_errors,
1098
+ wait_for_table,
1099
+ } = parse_options(options)?;
1100
+
1101
+ let errors = if suppress_errors.unwrap_or_default() {
1102
+ RestoreErrors::Suppress
1103
+ } else {
1104
+ RestoreErrors::Publish
1105
+ };
1106
+
1107
+ let missing = MissingTable::from_wait(wait_for_table);
1108
+
1044
1109
  let _effect = effect;
1045
1110
  let id = name.map(PanelId::from);
1046
1111
  let mut update = ViewerConfigUpdate::decode(&update)?;
@@ -1074,19 +1139,6 @@ impl PerspectiveViewerElement {
1074
1139
  };
1075
1140
 
1076
1141
  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
1142
  let (sender, receiver) = channel::<ApiResult<JsValue>>();
1091
1143
  this.root.borrow().as_ref().into_apierror()?.send_message(
1092
1144
  PerspectiveViewerMsg::ToggleSettingsInit(Some(settings), false, Some(sender)),
@@ -1105,6 +1157,7 @@ impl PerspectiveViewerElement {
1105
1157
  RestoreMode::Existing { active },
1106
1158
  update,
1107
1159
  errors,
1160
+ missing,
1108
1161
  )
1109
1162
  .await
1110
1163
  },
@@ -1117,6 +1170,7 @@ impl PerspectiveViewerElement {
1117
1170
  RestoreMode::Existing { active: true },
1118
1171
  update,
1119
1172
  errors,
1173
+ missing,
1120
1174
  )
1121
1175
  .await
1122
1176
  },
@@ -1129,6 +1183,7 @@ impl PerspectiveViewerElement {
1129
1183
  id,
1130
1184
  *config,
1131
1185
  None,
1186
+ missing,
1132
1187
  )
1133
1188
  .await?;
1134
1189
  Ok(())
@@ -1139,25 +1194,47 @@ impl PerspectiveViewerElement {
1139
1194
  js_sys::Promise::from(fut).unchecked_into()
1140
1195
  }
1141
1196
 
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.
1197
+ /// Apply a [`WorkspaceConfigUpdate`] to the element — the multi-panel
1198
+ /// counterpart of [`Self::restore`] and like it a field-wise update, where
1199
+ /// an absent field is unchanged, `null` resets and a value replaces.
1200
+ ///
1201
+ /// # Arguments
1202
+ ///
1203
+ /// - `update` - The workspace config update: a [`Self::saveWorkspace`]
1204
+ /// token replaces every panel (`panels` is the whole set), while a token
1205
+ /// without `panels` keeps them and `layout`, `active` and `masters` name
1206
+ /// them by their existing ids.
1207
+ /// - `options.wait_for_table` - when `true`, a panel whose `table` no
1208
+ /// loaded client hosts yet is created empty and pending until the table
1209
+ /// is created, instead of the default error.
1148
1210
  ///
1149
1211
  /// # JavaScript Examples
1150
1212
  ///
1151
1213
  /// ```javascript
1152
1214
  /// await viewer.restoreWorkspace(await otherViewer.saveWorkspace());
1153
1215
  /// ```
1154
- pub fn restoreWorkspace(&self, update: JsWorkspaceConfigUpdate) -> JsVoidPromise {
1155
- let update: JsViewerConfigUpdate = update.unchecked_into();
1216
+ ///
1217
+ /// Cross-filter the current panels without re-creating them:
1218
+ ///
1219
+ /// ```javascript
1220
+ /// await viewer.restoreWorkspace({ global_filters: [["Region", "==", "West"]] });
1221
+ /// ```
1222
+ pub fn restoreWorkspace(
1223
+ &self,
1224
+ update: JsWorkspaceConfigUpdate,
1225
+ options: Option<JsRestoreWorkspaceOptions>,
1226
+ ) -> JsVoidPromise {
1156
1227
  let effect = self.workspace.effects().guard();
1157
1228
  let this = self.clone();
1158
1229
  let fut = ApiFuture::new(async move {
1159
1230
  let _effect = effect;
1231
+ let RestoreWorkspaceOptions { wait_for_table } = parse_options(options)?;
1232
+ let missing = MissingTable::from_wait(wait_for_table);
1160
1233
  let (contents, eject_tasks) = sync_update_panels(&this, update)?;
1234
+ let fresh = contents
1235
+ .iter()
1236
+ .map(|(id, ..)| id.clone())
1237
+ .collect::<Vec<_>>();
1161
1238
  let results = join_all(contents.into_iter().map(|(id, session, renderer, config)| {
1162
1239
  let presentation = this.presentation.clone();
1163
1240
  let workspace = this.workspace.clone();
@@ -1171,6 +1248,7 @@ impl PerspectiveViewerElement {
1171
1248
  RestoreMode::Fresh,
1172
1249
  config,
1173
1250
  crate::tasks::RestoreErrors::Publish,
1251
+ missing,
1174
1252
  )
1175
1253
  .await?;
1176
1254
  if workspace.is_master(&id) {
@@ -1183,6 +1261,7 @@ impl PerspectiveViewerElement {
1183
1261
  .await;
1184
1262
 
1185
1263
  results.into_iter().collect::<ApiResult<Vec<_>>>()?;
1264
+ this.rerender_retained_overlays(&fresh).await?;
1186
1265
  join_all(eject_tasks)
1187
1266
  .await
1188
1267
  .into_iter()
@@ -1246,9 +1325,9 @@ impl PerspectiveViewerElement {
1246
1325
  /// });
1247
1326
  /// ```
1248
1327
  pub fn save(&self, options: Option<JsPanelOptions>) -> JsViewerConfigPromise {
1249
- let PanelOptions { panel: name } = parse_options(options);
1250
1328
  let this = self.clone();
1251
1329
  let fut = ApiFuture::new(async move {
1330
+ let PanelOptions { panel: name } = parse_options(options)?;
1252
1331
  this.workspace.effects().settle().await;
1253
1332
  let panel = this.resolve_panel(name)?;
1254
1333
  let viewer_config = panel
@@ -1282,9 +1361,12 @@ impl PerspectiveViewerElement {
1282
1361
  &self,
1283
1362
  options: Option<JsSaveWorkspaceOptions>,
1284
1363
  ) -> JsWorkspaceConfigPromise {
1285
- let SaveWorkspaceOptions { full_palette } = parse_options(options);
1286
1364
  let this = self.clone();
1287
- let fut = ApiFuture::new(Self::workspace_config(this, full_palette.unwrap_or(false)));
1365
+ let fut = ApiFuture::new(async move {
1366
+ let SaveWorkspaceOptions { full_palette } = parse_options(options)?;
1367
+ Self::workspace_config(this, full_palette.unwrap_or(false)).await
1368
+ });
1369
+
1288
1370
  js_sys::Promise::from(fut).unchecked_into()
1289
1371
  }
1290
1372
 
@@ -1303,13 +1385,13 @@ impl PerspectiveViewerElement {
1303
1385
  /// })
1304
1386
  /// ```
1305
1387
  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
1388
  let this = self.clone();
1312
1389
  ApiFuture::new_throttled(async move {
1390
+ let ExportOptions {
1391
+ method,
1392
+ panel: name,
1393
+ } = parse_options(options)?;
1394
+ let method = method.map(|m| JsString::from(m.as_str()));
1313
1395
  let method = if let Some(method) = method
1314
1396
  .map(|x| x.unchecked_into())
1315
1397
  .map(serde_wasm_bindgen::from_value)
@@ -1350,13 +1432,14 @@ impl PerspectiveViewerElement {
1350
1432
  /// const data = await viewer.export("plugin");
1351
1433
  /// ```
1352
1434
  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
1435
  let this = self.clone();
1359
1436
  ApiFuture::new(async move {
1437
+ let ExportOptions {
1438
+ method,
1439
+ panel: name,
1440
+ } = parse_options(options)?;
1441
+
1442
+ let method = method.map(|m| JsString::from(m.as_str()));
1360
1443
  let method = if let Some(method) = method
1361
1444
  .map(|x| x.unchecked_into())
1362
1445
  .map(serde_wasm_bindgen::from_value)
@@ -1388,13 +1471,14 @@ impl PerspectiveViewerElement {
1388
1471
  /// })
1389
1472
  /// ```
1390
1473
  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
1474
  let this = self.clone();
1397
1475
  ApiFuture::new_throttled(async move {
1476
+ let ExportOptions {
1477
+ method,
1478
+ panel: name,
1479
+ } = parse_options(options)?;
1480
+
1481
+ let method = method.map(|m| JsString::from(m.as_str()));
1398
1482
  let method = if let Some(method) = method
1399
1483
  .map(|x| x.unchecked_into())
1400
1484
  .map(serde_wasm_bindgen::from_value)
@@ -1433,11 +1517,11 @@ impl PerspectiveViewerElement {
1433
1517
  /// await viewer.reset(true, {panel: "p1"}); // just "p1", + expressions
1434
1518
  /// ```
1435
1519
  pub fn reset(&self, reset_all: Option<bool>, options: Option<JsPanelOptions>) -> ApiFuture<()> {
1436
- let PanelOptions { panel: name } = parse_options(options);
1437
1520
  let effect = self.workspace.effects().guard();
1438
1521
  let this = self.clone();
1439
1522
  let all = reset_all.unwrap_or_default();
1440
1523
  ApiFuture::new_throttled(async move {
1524
+ let PanelOptions { panel: name } = parse_options(options)?;
1441
1525
  let _effect = effect;
1442
1526
  let (completion, receiver) = Completion::new();
1443
1527
  {
@@ -1615,7 +1699,7 @@ impl PerspectiveViewerElement {
1615
1699
  /// region of the named panel, or the active panel when `panel` is omitted.
1616
1700
  #[wasm_bindgen]
1617
1701
  pub fn getSelection(&self, options: Option<JsPanelOptions>) -> ApiResult<Option<JsViewWindow>> {
1618
- let PanelOptions { panel: name } = parse_options(options);
1702
+ let PanelOptions { panel: name } = parse_options(options)?;
1619
1703
  let panel = self.resolve_panel(name)?;
1620
1704
  Ok(panel.renderer.get_selection().map(|x| x.into()))
1621
1705
  }
@@ -1628,7 +1712,7 @@ impl PerspectiveViewerElement {
1628
1712
  window: Option<JsViewWindow>,
1629
1713
  options: Option<JsPanelOptions>,
1630
1714
  ) -> ApiResult<()> {
1631
- let PanelOptions { panel: name } = parse_options(options);
1715
+ let PanelOptions { panel: name } = parse_options(options)?;
1632
1716
  let window = window.map(|x| x.into_serde_ext()).transpose()?;
1633
1717
  self.resolve_panel(name)?.renderer.set_selection(window);
1634
1718
  Ok(())
@@ -1639,7 +1723,7 @@ impl PerspectiveViewerElement {
1639
1723
  /// `panel` is omitted.
1640
1724
  #[wasm_bindgen]
1641
1725
  pub fn getEditPort(&self, options: Option<JsPanelOptions>) -> ApiResult<f64> {
1642
- let PanelOptions { panel: name } = parse_options(options);
1726
+ let PanelOptions { panel: name } = parse_options(options)?;
1643
1727
  let panel = self.resolve_panel(name)?;
1644
1728
  let edit_port = if let Some(ctx) = panel.renderer.render_context() {
1645
1729
  ctx.edit_port
@@ -1855,13 +1939,25 @@ impl PerspectiveViewerElement {
1855
1939
  ///
1856
1940
  /// The element-level `settings` field does not exist on the argument
1857
1941
  /// type (it is shared across the element, not per-panel).
1942
+ ///
1943
+ /// # Arguments
1944
+ ///
1945
+ /// - `config` - The new panel's initial config, whose `table` is required.
1946
+ /// - `options.wait_for_table` - when `true`, a `table` no loaded client
1947
+ /// hosts yet creates the panel empty and pending until the table is
1948
+ /// created, instead of the default rejection.
1858
1949
  #[wasm_bindgen]
1859
- pub fn addPanel(&self, config: JsViewerConfigInitial) -> ApiFuture<JsValue> {
1950
+ pub fn addPanel(
1951
+ &self,
1952
+ config: JsViewerConfigInitial,
1953
+ options: Option<JsAddPanelOptions>,
1954
+ ) -> ApiFuture<JsValue> {
1860
1955
  clone!(self.elem, self.presentation, self.workspace);
1861
1956
  let effect = workspace.effects().guard();
1862
1957
  let notify = self.layout_changed_notify();
1863
1958
  ApiFuture::new(async move {
1864
1959
  let _effect = effect;
1960
+ let AddPanelOptions { wait_for_table } = parse_options(options)?;
1865
1961
  let config = ViewerConfigInitial::decode(&config)?;
1866
1962
  let id = create_panel(
1867
1963
  &elem,
@@ -1871,6 +1967,7 @@ impl PerspectiveViewerElement {
1871
1967
  None,
1872
1968
  config,
1873
1969
  None,
1970
+ MissingTable::from_wait(wait_for_table),
1874
1971
  )
1875
1972
  .await?;
1876
1973
  Ok(JsValue::from_str(id.as_str()))
@@ -1960,10 +2057,10 @@ impl PerspectiveViewerElement {
1960
2057
  column_name: String,
1961
2058
  options: Option<JsPanelOptions>,
1962
2059
  ) -> ApiFuture<()> {
1963
- let PanelOptions { panel: name } = parse_options(options);
1964
2060
  let effect = self.workspace.effects().guard();
1965
2061
  let this = self.clone();
1966
2062
  ApiFuture::new_throttled(async move {
2063
+ let PanelOptions { panel: name } = parse_options(options)?;
1967
2064
  let _effect = effect;
1968
2065
  let panel = this.resolve_panel(name)?;
1969
2066
  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`
package/src/rust/lib.rs CHANGED
@@ -93,25 +93,29 @@ export type * from "../../src/ts/ts-rs/WorkspaceConfigUpdate.d.ts";
93
93
  export type * from "../../src/ts/ts-rs/ExportMethod.d.ts";
94
94
  export type * from "../../src/ts/ts-rs/PanelOptions.d.ts";
95
95
  export type * from "../../src/ts/ts-rs/RestoreOptions.d.ts";
96
+ export type * from "../../src/ts/ts-rs/RestoreWorkspaceOptions.d.ts";
97
+ export type * from "../../src/ts/ts-rs/AddPanelOptions.d.ts";
96
98
  export type * from "../../src/ts/ts-rs/SaveWorkspaceOptions.d.ts";
97
99
  export type * from "../../src/ts/ts-rs/ClientOptions.d.ts";
98
100
  export type * from "../../src/ts/ts-rs/ExportOptions.d.ts";
99
101
  export type * from "../../src/ts/ts-rs/GetTableOptions.d.ts";
100
102
  export type * from "../../src/ts/ts-rs/GetClientOptions.d.ts";
103
+ export type * from "../../src/ts/ts-rs/GetViewOptions.d.ts";
104
+ export type * from "../../src/ts/ts-rs/GetViewMode.d.ts";
101
105
  export type * from "../../src/ts/ts-rs/CustomNumberFormatConfig.d.ts";
102
106
  export type * from "../../src/ts/ts-rs/NumberFormatStyle.d.ts";
103
107
  export type * from "../../src/ts/ts-rs/Notation.d.ts";
104
108
  export type * from "../../src/ts/ts-rs/DatetimeFormatType.d.ts";
105
- export type * from "../../src/ts/ts-rs/StringColorMode.d.ts";
106
- export type * from "../../src/ts/ts-rs/DatetimeColorMode.d.ts";
107
- export type * from "../../src/ts/ts-rs/FormatMode.d.ts";
108
109
 
109
110
  import type {GetTableOptions} from "../../src/ts/ts-rs/GetTableOptions.d.ts";
110
111
  import type {PanelOptions} from "../../src/ts/ts-rs/PanelOptions.d.ts";
111
112
  import type {RestoreOptions} from "../../src/ts/ts-rs/RestoreOptions.d.ts";
113
+ import type {RestoreWorkspaceOptions} from "../../src/ts/ts-rs/RestoreWorkspaceOptions.d.ts";
114
+ import type {AddPanelOptions} from "../../src/ts/ts-rs/AddPanelOptions.d.ts";
112
115
  import type {SaveWorkspaceOptions} from "../../src/ts/ts-rs/SaveWorkspaceOptions.d.ts";
113
116
  import type {ExportOptions} from "../../src/ts/ts-rs/ExportOptions.d.ts";
114
117
  import type {GetClientOptions} from "../../src/ts/ts-rs/GetClientOptions.d.ts";
118
+ import type {GetViewOptions} from "../../src/ts/ts-rs/GetViewOptions.d.ts";
115
119
  import type {ClientOptions} from "../../src/ts/ts-rs/ClientOptions.d.ts";
116
120
  import type {ViewerConfig} from "../../src/ts/ts-rs/ViewerConfig.d.ts";
117
121
  import type {ViewerConfigUpdate} from "../../src/ts/ts-rs/ViewerConfigUpdate.d.ts";
@@ -29,18 +29,19 @@ struct ColumnStats {
29
29
  abs_max: Option<f64>,
30
30
  }
31
31
 
32
- /// Queries the active plugin for its plugin-scoped
33
- /// [`ColumnConfigSchema`]. Mirrors [`get_column_config_schema`] but
34
- /// drops the column-specific args — the schema describes the shape
35
- /// of the plugin's own config bucket on the renderer.
32
+ /// Queries the active plugin for its plugin-scoped [`ColumnConfigSchema`] as
33
+ /// it applies to `current_value`.
36
34
  pub fn get_plugin_config_schema(
37
35
  renderer: &Renderer,
38
36
  view_config: &ViewConfig,
37
+ current_value: Option<&serde_json::Map<String, serde_json::Value>>,
39
38
  ) -> ApiResult<ColumnConfigSchema> {
40
39
  let plugin = renderer.ensure_plugin_selected()?;
41
40
  let view_config_js =
42
41
  wasm_bindgen::JsValue::from_serde_ext(view_config).unwrap_or(wasm_bindgen::JsValue::NULL);
43
- let raw = plugin._plugin_config_schema(&view_config_js)?;
42
+ let current_js = wasm_bindgen::JsValue::from_serde_ext(&current_value)
43
+ .unwrap_or(wasm_bindgen::JsValue::NULL);
44
+ let raw = plugin._plugin_config_schema(&view_config_js, &current_js)?;
44
45
  serde_wasm_bindgen::from_value::<ColumnConfigSchema>(raw)
45
46
  .map(|schema| schema.canonicalize())
46
47
  .map_err(|e| e.into())