@perspective-dev/viewer 5.5.0 → 5.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (201) hide show
  1. package/README.md +82 -3
  2. package/dist/cdn/perspective-viewer.js +9 -9
  3. package/dist/cdn/perspective-viewer.js.map +3 -3
  4. package/dist/css/blueprint.css +1 -1
  5. package/dist/css/botanical.css +1 -1
  6. package/dist/css/dracula.css +1 -1
  7. package/dist/css/eggplant.css +1 -1
  8. package/dist/css/gruvbox-dark.css +1 -1
  9. package/dist/css/gruvbox.css +1 -1
  10. package/dist/css/icons.css +1 -1
  11. package/dist/css/intl/ar.css +1 -1
  12. package/dist/css/intl/de.css +1 -1
  13. package/dist/css/intl/es.css +1 -1
  14. package/dist/css/intl/fr.css +1 -1
  15. package/dist/css/intl/ja.css +1 -1
  16. package/dist/css/intl/pt.css +1 -1
  17. package/dist/css/intl/zh.css +1 -1
  18. package/dist/css/intl.css +1 -1
  19. package/dist/css/ledger.css +1 -1
  20. package/dist/css/monokai.css +1 -1
  21. package/dist/css/nord.css +1 -1
  22. package/dist/css/phosphor.css +1 -1
  23. package/dist/css/pro-dark.css +1 -1
  24. package/dist/css/pro.css +1 -1
  25. package/dist/css/solarized-dark.css +1 -1
  26. package/dist/css/solarized.css +1 -1
  27. package/dist/css/themes.css +1 -1
  28. package/dist/css/vaporwave.css +1 -1
  29. package/dist/css/velvet.css +1 -1
  30. package/dist/docs/perspective-docs.json +1 -1
  31. package/dist/esm/column-format.js +2 -0
  32. package/dist/esm/column-format.js.map +7 -0
  33. package/dist/esm/extensions.d.ts +3 -13
  34. package/dist/esm/perspective-viewer.d.ts +1 -1
  35. package/dist/esm/perspective-viewer.js +9 -9
  36. package/dist/esm/perspective-viewer.js.map +3 -3
  37. package/dist/esm/plugin.d.ts +4 -1
  38. package/dist/esm/select-detail.d.ts +15 -0
  39. package/dist/esm/select-detail.js +2 -0
  40. package/dist/esm/select-detail.js.map +7 -0
  41. package/dist/esm/ts-rs/DerivedTableConfig.d.ts +35 -0
  42. package/dist/esm/ts-rs/ExportMethod.d.ts +1 -1
  43. package/dist/esm/ts-rs/PanelViewerConfig.d.ts +7 -6
  44. package/dist/esm/ts-rs/RestoreOptions.d.ts +3 -7
  45. package/dist/esm/ts-rs/TableRef.d.ts +9 -0
  46. package/dist/esm/ts-rs/ViewerConfig.d.ts +7 -6
  47. package/dist/esm/ts-rs/ViewerConfigUpdate.d.ts +7 -5
  48. package/dist/esm/ts-rs/WorkspaceConfig.d.ts +9 -1
  49. package/dist/esm/ts-rs/WorkspaceConfigUpdate.d.ts +9 -0
  50. package/dist/wasm/perspective-viewer.d.ts +126 -35
  51. package/dist/wasm/perspective-viewer.js +320 -185
  52. package/dist/wasm/perspective-viewer.wasm +0 -0
  53. package/dist/wasm/perspective-viewer.wasm.d.ts +21 -17
  54. package/package.json +33 -20
  55. package/src/css/column-selector.css +205 -50
  56. package/src/css/ui/containers/context-menu.css +12 -1
  57. package/src/css/ui/containers/dropdown-menu-items.css +29 -0
  58. package/src/css/ui/containers/scroll-panel.css +4 -1
  59. package/src/rust/agent/client/protocol.rs +0 -89
  60. package/src/rust/agent/markdown/mod.rs +0 -238
  61. package/src/rust/agent/preamble.md +1 -0
  62. package/src/rust/agent/tools.rs +2 -16
  63. package/src/rust/components/column_selector/active_column.rs +15 -5
  64. package/src/rust/components/column_selector/add_expression_button.rs +9 -26
  65. package/src/rust/components/column_selector/config_selector.rs +34 -6
  66. package/src/rust/components/column_selector/derived_control.rs +229 -0
  67. package/src/rust/components/column_selector/expr_edit_button.rs +3 -2
  68. package/src/rust/components/column_selector/filter_column.rs +0 -2
  69. package/src/rust/components/column_selector/inactive_column.rs +4 -5
  70. package/src/rust/components/column_selector/pivot_column.rs +21 -1
  71. package/src/rust/components/column_selector/sort_column.rs +4 -3
  72. package/src/rust/components/column_selector.rs +21 -1
  73. package/src/rust/components/column_settings_sidebar/save_settings.rs +5 -3
  74. package/src/rust/components/column_settings_sidebar/style_tab.rs +1 -0
  75. package/src/rust/components/column_settings_sidebar.rs +25 -19
  76. package/src/rust/components/debug_panel.rs +38 -29
  77. package/src/rust/components/dragdrop_list.rs +8 -9
  78. package/src/rust/components/export_dropdown.rs +6 -0
  79. package/src/rust/components/main_panel/msg.rs +6 -4
  80. package/src/rust/components/main_panel/reconcile.rs +6 -1
  81. package/src/rust/components/main_panel/render.rs +70 -51
  82. package/src/rust/components/main_panel/update.rs +39 -1
  83. package/src/rust/components/main_panel.rs +97 -59
  84. package/src/rust/components/new_panel_menu.rs +69 -1
  85. package/src/rust/components/panel_menu.rs +63 -12
  86. package/src/rust/components/plugin_tab.rs +3 -2
  87. package/src/rust/components/render_warning.rs +2 -1
  88. package/src/rust/components/settings_panel.rs +4 -4
  89. package/src/rust/components/status_bar.rs +80 -21
  90. package/src/rust/components/status_indicator.rs +8 -1
  91. package/src/rust/components/viewer/filters.rs +15 -3
  92. package/src/rust/components/viewer/msg.rs +8 -0
  93. package/src/rust/components/viewer/panels.rs +44 -4
  94. package/src/rust/components/viewer/render.rs +1 -0
  95. package/src/rust/components/viewer/settings.rs +0 -22
  96. package/src/rust/components/viewer/snapshots.rs +2 -2
  97. package/src/rust/components/viewer/wiring.rs +18 -1
  98. package/src/rust/components/viewer.rs +47 -27
  99. package/src/rust/components/window_editor.rs +5 -4
  100. package/src/rust/config/column_config_schema.rs +38 -377
  101. package/src/rust/config/export_method.rs +24 -0
  102. package/src/rust/config/number_string_format.rs +0 -94
  103. package/src/rust/config/options.rs +3 -7
  104. package/src/rust/config/plugin_static_config.rs +0 -42
  105. package/src/rust/config/viewer_config.rs +47 -17
  106. package/src/rust/config/workspace_config.rs +158 -128
  107. package/src/rust/custom_elements/viewer.rs +189 -129
  108. package/src/rust/custom_events.rs +33 -18
  109. package/src/rust/derived.rs +266 -0
  110. package/src/rust/exprtk/tokenize.rs +0 -54
  111. package/src/rust/js/plugin.rs +1 -1
  112. package/src/rust/lib.rs +3 -0
  113. package/src/rust/presentation.rs +23 -4
  114. package/src/rust/queries/column_locator.rs +55 -48
  115. package/src/rust/queries/get_viewer_config.rs +1 -1
  116. package/src/rust/queries/plugin_column_styles.rs +22 -23
  117. package/src/rust/renderer/dispatch.rs +2 -0
  118. package/src/rust/renderer/plugin_config.rs +652 -487
  119. package/src/rust/renderer/props.rs +4 -0
  120. package/src/rust/renderer/registry.rs +2 -6
  121. package/src/rust/renderer/state.rs +205 -0
  122. package/src/rust/renderer.rs +201 -59
  123. package/src/rust/session/metadata.rs +35 -19
  124. package/src/rust/session/op_queue.rs +430 -0
  125. package/src/rust/session/panel_state.rs +327 -0
  126. package/src/rust/session/view_subscription.rs +42 -3
  127. package/src/rust/session.rs +931 -419
  128. package/src/rust/tasks/apply_global_filters.rs +49 -35
  129. package/src/rust/tasks/auto_pause.rs +22 -12
  130. package/src/rust/tasks/copy_export.rs +72 -0
  131. package/src/rust/tasks/create_panel.rs +46 -21
  132. package/src/rust/tasks/derive.rs +312 -0
  133. package/src/rust/tasks/mod.rs +6 -2
  134. package/src/rust/tasks/pipeline.rs +152 -89
  135. package/src/rust/tasks/reset_all.rs +31 -20
  136. package/src/rust/tasks/restore_derived.rs +200 -0
  137. package/src/rust/tasks/restore_panel.rs +153 -141
  138. package/src/rust/tasks/send_column_config.rs +56 -29
  139. package/src/rust/tasks/send_plugin_config.rs +52 -23
  140. package/src/rust/tasks/set_edit_mode.rs +33 -25
  141. package/src/rust/tasks/sync_update_panels.rs +40 -13
  142. package/src/rust/tasks/table_lifecycle.rs +70 -34
  143. package/src/rust/tasks/transactional_restore.rs +387 -0
  144. package/src/rust/tasks/update_theme.rs +45 -6
  145. package/src/rust/ui/containers/scroll_panel.rs +17 -10
  146. package/src/rust/ui/containers/split_panel.rs +0 -41
  147. package/src/rust/ui/context_menu.rs +157 -18
  148. package/src/rust/ui/font_loader.rs +23 -35
  149. package/src/rust/ui/form/mirrored_textarea.rs +0 -12
  150. package/src/rust/ui/form/multi_stop_gradient_selector.rs +0 -89
  151. package/src/rust/ui/form/palette_selector.rs +0 -51
  152. package/src/rust/ui/portal.rs +4 -10
  153. package/src/rust/utils/browser/mod.rs +2 -3
  154. package/src/{ts/perspective-viewer.inline.ts → rust/utils/browser/mouse.rs} +11 -10
  155. package/src/rust/utils/browser/request_animation_frame.rs +27 -8
  156. package/src/rust/utils/css.rs +0 -360
  157. package/src/rust/utils/{tests/mod.rs → in_flight.rs} +62 -2
  158. package/src/rust/utils/local_fonts.rs +0 -36
  159. package/src/rust/utils/mod.rs +4 -3
  160. package/src/rust/utils/modal_position.rs +15 -21
  161. package/src/rust/utils/pending_effects.rs +6 -30
  162. package/src/rust/utils/{browser/tests/mod.rs → plugin_host_state.rs} +31 -1
  163. package/src/rust/utils/pubsub.rs +0 -1
  164. package/src/rust/workspace.rs +187 -140
  165. package/src/svg/derived.svg +6 -0
  166. package/src/themes/icons.css +1 -0
  167. package/src/themes/intl/ar.css +10 -0
  168. package/src/themes/intl/de.css +10 -0
  169. package/src/themes/intl/es.css +10 -0
  170. package/src/themes/intl/fr.css +10 -0
  171. package/src/themes/intl/ja.css +10 -0
  172. package/src/themes/intl/pt.css +10 -0
  173. package/src/themes/intl/zh.css +10 -0
  174. package/src/themes/intl.css +10 -0
  175. package/src/themes/monokai.css +1 -1
  176. package/src/themes/vaporwave.css +1 -1
  177. package/src/ts/extensions.ts +3 -36
  178. package/src/ts/perspective-viewer.ts +1 -1
  179. package/src/ts/plugin.ts +4 -1
  180. package/src/{rust/utils/tests/request_animation_frame.rs → ts/select-detail.ts} +32 -26
  181. package/src/ts/ts-rs/DerivedTableConfig.ts +22 -0
  182. package/src/ts/ts-rs/ExportMethod.ts +1 -1
  183. package/src/ts/ts-rs/PanelViewerConfig.ts +7 -6
  184. package/src/ts/ts-rs/RestoreOptions.ts +3 -7
  185. package/src/ts/ts-rs/TableRef.ts +9 -0
  186. package/src/ts/ts-rs/ViewerConfig.ts +7 -6
  187. package/src/ts/ts-rs/ViewerConfigUpdate.ts +7 -5
  188. package/src/ts/ts-rs/WorkspaceConfig.ts +7 -1
  189. package/src/ts/ts-rs/WorkspaceConfigUpdate.ts +7 -0
  190. package/dist/esm/perspective-viewer.inline.d.ts +0 -3
  191. package/dist/esm/perspective-viewer.inline.js +0 -21
  192. package/dist/esm/perspective-viewer.inline.js.map +0 -7
  193. package/src/rust/tasks/restore_and_render.rs +0 -107
  194. package/src/rust/utils/browser/tests/debounce.rs +0 -163
  195. package/src/rust/utils/tests/pubsub.rs +0 -95
  196. /package/dist/wasm/snippets/{perspective-viewer-ea71a3990cad35d7 → perspective-viewer-d0d525977a1acba2}/inline0.js +0 -0
  197. /package/dist/wasm/snippets/{perspective-viewer-ea71a3990cad35d7 → perspective-viewer-d0d525977a1acba2}/inline1.js +0 -0
  198. /package/dist/wasm/snippets/{perspective-viewer-ea71a3990cad35d7 → perspective-viewer-d0d525977a1acba2}/inline2.js +0 -0
  199. /package/dist/wasm/snippets/{perspective-viewer-ea71a3990cad35d7 → perspective-viewer-d0d525977a1acba2}/inline3.js +0 -0
  200. /package/dist/wasm/snippets/{perspective-viewer-ea71a3990cad35d7 → perspective-viewer-d0d525977a1acba2}/inline4.js +0 -0
  201. /package/dist/wasm/snippets/{perspective-viewer-ea71a3990cad35d7 → perspective-viewer-d0d525977a1acba2}/inline5.js +0 -0
@@ -10,15 +10,18 @@
10
10
  // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11
11
  // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
12
 
13
+ use std::rc::Rc;
14
+
13
15
  use perspective_client::clone;
14
16
  use perspective_client::utils::PerspectiveResultExt;
15
17
 
16
18
  use crate::config::*;
17
19
  use crate::presentation::Presentation;
18
20
  use crate::renderer::Renderer;
19
- use crate::session::{MissingTable, ResetOptions, Session, TableIntermediateState, probe_table};
21
+ use crate::session::{BindPlan, MissingTable, OpCtx, OpKind, Session, probe_table};
22
+ use crate::tasks::transactional_restore::{Outcome, commit_and_render, prepare};
20
23
  use crate::tasks::*;
21
- use crate::workspace::Workspace;
24
+ use crate::workspace::{PanelId, Workspace};
22
25
  use crate::*;
23
26
 
24
27
  /// How a [`restore_panel`] call reached the pipeline — the only two genuine
@@ -28,7 +31,7 @@ pub(crate) enum RestoreMode {
28
31
  Fresh,
29
32
  }
30
33
 
31
- /// Where a failed restore's error goes.
34
+ /// Where the error of a restore that COMMITTED and then failed to render goes.
32
35
  #[derive(Clone, Copy)]
33
36
  pub(crate) enum RestoreErrors {
34
37
  // Raise errors in the UI.
@@ -62,77 +65,67 @@ async fn probe(
62
65
  Ok((client, table))
63
66
  }
64
67
 
65
- /// Bind an unbound or same-named session to `name`.
66
- pub(crate) async fn bind_table_task(
67
- session: &Session,
68
- workspace: &Workspace,
69
- name: String,
70
- missing: MissingTable,
71
- ) -> ApiResult<()> {
72
- if session
73
- .get_table()
74
- .is_some_and(|t| t.get_name() == name.as_str())
75
- {
76
- return Ok(());
77
- }
68
+ /// What a restore naming `table` does to the panel's binding.
69
+ #[derive(Clone, Copy, PartialEq, Eq)]
70
+ pub(crate) enum Rebind {
71
+ /// A caller's restore: a table that REPLACES what the panel showed (or
72
+ /// recovers an errored panel) starts from a default config.
73
+ Replace,
78
74
 
79
- let (client, table) = probe(session, workspace, &name, missing).await?;
80
- session.set_client(client);
81
- match table {
82
- Some(table) => session.bind_table(table).await?,
83
- None => session.pend_table(name).await?,
84
- }
85
-
86
- session.commit_table_defaults();
87
- Ok(())
75
+ /// The table lifecycle completing a pending bind: the committed config is
76
+ /// the intent the bind exists to honor.
77
+ Complete,
88
78
  }
89
79
 
90
- /// Rebind a session to `name`, probing the incoming table before the
91
- /// outgoing binding is dropped and replaying the journal opened as
92
- /// `generation` over the incoming table's defaults.
93
- async fn rebind_table_task(
80
+ /// Decide a restore's [`BindPlan`], probing the incoming table — without
81
+ /// touching the session's own binding.
82
+ #[allow(clippy::too_many_arguments)]
83
+ async fn plan_binding(
84
+ ctx: &OpCtx,
94
85
  session: &Session,
95
86
  renderer: &Renderer,
96
87
  workspace: &Workspace,
97
- name: String,
88
+ table: &TableUpdate,
89
+ fresh: bool,
90
+ rebind: Rebind,
98
91
  missing: MissingTable,
99
- generation: u32,
100
- ) -> ApiResult<()> {
101
- let probed = probe(session, workspace, &name, missing).await;
102
- let journal = session.take_pending_load(generation);
103
- let (client, table) = probed?;
104
- let Some(journal) = journal else {
105
- return Ok(());
92
+ ) -> ApiResult<Option<BindPlan>> {
93
+ let OptionalUpdate::Update(table) = table else {
94
+ return Ok(Some(BindPlan::Keep));
106
95
  };
107
96
 
108
- session
109
- .reset(ResetOptions {
110
- config: true,
111
- expressions: true,
112
- stats: true,
113
- table: Some(TableIntermediateState::Reloaded),
114
- })
115
- .await?;
116
-
117
- session.set_client(client);
118
- match table {
119
- Some(table) => session.bind_table(table).await?,
120
- None => session.pend_table(name).await?,
97
+ let name = table.name()?;
98
+ let same = session.get_table().is_some_and(|t| t.get_name() == name);
99
+
100
+ if same && !session.is_errored() && renderer.failure().is_none() {
101
+ return Ok(Some(BindPlan::Keep));
121
102
  }
122
103
 
123
- session.commit_table_defaults();
124
- for delta in journal {
125
- session.commit_view_config(delta)?;
104
+ let (client, table) = probe(session, workspace, name, missing).await?;
105
+ if !fresh && ctx.is_superseded() {
106
+ return Ok(None);
126
107
  }
127
108
 
128
- session.commit_table_defaults();
129
- if session.get_table().is_none()
130
- && let Some(plugin) = renderer.active_plugin()
109
+ if table.is_none()
110
+ && rebind == Rebind::Complete
111
+ && session.pending_table().as_deref() == Some(name)
131
112
  {
132
- plugin.clear().await?;
113
+ return Ok(None);
133
114
  }
134
115
 
135
- Ok(())
116
+ let reset = !fresh && rebind == Rebind::Replace;
117
+ Ok(Some(match table {
118
+ Some(table) => BindPlan::Bind {
119
+ client,
120
+ table: Box::new(table),
121
+ reset,
122
+ },
123
+ None => BindPlan::Pend {
124
+ client,
125
+ name: name.to_owned(),
126
+ reset,
127
+ },
128
+ }))
136
129
  }
137
130
 
138
131
  /// Apply a [`ViewerConfigUpdate`] to a single panel and re-draw — the one
@@ -145,111 +138,130 @@ pub(crate) async fn restore_panel(
145
138
  presentation: &Presentation,
146
139
  workspace: &Workspace,
147
140
  mode: RestoreMode,
141
+ update: ViewerConfigUpdate,
142
+ errors: RestoreErrors,
143
+ missing: MissingTable,
144
+ preamble: Option<futures::future::LocalBoxFuture<'static, ApiResult<()>>>,
145
+ ) -> ApiResult<()> {
146
+ let update = Rc::new(update);
147
+ let kind = OpKind::Restore {
148
+ update: (!matches!(mode, RestoreMode::Fresh)).then(|| update.clone()),
149
+ };
150
+
151
+ let ticket = session.submit(kind, {
152
+ clone!(session, renderer, presentation, workspace);
153
+ move |ctx| {
154
+ Box::pin(async move {
155
+ if let Some(preamble) = preamble {
156
+ preamble.await?;
157
+ }
158
+
159
+ restore_panel_step(
160
+ &ctx,
161
+ &session,
162
+ &renderer,
163
+ &presentation,
164
+ &workspace,
165
+ mode,
166
+ Rebind::Replace,
167
+ RunOrigin::Public,
168
+ Rc::unwrap_or_clone(update),
169
+ errors,
170
+ missing,
171
+ )
172
+ .await?;
173
+
174
+ Ok(None)
175
+ })
176
+ }
177
+ });
178
+
179
+ ticket.settle().await
180
+ }
181
+
182
+ /// The body of [`restore_panel`], to be called only from a running op's step.
183
+ #[allow(clippy::too_many_arguments)]
184
+ pub(crate) async fn restore_panel_step(
185
+ ctx: &OpCtx,
186
+ session: &Session,
187
+ renderer: &Renderer,
188
+ presentation: &Presentation,
189
+ workspace: &Workspace,
190
+ mode: RestoreMode,
191
+ rebind: Rebind,
192
+ origin: RunOrigin,
148
193
  mut update: ViewerConfigUpdate,
149
194
  errors: RestoreErrors,
150
195
  missing: MissingTable,
151
196
  ) -> ApiResult<()> {
152
197
  let active = matches!(mode, RestoreMode::Existing { active: true });
153
198
  let fresh = matches!(mode, RestoreMode::Fresh);
154
- match &update.theme {
155
- OptionalUpdate::Update(theme) => renderer.set_theme_stamped(Some(theme.clone())),
156
- // `SetDefault` resolves to a CONCRETE registry default here, rather
157
- // than clearing the panel's theme — nothing downstream re-resolves.
158
- OptionalUpdate::SetDefault => {
159
- renderer.set_theme_stamped(presentation.get_default_theme_name().await)
199
+
200
+ crate::tasks::resolve_table_ref(workspace, &mut update, missing).await?;
201
+
202
+ // A derived definition parked in the pending registry is a known-future
203
+ // table, never an unknown name — bind as PEND whatever the caller's
204
+ // missing-table mode.
205
+ let missing = match &update.table {
206
+ OptionalUpdate::Update(table)
207
+ if table
208
+ .name()
209
+ .is_ok_and(|name| workspace.is_derived_pending(name)) =>
210
+ {
211
+ MissingTable::Pend
160
212
  },
161
- OptionalUpdate::Missing => {},
162
- }
213
+ _ => missing,
214
+ };
163
215
 
164
- if !active {
165
- update.theme = OptionalUpdate::Missing;
166
- }
216
+ renderer.check_plugin_update(&update.plugin)?;
217
+ let Some(plan) = plan_binding(
218
+ ctx,
219
+ session,
220
+ renderer,
221
+ workspace,
222
+ &update.table,
223
+ fresh,
224
+ rebind,
225
+ missing,
226
+ )
227
+ .await?
228
+ else {
229
+ return Ok(());
230
+ };
167
231
 
168
232
  if !fresh {
169
233
  tracing::info!("Restoring {update}");
170
234
  }
171
235
 
172
- // NOTE: `update.settings` is deliberately NOT applied here. It is
173
- // element-level chrome rather than panel state, so `restore()` — the
174
- // only caller that can carry it — applies it before dispatching, and
175
- // this pipeline stays per-panel. Applying it here reached only the
176
- // `Existing { active: true }` mode, which is why a freshly created
177
- // panel silently ignored it.
178
-
179
- // Under `Suppress` the restore is TRANSACTIONAL, so snapshot the config
180
- // it is about to overwrite — see the failure tail below for why.
181
- let rollback =
182
- matches!(errors, RestoreErrors::Suppress).then(|| session.get_view_config().clone());
183
-
184
- let binding_before = (
185
- session.get_table().map(|t| t.get_name().to_owned()),
186
- session.pending_table(),
187
- );
188
-
189
- let generation = match &update.table {
190
- OptionalUpdate::Update(name)
191
- if session.is_errored()
192
- || (!fresh
193
- && session
194
- .get_table()
195
- .map(|t| t.get_name() != name.as_str())
196
- .unwrap_or(true)) =>
197
- {
198
- Some(session.begin_pending_load())
199
- },
200
- _ => None,
201
- };
236
+ let overlay = renderer
237
+ .slot_name()
238
+ .map(|id| overlay_for(workspace, &PanelId::from(id)));
202
239
 
203
- let result = restore_and_render(
240
+ let rebound = !matches!(plan, BindPlan::Keep);
241
+ let prepared = prepare(
204
242
  session,
205
243
  renderer,
206
244
  presentation,
207
- RunOrigin::Public,
208
- update.clone(),
209
- {
210
- clone!(session, renderer, update.table, workspace);
211
- async move {
212
- let OptionalUpdate::Update(name) = table else {
213
- return Ok(());
214
- };
215
-
216
- match generation {
217
- Some(generation) => {
218
- rebind_table_task(
219
- &session, &renderer, &workspace, name, missing, generation,
220
- )
221
- .await
222
- },
223
- None => bind_table_task(&session, &workspace, name, missing).await,
224
- }
225
- }
226
- },
245
+ active,
246
+ plan,
247
+ overlay,
248
+ update,
227
249
  )
228
- .await;
229
-
230
- if let Some(generation) = generation {
231
- session.take_pending_load(generation);
232
- }
250
+ .await?;
251
+ let Outcome { committed, result } =
252
+ commit_and_render(session, renderer, presentation, origin, prepared).await;
233
253
 
234
- if let Err(e) = &result {
235
- match errors {
236
- RestoreErrors::Publish => session.set_error(false, e.clone()).await?,
237
- RestoreErrors::Suppress => {
238
- let binding_after = (
239
- session.get_table().map(|t| t.get_name().to_owned()),
240
- session.pending_table(),
241
- );
242
-
243
- if let Some(config) = rollback
244
- && binding_after == binding_before
245
- {
246
- session.commit_view_config(config.into()).unwrap_or_log();
247
- }
248
- },
249
- }
254
+ if let Err(e) = &result
255
+ && committed
256
+ && matches!(errors, RestoreErrors::Publish)
257
+ {
258
+ let _ = renderer.fail(e.clone());
250
259
  }
251
260
 
252
261
  result?;
262
+ if rebound {
263
+ workspace.bindings_changed().emit(());
264
+ }
253
265
 
254
266
  if fresh {
255
267
  renderer.resize().await.unwrap_or_log();
@@ -10,12 +10,13 @@
10
10
  // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11
11
  // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
12
 
13
+ use futures::FutureExt;
13
14
  use perspective_client::clone;
14
15
  use perspective_js::utils::*;
15
16
 
16
17
  use crate::config::ColumnConfigFieldUpdate;
17
- use crate::renderer::Renderer;
18
- use crate::session::Session;
18
+ use crate::renderer::{Renderer, StagedEdit};
19
+ use crate::session::{EditDelta, OpKind, Session};
19
20
 
20
21
  /// Apply a [`ColumnConfigFieldUpdate`] from a column-style sidebar
21
22
  /// control to the active plugin's per-column config bucket on
@@ -30,31 +31,57 @@ pub fn send_column_config(
30
31
  column_name: &str,
31
32
  update: ColumnConfigFieldUpdate,
32
33
  ) {
33
- // Apply the renderer write synchronously so the StyleTab's
34
- // revision-bump re-render path sees the new state immediately.
35
- let view_config = session.get_view_config().clone();
36
- renderer.update_columns_config_field(&view_config, session, column_name.to_string(), update);
37
-
38
- clone!(session, renderer);
39
- ApiFuture::spawn(async move {
40
- let view_config_snapshot = session.get_view_config().clone();
41
- let columns_configs = renderer
42
- .all_columns_configs_materialized(&view_config_snapshot, &session)
43
- .await;
44
- let plugin_token =
45
- wasm_bindgen::JsValue::from_serde_ext(&renderer.get_plugin_config()).unwrap();
46
- renderer
47
- .ensure_plugin_selected()?
48
- .restore(&plugin_token, Some(&columns_configs))?;
49
-
50
- clone!(session);
51
- renderer
52
- .update_lazy(async move { Ok(session.get_view_with_dimensions()) })
53
- .await?;
54
- renderer
55
- .columns_config_changed
56
- .emit(columns_configs.clone());
57
- renderer.column_style_changed.emit(columns_configs);
58
- Ok(())
59
- })
34
+ let column = column_name.to_string();
35
+ let staged = renderer.stage(StagedEdit::ColumnField {
36
+ column: column.clone(),
37
+ update: update.clone(),
38
+ });
39
+
40
+ let kind = OpKind::Edit {
41
+ delta: EditDelta::Renderer,
42
+ swaps_plugin: false,
43
+ };
44
+
45
+ let ticket = session.submit(kind, {
46
+ clone!(session, renderer);
47
+ move |_ctx| {
48
+ Box::pin(async move {
49
+ let view_config = session.committed_view_config().clone();
50
+ renderer.update_columns_config_field(&view_config, &session, column, update);
51
+ drop(staged);
52
+ Ok(Some(
53
+ async move {
54
+ let view_config_snapshot = session.committed_view_config().clone();
55
+ let columns_configs = renderer
56
+ .all_columns_configs_materialized(&view_config_snapshot, &session)
57
+ .await;
58
+
59
+ let plugin_token = wasm_bindgen::JsValue::from_serde_ext(
60
+ &renderer.committed_plugin_config(),
61
+ )
62
+ .unwrap();
63
+
64
+ renderer
65
+ .ensure_plugin_selected()?
66
+ .restore(&plugin_token, Some(&columns_configs))?;
67
+
68
+ clone!(session);
69
+ renderer
70
+ .update_lazy(async move { Ok(session.get_view_with_dimensions()) })
71
+ .await?;
72
+
73
+ renderer
74
+ .columns_config_changed
75
+ .emit(columns_configs.clone());
76
+
77
+ renderer.column_style_changed.emit(columns_configs);
78
+ Ok(())
79
+ }
80
+ .boxed_local(),
81
+ ))
82
+ })
83
+ }
84
+ });
85
+
86
+ ApiFuture::spawn(ticket.settle());
60
87
  }
@@ -10,12 +10,13 @@
10
10
  // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11
11
  // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
12
 
13
+ use futures::FutureExt;
13
14
  use perspective_client::clone;
14
15
  use perspective_js::utils::*;
15
16
 
16
17
  use crate::config::ColumnConfigFieldUpdate;
17
- use crate::renderer::Renderer;
18
- use crate::session::Session;
18
+ use crate::renderer::{Renderer, StagedEdit};
19
+ use crate::session::{EditDelta, OpKind, Session};
19
20
 
20
21
  /// Apply a [`ColumnConfigFieldUpdate`] from the Plugin-settings tab to
21
22
  /// the active plugin's bucket on [`Renderer`], then re-`restore` the
@@ -28,27 +29,55 @@ use crate::session::Session;
28
29
  ///
29
30
  /// Column-style updates go through [`super::send_column_config`].
30
31
  pub fn send_plugin_config(session: &Session, renderer: &Renderer, update: ColumnConfigFieldUpdate) {
31
- let view_config = session.get_view_config().clone();
32
- let changed = renderer.update_plugin_config_field(&view_config, update);
33
- clone!(session, renderer);
34
- ApiFuture::spawn(async move {
35
- if changed {
36
- let plugin_config = renderer.get_plugin_config();
37
- let plugin_token = wasm_bindgen::JsValue::from_serde_ext(&plugin_config).unwrap();
38
- let view_config_snapshot = session.get_view_config().clone();
39
- let columns_configs = renderer
40
- .all_columns_configs_materialized(&view_config_snapshot, &session)
41
- .await;
42
- renderer
43
- .ensure_plugin_selected()?
44
- .restore(&plugin_token, Some(&columns_configs))?;
45
- clone!(session);
46
- renderer
47
- .update_lazy(async move { Ok(session.get_view_with_dimensions()) })
48
- .await?;
49
- renderer.plugin_config_changed.emit(plugin_config);
32
+ let staged = renderer.stage(StagedEdit::PluginField(update.clone()));
33
+ let kind = OpKind::Edit {
34
+ delta: EditDelta::Renderer,
35
+ swaps_plugin: false,
36
+ };
37
+
38
+ let ticket = session.submit(kind, {
39
+ clone!(session, renderer);
40
+ move |_ctx| {
41
+ Box::pin(async move {
42
+ let view_config = session.committed_view_config().clone();
43
+ let changed = renderer.update_plugin_config_field(&view_config, update);
44
+ drop(staged);
45
+ Ok(Some(
46
+ async move {
47
+ if changed {
48
+ deliver_plugin_config(&session, &renderer).await?;
49
+ }
50
+
51
+ Ok(())
52
+ }
53
+ .boxed_local(),
54
+ ))
55
+ })
50
56
  }
57
+ });
58
+
59
+ ApiFuture::spawn(ticket.settle());
60
+ }
61
+
62
+ /// Re-`restore` the active plugin with its committed buckets, repaint, and
63
+ /// announce the plugin-level change.
64
+ pub(super) async fn deliver_plugin_config(session: &Session, renderer: &Renderer) -> ApiResult<()> {
65
+ let plugin_config = renderer.committed_plugin_config();
66
+ let plugin_token = wasm_bindgen::JsValue::from_serde_ext(&plugin_config).unwrap();
67
+ let view_config_snapshot = session.committed_view_config().clone();
68
+ let columns_configs = renderer
69
+ .all_columns_configs_materialized(&view_config_snapshot, session)
70
+ .await;
71
+
72
+ renderer
73
+ .ensure_plugin_selected()?
74
+ .restore(&plugin_token, Some(&columns_configs))?;
75
+
76
+ clone!(session);
77
+ renderer
78
+ .update_lazy(async move { Ok(session.get_view_with_dimensions()) })
79
+ .await?;
51
80
 
52
- Ok(())
53
- })
81
+ renderer.plugin_config_changed.emit(plugin_config);
82
+ Ok(())
54
83
  }
@@ -10,12 +10,13 @@
10
10
  // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11
11
  // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
12
 
13
+ use futures::FutureExt;
13
14
  use perspective_client::clone;
14
15
  use perspective_js::utils::*;
15
16
 
16
17
  use crate::config::*;
17
- use crate::renderer::Renderer;
18
- use crate::session::Session;
18
+ use crate::renderer::{Renderer, StagedEdit};
19
+ use crate::session::{EditDelta, OpKind, Session};
19
20
 
20
21
  /// Set the active plugin's `edit_mode`, persisting it in the [`Renderer`]'s
21
22
  /// plugin bucket and re-`restore`+rendering (the same merged-token path as
@@ -27,35 +28,42 @@ use crate::session::Session;
27
28
  /// (e.g. charts) schema-gate the key out in [`Renderer::update_plugin_config`],
28
29
  /// making this a no-op for them.
29
30
  pub fn set_edit_mode(session: &Session, renderer: &Renderer, mode: &str) {
30
- let view_config = session.get_view_config().clone();
31
31
  let mut map = serde_json::Map::new();
32
32
  map.insert(
33
33
  "edit_mode".to_owned(),
34
34
  serde_json::Value::String(mode.to_owned()),
35
35
  );
36
36
 
37
- let changed = renderer
38
- .update_plugin_config(&view_config, OptionalUpdate::Update(map))
39
- .unwrap_or_default();
40
- clone!(session, renderer);
41
- ApiFuture::spawn(async move {
42
- if changed {
43
- let plugin_config = renderer.get_plugin_config();
44
- let plugin_token = wasm_bindgen::JsValue::from_serde_ext(&plugin_config).unwrap();
45
- let view_config_snapshot = session.get_view_config().clone();
46
- let columns_configs = renderer
47
- .all_columns_configs_materialized(&view_config_snapshot, &session)
48
- .await;
49
- renderer
50
- .ensure_plugin_selected()?
51
- .restore(&plugin_token, Some(&columns_configs))?;
52
- clone!(session);
53
- renderer
54
- .update_lazy(async move { Ok(session.get_view_with_dimensions()) })
55
- .await?;
56
- renderer.plugin_config_changed.emit(plugin_config);
37
+ let staged = renderer.stage(StagedEdit::PluginConfig(map.clone()));
38
+ let kind = OpKind::Edit {
39
+ delta: EditDelta::Renderer,
40
+ swaps_plugin: false,
41
+ };
42
+
43
+ let ticket = session.submit(kind, {
44
+ clone!(session, renderer);
45
+ move |_ctx| {
46
+ Box::pin(async move {
47
+ let view_config = session.committed_view_config().clone();
48
+ let changed = renderer
49
+ .update_plugin_config(&view_config, OptionalUpdate::Update(map))
50
+ .unwrap_or_default();
51
+
52
+ drop(staged);
53
+ Ok(Some(
54
+ async move {
55
+ if changed {
56
+ super::send_plugin_config::deliver_plugin_config(&session, &renderer)
57
+ .await?;
58
+ }
59
+
60
+ Ok(())
61
+ }
62
+ .boxed_local(),
63
+ ))
64
+ })
57
65
  }
66
+ });
58
67
 
59
- Ok(())
60
- })
68
+ ApiFuture::spawn(ticket.settle());
61
69
  }