@perspective-dev/viewer 4.5.1 → 5.0.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 (212) hide show
  1. package/dist/cdn/perspective-viewer.js +20 -1
  2. package/dist/cdn/perspective-viewer.js.map +4 -4
  3. package/dist/css/botanical.css +1 -1
  4. package/dist/css/dracula.css +1 -1
  5. package/dist/css/gruvbox-dark.css +1 -1
  6. package/dist/css/gruvbox.css +1 -1
  7. package/dist/css/icons.css +1 -1
  8. package/dist/css/monokai.css +1 -1
  9. package/dist/css/phosphor.css +1 -1
  10. package/dist/css/pro-dark.css +1 -1
  11. package/dist/css/pro.css +1 -1
  12. package/dist/css/solarized-dark.css +1 -1
  13. package/dist/css/solarized.css +1 -1
  14. package/dist/css/themes.css +1 -1
  15. package/dist/css/vaporwave.css +1 -1
  16. package/dist/esm/extensions.d.ts +22 -3
  17. package/dist/esm/perspective-viewer.cdn.d.ts +3 -0
  18. package/dist/esm/perspective-viewer.d.ts +9 -0
  19. package/dist/esm/perspective-viewer.inline.d.ts +3 -0
  20. package/dist/esm/perspective-viewer.inline.js +20 -1
  21. package/dist/esm/perspective-viewer.inline.js.map +4 -4
  22. package/dist/esm/perspective-viewer.js +20 -1
  23. package/dist/esm/perspective-viewer.js.map +4 -4
  24. package/dist/esm/plugin.d.ts +71 -5
  25. package/dist/esm/ts-rs/ClientOptions.d.ts +7 -0
  26. package/dist/esm/ts-rs/ExportMethod.d.ts +1 -0
  27. package/dist/esm/ts-rs/ExportOptions.d.ts +9 -0
  28. package/dist/esm/ts-rs/GetClientOptions.d.ts +8 -0
  29. package/dist/esm/ts-rs/GetTableOptions.d.ts +8 -0
  30. package/dist/esm/ts-rs/Layout.d.ts +11 -0
  31. package/dist/esm/ts-rs/OnUpdateData.d.ts +6 -0
  32. package/dist/esm/ts-rs/Orientation.d.ts +4 -0
  33. package/dist/esm/ts-rs/PanelOptions.d.ts +7 -0
  34. package/dist/esm/ts-rs/PanelViewerConfig.d.ts +39 -0
  35. package/dist/esm/ts-rs/SplitLayout.d.ts +7 -0
  36. package/dist/esm/ts-rs/TabLayout.d.ts +10 -0
  37. package/dist/esm/ts-rs/ViewOnUpdateResp.d.ts +4 -0
  38. package/dist/esm/ts-rs/ViewerConfig.d.ts +6 -2
  39. package/dist/esm/ts-rs/WorkspaceConfig.d.ts +40 -0
  40. package/dist/esm/ts-rs/WorkspaceConfigUpdate.d.ts +28 -0
  41. package/dist/wasm/perspective-viewer.d.ts +213 -79
  42. package/dist/wasm/perspective-viewer.js +537 -189
  43. package/dist/wasm/perspective-viewer.wasm +0 -0
  44. package/dist/wasm/perspective-viewer.wasm.d.ts +36 -28
  45. package/dist/wasm/snippets/perspective-viewer-daeef4ceb066e6c7/inline0.js +6 -0
  46. package/dist/wasm/snippets/{perspective-viewer-3cd58f0374935772/inline3.js → perspective-viewer-daeef4ceb066e6c7/inline4.js} +5 -0
  47. package/package.json +3 -2
  48. package/src/css/_column_dropdown.css +14 -0
  49. package/src/css/_context_menu.css +14 -0
  50. package/src/css/_dropdown_menu.css +14 -0
  51. package/src/css/_filter_dropdown.css +14 -0
  52. package/src/css/_function_dropdown.css +14 -0
  53. package/src/css/_viewer_bundle.css +41 -0
  54. package/src/css/column-settings-panel.css +1 -1
  55. package/src/css/containers/context-menu.css +101 -0
  56. package/src/css/panel-tab.css +193 -0
  57. package/src/css/plugin-selector.css +7 -10
  58. package/src/css/status-bar.css +75 -81
  59. package/src/css/viewer.css +181 -71
  60. package/src/rust/components/column_dropdown.rs +43 -33
  61. package/src/rust/components/column_selector/active_column.rs +7 -18
  62. package/src/rust/components/column_selector/aggregate_selector.rs +4 -10
  63. package/src/rust/components/column_selector/config_selector.rs +33 -84
  64. package/src/rust/components/column_selector/empty_column.rs +0 -3
  65. package/src/rust/components/column_selector/filter_column.rs +5 -18
  66. package/src/rust/components/column_selector/inactive_column.rs +3 -8
  67. package/src/rust/components/column_selector/invalid_column.rs +0 -4
  68. package/src/rust/components/column_selector/sort_column.rs +3 -6
  69. package/src/rust/components/column_selector.rs +5 -14
  70. package/src/rust/components/column_settings_sidebar/style_tab/symbol/symbol_pairs.rs +0 -3
  71. package/src/rust/components/column_settings_sidebar/style_tab/symbol.rs +0 -3
  72. package/src/rust/components/column_settings_sidebar.rs +0 -3
  73. package/src/rust/components/containers/dropdown_menu.rs +1 -3
  74. package/src/rust/components/containers/scroll_panel.rs +1 -3
  75. package/src/rust/components/containers/split_panel.rs +175 -66
  76. package/src/rust/components/containers/tab_list.rs +0 -3
  77. package/src/rust/components/context_menu.rs +130 -0
  78. package/src/rust/components/datetime_column_style.rs +0 -3
  79. package/src/rust/components/empty_row.rs +0 -3
  80. package/src/rust/components/expression_editor.rs +2 -3
  81. package/src/rust/components/filter_dropdown.rs +3 -3
  82. package/src/rust/components/font_loader.rs +2 -2
  83. package/src/rust/components/form/code_editor.rs +0 -3
  84. package/src/rust/components/form/debug.rs +3 -4
  85. package/src/rust/components/function_dropdown.rs +3 -3
  86. package/src/rust/components/global_filter_bar.rs +76 -0
  87. package/src/rust/components/main_panel/frame_theme.rs +186 -0
  88. package/src/rust/components/main_panel/msg.rs +62 -0
  89. package/src/rust/components/main_panel/presize.rs +220 -0
  90. package/src/rust/components/main_panel/reconcile.rs +224 -0
  91. package/src/rust/components/main_panel/render.rs +385 -0
  92. package/src/rust/components/main_panel/update.rs +184 -0
  93. package/src/rust/components/main_panel.rs +311 -71
  94. package/src/rust/components/mod.rs +4 -0
  95. package/src/rust/components/number_series_style.rs +0 -3
  96. package/src/rust/components/panel_menu.rs +486 -0
  97. package/src/rust/components/panel_tab.rs +505 -0
  98. package/src/rust/components/plugin_selector.rs +0 -3
  99. package/src/rust/components/plugin_tab.rs +1 -6
  100. package/src/rust/components/portal.rs +5 -2
  101. package/src/rust/components/render_warning.rs +0 -3
  102. package/src/rust/components/settings_panel.rs +13 -5
  103. package/src/rust/components/status_bar.rs +113 -180
  104. package/src/rust/components/status_indicator.rs +6 -9
  105. package/src/rust/components/string_column_style.rs +0 -3
  106. package/src/rust/components/style/mod.rs +65 -27
  107. package/src/rust/components/style/style_provider.rs +51 -16
  108. package/src/rust/components/style_controls/number_string_format.rs +1 -8
  109. package/src/rust/components/type_icon.rs +1 -4
  110. package/src/rust/components/viewer/filters.rs +133 -0
  111. package/src/rust/components/viewer/msg.rs +180 -0
  112. package/src/rust/components/viewer/panels.rs +326 -0
  113. package/src/rust/components/viewer/render.rs +243 -0
  114. package/src/rust/components/viewer/settings.rs +402 -0
  115. package/src/rust/components/viewer/snapshots.rs +134 -0
  116. package/src/rust/components/viewer/wiring.rs +505 -0
  117. package/src/rust/components/viewer.rs +243 -758
  118. package/src/rust/config/export_method.rs +1 -1
  119. package/src/rust/config/mod.rs +4 -0
  120. package/src/rust/config/options.rs +71 -0
  121. package/src/rust/config/viewer_config.rs +24 -2
  122. package/src/rust/config/workspace_config.rs +92 -0
  123. package/src/rust/custom_elements/copy_dropdown.rs +9 -3
  124. package/src/rust/custom_elements/debug_plugin.rs +20 -1
  125. package/src/rust/custom_elements/export_dropdown.rs +9 -3
  126. package/src/rust/custom_elements/viewer.rs +1015 -286
  127. package/src/rust/custom_events.rs +212 -61
  128. package/src/rust/js/mod.rs +2 -0
  129. package/src/rust/js/plugin.rs +297 -10
  130. package/src/rust/js/regular_layout.rs +258 -0
  131. package/src/rust/lib.rs +20 -2
  132. package/src/rust/presentation.rs +66 -33
  133. package/src/rust/queries/get_viewer_config.rs +29 -11
  134. package/src/rust/queries/plugin_column_styles.rs +2 -2
  135. package/src/rust/renderer/activate.rs +41 -14
  136. package/src/rust/renderer/dispatch.rs +436 -0
  137. package/src/rust/renderer/plugin_config.rs +495 -0
  138. package/src/rust/renderer/registry.rs +18 -6
  139. package/src/rust/renderer/render_timer.rs +29 -10
  140. package/src/rust/renderer.rs +350 -827
  141. package/src/rust/session/column_defaults_update.rs +26 -8
  142. package/src/rust/session/metadata.rs +11 -1
  143. package/src/rust/session/view_subscription.rs +30 -15
  144. package/src/rust/session.rs +753 -251
  145. package/src/rust/tasks/apply_global_filters.rs +89 -0
  146. package/src/rust/tasks/auto_pause.rs +171 -0
  147. package/src/rust/tasks/copy_export.rs +29 -19
  148. package/src/rust/tasks/create_panel.rs +243 -0
  149. package/src/rust/tasks/dismiss_render_warning.rs +3 -1
  150. package/src/rust/tasks/edit_expression.rs +4 -4
  151. package/src/rust/tasks/eject.rs +64 -23
  152. package/src/rust/tasks/mod.rs +28 -4
  153. package/src/rust/tasks/pipeline.rs +411 -0
  154. package/src/rust/tasks/presize_panels.rs +324 -0
  155. package/src/rust/tasks/reset_all.rs +22 -12
  156. package/src/rust/tasks/resize_observer.rs +124 -87
  157. package/src/rust/tasks/restore_and_render.rs +80 -86
  158. package/src/rust/tasks/restore_panel.rs +145 -0
  159. package/src/rust/tasks/send_column_config.rs +5 -2
  160. package/src/rust/tasks/send_plugin_config.rs +5 -2
  161. package/src/rust/tasks/set_edit_mode.rs +59 -0
  162. package/src/rust/tasks/sync_update_panels.rs +152 -0
  163. package/src/rust/tasks/table_lifecycle.rs +158 -0
  164. package/src/rust/tasks/update_theme.rs +56 -12
  165. package/src/rust/utils/completion.rs +56 -0
  166. package/src/rust/utils/custom_element.rs +5 -0
  167. package/src/rust/utils/debounce.rs +130 -7
  168. package/src/rust/utils/mod.rs +6 -0
  169. package/src/rust/utils/pending_effects.rs +62 -0
  170. package/src/rust/{components/style/local_style.rs → utils/spawn.rs} +17 -19
  171. package/src/rust/utils/weak_scope.rs +0 -10
  172. package/src/rust/workspace.rs +846 -0
  173. package/src/svg/active-indicator.svg +3 -0
  174. package/src/themes/botanical.css +7 -2
  175. package/src/themes/defaults.css +54 -2
  176. package/src/themes/dracula.css +8 -3
  177. package/src/themes/gruvbox-dark.css +7 -2
  178. package/src/themes/gruvbox.css +5 -2
  179. package/src/themes/icons.css +4 -2
  180. package/src/themes/monokai.css +7 -2
  181. package/src/themes/phosphor.css +7 -2
  182. package/src/themes/pro-dark.css +7 -2
  183. package/src/themes/pro.css +2 -1
  184. package/src/themes/solarized-dark.css +5 -2
  185. package/src/themes/solarized.css +5 -2
  186. package/src/themes/vaporwave.css +7 -2
  187. package/src/ts/bootstrap.ts +8 -7
  188. package/src/ts/extensions.ts +30 -2
  189. package/src/ts/perspective-viewer.ts +10 -0
  190. package/src/ts/plugin.ts +72 -5
  191. package/src/ts/ts-rs/ClientOptions.ts +7 -0
  192. package/src/ts/ts-rs/ExportMethod.ts +3 -0
  193. package/src/ts/ts-rs/ExportOptions.ts +8 -0
  194. package/src/ts/ts-rs/GetClientOptions.ts +7 -0
  195. package/src/ts/ts-rs/GetTableOptions.ts +7 -0
  196. package/src/ts/ts-rs/Layout.ts +9 -0
  197. package/src/ts/ts-rs/Orientation.ts +6 -0
  198. package/src/ts/ts-rs/PanelOptions.ts +7 -0
  199. package/src/ts/ts-rs/PanelViewerConfig.ts +15 -0
  200. package/src/ts/ts-rs/SplitLayout.ts +5 -0
  201. package/src/ts/ts-rs/TabLayout.ts +11 -0
  202. package/src/ts/ts-rs/ViewerConfig.ts +6 -2
  203. package/src/ts/ts-rs/WorkspaceConfig.ts +36 -0
  204. package/src/ts/ts-rs/WorkspaceConfigUpdate.ts +24 -0
  205. package/tsconfig.json +1 -1
  206. package/src/rust/components/style/style_cache.rs +0 -136
  207. package/src/rust/tasks/intersection_observer.rs +0 -98
  208. package/src/rust/tasks/update_and_render.rs +0 -110
  209. /package/dist/wasm/snippets/{perspective-viewer-3cd58f0374935772/inline0.js → perspective-viewer-daeef4ceb066e6c7/inline1.js} +0 -0
  210. /package/dist/wasm/snippets/{perspective-viewer-3cd58f0374935772/inline1.js → perspective-viewer-daeef4ceb066e6c7/inline2.js} +0 -0
  211. /package/dist/wasm/snippets/{perspective-viewer-3cd58f0374935772/inline2.js → perspective-viewer-daeef4ceb066e6c7/inline3.js} +0 -0
  212. /package/dist/wasm/snippets/{perspective-viewer-3cd58f0374935772/inline4.js → perspective-viewer-daeef4ceb066e6c7/inline5.js} +0 -0
@@ -11,11 +11,13 @@
11
11
  // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
12
 
13
13
  //! Apply a full [`ViewerConfigUpdate`] (settings, theme, title, plugin,
14
- //! plugin_config, columns_config, view_config) and re-draw.
14
+ //! plugin_config, columns_config, view_config) and re-draw, on the snapshot
15
+ //! pipeline (see `tasks/pipeline.rs` / `SESSION_CONFIG_COHERENCE_PLAN.md`).
15
16
 
16
17
  use futures::Future;
17
18
  use perspective_client::clone;
18
19
 
20
+ use super::pipeline::{RunOrigin, RunSpec, locked_run};
19
21
  use crate::config::{OptionalUpdate, ViewerConfigUpdate};
20
22
  use crate::presentation::Presentation;
21
23
  use crate::renderer::Renderer;
@@ -25,10 +27,24 @@ use crate::*;
25
27
  /// Apply a full [`ViewerConfigUpdate`] (theme, title, plugin selection,
26
28
  /// plugin config, columns config, view config) to the engines and re-draw.
27
29
  /// Returns an [`ApiFuture<()>`] which resolves when the draw completes.
30
+ ///
31
+ /// `origin` says who initiated the restore ([`RunOrigin`]): a `Public`
32
+ /// element-API call keeps the no-op-restore refresh affordance (an
33
+ /// `Unchanged` reconcile still repaints via `update`); an `Internal`
34
+ /// restore that reconciles `Unchanged` and changes no plugin state
35
+ /// dispatches nothing.
36
+ ///
37
+ /// This function owns the PRE-LOCK prologue only (element-level settings /
38
+ /// title / host-theme mirror, plugin resolution, the synchronous config
39
+ /// commit and spinner token); the run itself is
40
+ /// [`locked_run`] with the caller's `task` awaited inside the lock. The
41
+ /// `update`'s `table` field is NOT applied here — table binding is the
42
+ /// `task`'s job (see `restore_panel` / `table_lifecycle`).
28
43
  pub fn restore_and_render(
29
44
  session: &Session,
30
45
  renderer: &Renderer,
31
46
  presentation: &Presentation,
47
+ origin: RunOrigin,
32
48
  ViewerConfigUpdate {
33
49
  plugin,
34
50
  plugin_config,
@@ -54,103 +70,81 @@ pub fn restore_and_render(
54
70
  session.set_title(None);
55
71
  }
56
72
 
57
- let needs_restyle = match theme_name {
73
+ // Mirror a config-carried theme onto the host attribute (the shared
74
+ // chrome). The restyle a theme change requires is owned by the
75
+ // MUTATION SITES (`restorePanel`'s own-theme tail, the theme-picker
76
+ // task), not here — this run's own draw below already stamps the new
77
+ // effective theme before the plugin's first style read.
78
+ match theme_name {
58
79
  OptionalUpdate::SetDefault => {
59
80
  let current_name = presentation.get_selected_theme_name().await;
60
81
  if current_name.is_some() {
61
82
  presentation.set_theme_name(None).await?;
62
- true
63
- } else {
64
- false
65
83
  }
66
84
  },
67
85
  OptionalUpdate::Update(x) => {
68
- let current_name = presentation.get_selected_theme_name().await;
69
- if current_name.is_some() && current_name.as_ref().unwrap() != &x {
70
- presentation.set_theme_name(Some(&x)).await?;
71
- true
72
- } else {
73
- false
74
- }
86
+ // No pre-resolution gate: `set_theme_name` stamps the host
87
+ // attribute SYNCHRONOUSLY before its registry await and
88
+ // no-ops on literal equality itself. The old
89
+ // `get_selected_theme_name().await` guard was precisely
90
+ // the former-theme window on a cold registry — the host
91
+ // held the old attribute until stylesheet parsing (and
92
+ // everything queued behind it) resolved.
93
+ presentation.set_theme_name(Some(&x)).await?;
75
94
  },
76
- _ => false,
95
+ _ => {},
77
96
  };
78
97
 
79
- if let Some(metadata) = renderer.get_next_plugin_metadata(&plugin) {
80
- session.set_update_column_defaults(&mut view_config, &metadata);
98
+ // Resolve the target plugin here (pure — needed now for
99
+ // `set_update_column_defaults`), but COMMIT it only inside the locked
100
+ // run below, atomically with the view rebind it belongs to. No swap
101
+ // intent may exist outside that run: an unrelated run that wins the
102
+ // lock first (e.g. a `table_updated` redraw) must observe either the
103
+ // fully-old or fully-new world, never a staged half of this one.
104
+ let resolved_plugin = renderer.resolve_plugin_update(&plugin);
105
+ if let Some((_, metadata)) = &resolved_plugin {
106
+ session.set_update_column_defaults(&mut view_config, metadata);
107
+ } else {
108
+ // Same-plugin (or plugin-less) restore: `resolve_plugin_update`
109
+ // returns `None`, but the plugin-advised `group_rollup_mode`
110
+ // must STILL be enforced against the active plugin's metadata —
111
+ // `restorePanel`'s table-change reset wipes the committed mode,
112
+ // and nothing else on this path would restore it, leaving a
113
+ // flat-only chart (Treemap / Sunburst) rendering rollup
114
+ // subtotal rows. Rollup only; the full column-defaults pass is
115
+ // reserved for plugin swaps, where `columns` genuinely needs
116
+ // re-defaulting.
117
+ session.set_update_rollup_defaults(&mut view_config, &renderer.metadata());
81
118
  }
82
119
 
83
- session.update_view_config(view_config)?;
84
- let draw_task = renderer.draw(async {
85
- task.await?;
86
- let plugin_swapped = renderer.apply_pending_plugin()?;
87
- let plugin = renderer.get_active_plugin()?;
88
-
89
- // The previous call which acquired the lock errored, so skip this render
90
- if let Some(error) = session.get_error() {
91
- return Err(error);
92
- }
93
-
94
- // Validate + create the view BEFORE applying
95
- // columns_config / plugin_config updates, so the
96
- // strip-on-write and materialize passes see fresh
97
- // `expression_schema` and `view_schema`, and the
98
- // materialize warm step can call `View::get_min_max`
99
- // against a view that knows about any new expression
100
- // columns. Previously this happened after the strip,
101
- // which silently dropped any `columns_config` entry
102
- // keyed by a new expression column.
103
- let view = session.validate().await?.create_view().await?;
104
-
105
- // Apply incoming updates into the now-active plugin's
106
- // bucket on `Renderer`. Per-plugin storage means no
107
- // schema filter is needed before restore — foreign keys
108
- // cannot appear in the bucket by construction. The
109
- // renderer methods also strip schema-default entries so
110
- // restored maps containing default values produce an
111
- // empty bucket (correct: bucket-empty ⇒ reads-default).
112
- let view_config_snapshot = session.get_view_config().clone();
113
- let plugin_config_changed =
114
- renderer.update_plugin_config(&view_config_snapshot, plugin_config);
115
- let columns_config_changed =
116
- renderer.update_columns_configs(&view_config_snapshot, &session, columns_config);
117
- let changed = plugin_config_changed || columns_config_changed;
118
-
119
- // Force a materialized restore when the plugin just
120
- // swapped — `commit_plugin_idx` already restored from the
121
- // raw bucket, but the materialized restore is needed for
122
- // schema-revealed `include: true` defaults to reach the
123
- // plugin before its first draw.
124
- if changed || plugin_swapped {
125
- let plugin_config_snapshot = renderer.get_plugin_config();
126
- let plugin_update =
127
- wasm_bindgen::JsValue::from_serde_ext(&plugin_config_snapshot).unwrap();
128
- let columns_config = renderer
129
- .all_columns_configs_materialized(&view_config_snapshot, &session)
130
- .await;
131
- plugin.restore(&plugin_update, Some(&columns_config))?;
132
- if plugin_config_changed {
133
- renderer.plugin_config_changed.emit(plugin_config_snapshot);
134
- }
135
- }
136
-
137
- if !presentation.is_visible() {
138
- Ok(None)
139
- } else {
140
- Ok(view)
141
- }
142
- });
143
-
144
- draw_task.await?;
145
-
146
- // TODO this should be part of the API for `draw()` above, such that
147
- // the plugin need not render twice when a theme is provided.
148
- if needs_restyle
149
- && presentation.is_visible()
150
- && let Some(view) = session.get_view()
151
- {
152
- renderer.restyle_all(&view).await?;
153
- }
120
+ let plugin_idx = resolved_plugin.map(|(idx, _)| idx);
121
+
122
+ // The config COMMIT: synchronous, validated, atomic (I1/I4). Under
123
+ // I2/I3 committing before the lock is safe — whichever queued run
124
+ // snapshots next picks it up, and this run's own snapshot (taken
125
+ // inside the lock below) can only be this commit or fresher.
126
+ session.commit_view_config(view_config)?;
127
+
128
+ // Spinner accounting (RAII): held to the end of this restore —
129
+ // INCLUDING the deferred-draw exit (no table yet → no
130
+ // `bind_snapshot`), which under the old edge-counted scheme
131
+ // stranded the `StatusIndicator` spinner permanently.
132
+ let _run_token = session.begin_config_run();
133
+
134
+ // Awaits theme-registry init, so the stamp below can never observe a
135
+ // pre-init (empty) theme set on a cold first load. Seeds this
136
+ // panel's renderer default-theme cache, which every locked draw
137
+ // stamps the effective theme from.
138
+ renderer.set_default_theme(presentation.get_default_theme_name().await);
139
+ locked_run(&session, &renderer, RunSpec {
140
+ origin,
141
+ plugin_idx,
142
+ plugin_config,
143
+ columns_config,
144
+ task: Some(Box::pin(task)),
145
+ presentation: Some(presentation.clone()),
146
+ })
147
+ .await?;
154
148
 
155
149
  Ok(())
156
150
  })
@@ -0,0 +1,145 @@
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 futures::channel::oneshot::channel;
14
+ use perspective_client::clone;
15
+ use perspective_client::utils::PerspectiveResultExt;
16
+
17
+ use crate::components::viewer::{PerspectiveViewer, PerspectiveViewerMsg};
18
+ use crate::config::*;
19
+ use crate::presentation::Presentation;
20
+ use crate::renderer::Renderer;
21
+ use crate::root::Root;
22
+ use crate::session::{ResetOptions, Session};
23
+ use crate::tasks::*;
24
+ use crate::workspace::Workspace;
25
+ use crate::*;
26
+
27
+ /// How a [`restore_panel`] call reached the pipeline — the only two genuine
28
+ /// forks between updating a live panel and materializing a freshly-created one.
29
+ pub(crate) enum RestoreMode {
30
+ Existing { active: bool },
31
+ Fresh,
32
+ }
33
+
34
+ pub(crate) async fn bind_table_task(
35
+ session: &Session,
36
+ workspace: &Workspace,
37
+ name: String,
38
+ ) -> ApiResult<()> {
39
+ let current = session.get_client();
40
+ if let Some(client) = workspace
41
+ .resolve_client_for_table(&name, current.as_ref())
42
+ .await
43
+ {
44
+ session.set_client(client);
45
+ }
46
+
47
+ session.set_table(name).await?;
48
+ session.commit_table_defaults();
49
+ Ok(())
50
+ }
51
+
52
+ /// Apply a [`ViewerConfigUpdate`] to a single panel and re-draw — the one
53
+ /// pipeline shared by `restorePanel` (an existing panel), whole-element
54
+ /// `restoreWorkspace`, and `addPanel` (both fresh panels).
55
+ pub(crate) async fn restore_panel(
56
+ session: &Session,
57
+ renderer: &Renderer,
58
+ presentation: &Presentation,
59
+ workspace: &Workspace,
60
+ root: Option<&Root<PerspectiveViewer>>,
61
+ mode: RestoreMode,
62
+ mut update: ViewerConfigUpdate,
63
+ ) -> ApiResult<()> {
64
+ let active = matches!(mode, RestoreMode::Existing { active: true });
65
+ let fresh = matches!(mode, RestoreMode::Fresh);
66
+ match &update.theme {
67
+ OptionalUpdate::Update(theme) => renderer.set_theme_stamped(Some(theme.clone())),
68
+ OptionalUpdate::SetDefault => renderer.set_theme_stamped(None),
69
+ OptionalUpdate::Missing => {},
70
+ }
71
+
72
+ if !active {
73
+ update.theme = OptionalUpdate::Missing;
74
+ }
75
+
76
+ if !fresh {
77
+ tracing::info!("Restoring {update}");
78
+ }
79
+
80
+ let (sender, receiver) = channel::<()>();
81
+ match (active, root) {
82
+ (true, Some(root)) => {
83
+ root.borrow().as_ref().into_apierror()?.send_message(
84
+ PerspectiveViewerMsg::ToggleSettingsComplete(update.settings.clone(), sender),
85
+ );
86
+ },
87
+ _ => {
88
+ let _ = sender.send(());
89
+ },
90
+ }
91
+
92
+ let table_changed = !fresh
93
+ && matches!(&update.table, OptionalUpdate::Update(name)
94
+ if session.get_table().map(|t| t.get_name() != name.as_str()).unwrap_or(true));
95
+
96
+ let errored_recovery =
97
+ session.is_errored() && matches!(&update.table, OptionalUpdate::Update(_));
98
+
99
+ let reset = (table_changed || errored_recovery).then(|| {
100
+ session.reset(ResetOptions {
101
+ config: true,
102
+ expressions: true,
103
+ stats: true,
104
+ ..ResetOptions::default()
105
+ })
106
+ });
107
+
108
+ let result = restore_and_render(
109
+ session,
110
+ renderer,
111
+ presentation,
112
+ RunOrigin::Public,
113
+ update.clone(),
114
+ {
115
+ clone!(session, update.table, workspace);
116
+ async move {
117
+ if let OptionalUpdate::Update(name) = table {
118
+ if let Some(reset) = reset {
119
+ reset.await?;
120
+ }
121
+
122
+ bind_table_task(&session, &workspace, name).await?;
123
+ }
124
+
125
+ receiver.await.unwrap_or_log();
126
+ Ok(())
127
+ }
128
+ },
129
+ )
130
+ .await;
131
+
132
+ if let Err(e) = &result {
133
+ session.set_error(false, e.clone()).await?;
134
+ }
135
+
136
+ result?;
137
+
138
+ if fresh {
139
+ renderer.resize().await.unwrap_or_log();
140
+ } else if renderer.needs_restyle() {
141
+ renderer.restyle_all().await?;
142
+ }
143
+
144
+ Ok(())
145
+ }
@@ -44,10 +44,13 @@ pub fn send_column_config(
44
44
  let plugin_token =
45
45
  wasm_bindgen::JsValue::from_serde_ext(&renderer.get_plugin_config()).unwrap();
46
46
  renderer
47
- .get_active_plugin()?
47
+ .ensure_plugin_selected()?
48
48
  .restore(&plugin_token, Some(&columns_configs))?;
49
49
 
50
- renderer.update(session.get_view()).await?;
50
+ clone!(session);
51
+ renderer
52
+ .update_lazy(async move { Ok(session.get_view()) })
53
+ .await?;
51
54
  renderer.column_style_changed.emit(columns_configs);
52
55
  Ok(())
53
56
  })
@@ -40,9 +40,12 @@ pub fn send_plugin_config(session: &Session, renderer: &Renderer, update: Column
40
40
  .all_columns_configs_materialized(&view_config_snapshot, &session)
41
41
  .await;
42
42
  renderer
43
- .get_active_plugin()?
43
+ .ensure_plugin_selected()?
44
44
  .restore(&plugin_token, Some(&columns_configs))?;
45
- renderer.update(session.get_view()).await?;
45
+ clone!(session);
46
+ renderer
47
+ .update_lazy(async move { Ok(session.get_view()) })
48
+ .await?;
46
49
  renderer.plugin_config_changed.emit(plugin_config);
47
50
  }
48
51
 
@@ -0,0 +1,59 @@
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 perspective_client::clone;
14
+ use perspective_js::utils::*;
15
+
16
+ use crate::config::*;
17
+ use crate::renderer::Renderer;
18
+ use crate::session::Session;
19
+
20
+ /// Set the active plugin's `edit_mode`, persisting it in the [`Renderer`]'s
21
+ /// plugin bucket and re-`restore`+rendering (the same merged-token path as
22
+ /// [`super::send_plugin_config`], so column styles/sizes survive).
23
+ ///
24
+ /// Master/detail uses this to put a master into `SELECT_ROW_TREE` (the mode
25
+ /// in which the datagrid emits `perspective-global-filter` selections) and to
26
+ /// restore `READ_ONLY` on demotion. Plugins with no `edit_mode` schema field
27
+ /// (e.g. charts) schema-gate the key out in [`Renderer::update_plugin_config`],
28
+ /// making this a no-op for them.
29
+ pub fn set_edit_mode(session: &Session, renderer: &Renderer, mode: &str) {
30
+ let view_config = session.get_view_config().clone();
31
+ let mut map = serde_json::Map::new();
32
+ map.insert(
33
+ "edit_mode".to_owned(),
34
+ serde_json::Value::String(mode.to_owned()),
35
+ );
36
+
37
+ let changed = renderer.update_plugin_config(&view_config, OptionalUpdate::Update(map));
38
+ clone!(session, renderer);
39
+ ApiFuture::spawn(async move {
40
+ if changed {
41
+ let plugin_config = renderer.get_plugin_config();
42
+ let plugin_token = wasm_bindgen::JsValue::from_serde_ext(&plugin_config).unwrap();
43
+ let view_config_snapshot = session.get_view_config().clone();
44
+ let columns_configs = renderer
45
+ .all_columns_configs_materialized(&view_config_snapshot, &session)
46
+ .await;
47
+ renderer
48
+ .ensure_plugin_selected()?
49
+ .restore(&plugin_token, Some(&columns_configs))?;
50
+ clone!(session);
51
+ renderer
52
+ .update_lazy(async move { Ok(session.get_view()) })
53
+ .await?;
54
+ renderer.plugin_config_changed.emit(plugin_config);
55
+ }
56
+
57
+ Ok(())
58
+ })
59
+ }
@@ -0,0 +1,152 @@
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::HashMap;
14
+
15
+ use wasm_bindgen::prelude::*;
16
+
17
+ use crate::components::viewer::PerspectiveViewerMsg;
18
+ use crate::config::*;
19
+ use crate::renderer::*;
20
+ use crate::session::Session;
21
+ use crate::tasks::*;
22
+ use crate::workspace::PanelId;
23
+ use crate::*;
24
+
25
+ #[wasm_bindgen]
26
+ extern "C" {
27
+ #[wasm_bindgen(typescript_type = "ViewerConfigUpdate")]
28
+ pub type JsViewerConfigUpdate;
29
+ }
30
+
31
+ pub type SyncUpdatePanelsResult = (
32
+ Vec<(PanelId, Session, Renderer, ViewerConfigUpdate)>,
33
+ std::vec::Vec<perspective_js::utils::ApiFuture<()>>,
34
+ );
35
+
36
+ /// Update the panel state to match a `WorkspaceConfigUpdate`.
37
+ ///
38
+ /// This method must be synchronous.
39
+ pub fn sync_update_panels(
40
+ this: &PerspectiveViewerElement,
41
+ update: JsViewerConfigUpdate,
42
+ ) -> ApiResult<SyncUpdatePanelsResult> {
43
+ let WorkspaceConfigUpdate {
44
+ active,
45
+ layout,
46
+ panels,
47
+ global_filters,
48
+ masters,
49
+ } = update.into_serde_ext()?;
50
+
51
+ let old_ids = this.workspace.panel_ids();
52
+
53
+ // Phase 1 — models only, NO renders and NO draws
54
+ let mut id_map: HashMap<String, String> = Default::default();
55
+ let mut fallback_fresh: Option<String> = None;
56
+ let mut active_fresh: Option<String> = None;
57
+ let mut contents = Vec::new();
58
+ for (saved_id, config) in panels {
59
+ if !matches!(config.settings, OptionalUpdate::Missing) {
60
+ #[rustfmt::skip]
61
+ tracing::warn!(
62
+ "`settings` on panel \"{saved_id}\" is ignored in a whole-element config; use the top-level `active` field"
63
+ );
64
+ }
65
+
66
+ let (fresh, session, renderer, config) = create_panel_model(
67
+ &this.elem,
68
+ &this.presentation,
69
+ &this.workspace,
70
+ None,
71
+ config,
72
+ None,
73
+ Placement::Placed,
74
+ );
75
+
76
+ if active.as_deref() == Some(saved_id.as_str()) {
77
+ active_fresh = Some(fresh.as_str().to_owned());
78
+ }
79
+
80
+ fallback_fresh.get_or_insert_with(|| fresh.as_str().to_owned());
81
+ id_map.insert(saved_id, fresh.as_str().to_owned());
82
+ contents.push((fresh, session, renderer, config));
83
+ }
84
+
85
+ if contents.is_empty() {
86
+ let (fresh, session, renderer, config) = create_panel_model(
87
+ &this.elem,
88
+ &this.presentation,
89
+ &this.workspace,
90
+ None,
91
+ ViewerConfigUpdate::default(),
92
+ None,
93
+ Placement::Placed,
94
+ );
95
+
96
+ fallback_fresh = Some(fresh.as_str().to_owned());
97
+ contents.push((fresh, session, renderer, config));
98
+ }
99
+
100
+ let mut eject_tasks = Vec::new();
101
+ if let Some(panel) = this.workspace.take_reserved() {
102
+ eject_tasks.push(eject_panel(panel));
103
+ }
104
+
105
+ for old in old_ids {
106
+ if let Some(panel) = this.workspace.remove_panel(&old) {
107
+ eject_tasks.push(eject_panel(panel));
108
+ }
109
+ }
110
+
111
+ // Phase 2 — stage the remapped layout tree on the Workspace
112
+ if let Some(layout) = layout {
113
+ this.workspace
114
+ .set_pending_layout(layout.remap(&|name| id_map.get(name).cloned()));
115
+ }
116
+
117
+ // Phase 3 — the single visible commit
118
+ if let Some(saved) = &active
119
+ && active_fresh.is_none()
120
+ {
121
+ tracing::warn!("`active` names unknown panel \"{saved}\"");
122
+ }
123
+
124
+ let sidebar_open = active_fresh.is_some();
125
+ if let Some(target) = active_fresh.or(fallback_fresh)
126
+ && let Some(app) = this.root.borrow().as_ref()
127
+ {
128
+ app.send_message(PerspectiveViewerMsg::CommitWorkspaceRestore(target));
129
+ }
130
+
131
+ if let Some(app) = this.root.borrow().as_ref() {
132
+ app.send_message(PerspectiveViewerMsg::ToggleSettingsInit(
133
+ Some(SettingsUpdate::Update(sidebar_open)),
134
+ None,
135
+ ));
136
+ }
137
+
138
+ let masters = masters
139
+ .into_iter()
140
+ .filter_map(|saved| match id_map.get(&saved) {
141
+ Some(fresh) => Some(PanelId::from(fresh.as_str())),
142
+ None => {
143
+ tracing::warn!("`masters` names unknown panel \"{saved}\"");
144
+ None
145
+ },
146
+ })
147
+ .collect::<Vec<_>>();
148
+
149
+ this.workspace.set_masters(masters);
150
+ this.workspace.set_global_filters(global_filters);
151
+ Ok((contents, eject_tasks))
152
+ }