@perspective-dev/viewer 5.5.1 → 5.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (194) hide show
  1. package/README.md +82 -3
  2. package/dist/cdn/perspective-viewer.js +9 -9
  3. package/dist/cdn/perspective-viewer.js.map +3 -3
  4. package/dist/css/blueprint.css +1 -1
  5. package/dist/css/botanical.css +1 -1
  6. package/dist/css/dracula.css +1 -1
  7. package/dist/css/eggplant.css +1 -1
  8. package/dist/css/gruvbox-dark.css +1 -1
  9. package/dist/css/gruvbox.css +1 -1
  10. package/dist/css/icons.css +1 -1
  11. package/dist/css/intl/ar.css +1 -1
  12. package/dist/css/intl/de.css +1 -1
  13. package/dist/css/intl/es.css +1 -1
  14. package/dist/css/intl/fr.css +1 -1
  15. package/dist/css/intl/ja.css +1 -1
  16. package/dist/css/intl/pt.css +1 -1
  17. package/dist/css/intl/zh.css +1 -1
  18. package/dist/css/intl.css +1 -1
  19. package/dist/css/ledger.css +1 -1
  20. package/dist/css/monokai.css +1 -1
  21. package/dist/css/nord.css +1 -1
  22. package/dist/css/phosphor.css +1 -1
  23. package/dist/css/pro-dark.css +1 -1
  24. package/dist/css/pro.css +1 -1
  25. package/dist/css/solarized-dark.css +1 -1
  26. package/dist/css/solarized.css +1 -1
  27. package/dist/css/themes.css +1 -1
  28. package/dist/css/vaporwave.css +1 -1
  29. package/dist/css/velvet.css +1 -1
  30. package/dist/docs/perspective-docs.json +1 -1
  31. package/dist/esm/column-format.js +2 -0
  32. package/dist/esm/column-format.js.map +7 -0
  33. package/dist/esm/extensions.d.ts +3 -13
  34. package/dist/esm/perspective-viewer.d.ts +1 -1
  35. package/dist/esm/perspective-viewer.js +9 -9
  36. package/dist/esm/perspective-viewer.js.map +3 -3
  37. package/dist/esm/plugin.d.ts +4 -1
  38. package/dist/esm/select-detail.d.ts +15 -0
  39. package/dist/esm/select-detail.js +2 -0
  40. package/dist/esm/select-detail.js.map +7 -0
  41. package/dist/esm/ts-rs/DerivedTableConfig.d.ts +35 -0
  42. package/dist/esm/ts-rs/PanelViewerConfig.d.ts +7 -6
  43. package/dist/esm/ts-rs/RestoreOptions.d.ts +3 -7
  44. package/dist/esm/ts-rs/TableRef.d.ts +9 -0
  45. package/dist/esm/ts-rs/ViewerConfig.d.ts +7 -6
  46. package/dist/esm/ts-rs/ViewerConfigUpdate.d.ts +7 -5
  47. package/dist/esm/ts-rs/WorkspaceConfig.d.ts +9 -1
  48. package/dist/esm/ts-rs/WorkspaceConfigUpdate.d.ts +9 -0
  49. package/dist/wasm/perspective-viewer.d.ts +115 -35
  50. package/dist/wasm/perspective-viewer.js +314 -190
  51. package/dist/wasm/perspective-viewer.wasm +0 -0
  52. package/dist/wasm/perspective-viewer.wasm.d.ts +21 -17
  53. package/package.json +31 -18
  54. package/src/css/column-selector.css +205 -50
  55. package/src/css/ui/containers/context-menu.css +12 -1
  56. package/src/css/ui/containers/dropdown-menu-items.css +29 -0
  57. package/src/css/ui/containers/scroll-panel.css +4 -1
  58. package/src/rust/agent/client/protocol.rs +0 -89
  59. package/src/rust/agent/markdown/mod.rs +0 -238
  60. package/src/rust/agent/preamble.md +1 -0
  61. package/src/rust/agent/tools.rs +2 -16
  62. package/src/rust/components/column_selector/active_column.rs +15 -5
  63. package/src/rust/components/column_selector/add_expression_button.rs +9 -26
  64. package/src/rust/components/column_selector/config_selector.rs +34 -6
  65. package/src/rust/components/column_selector/derived_control.rs +229 -0
  66. package/src/rust/components/column_selector/expr_edit_button.rs +3 -2
  67. package/src/rust/components/column_selector/filter_column.rs +0 -2
  68. package/src/rust/components/column_selector/inactive_column.rs +4 -5
  69. package/src/rust/components/column_selector/pivot_column.rs +21 -1
  70. package/src/rust/components/column_selector/sort_column.rs +4 -3
  71. package/src/rust/components/column_selector.rs +21 -1
  72. package/src/rust/components/column_settings_sidebar/save_settings.rs +5 -3
  73. package/src/rust/components/column_settings_sidebar/style_tab.rs +1 -0
  74. package/src/rust/components/column_settings_sidebar.rs +25 -19
  75. package/src/rust/components/debug_panel.rs +38 -29
  76. package/src/rust/components/dragdrop_list.rs +8 -9
  77. package/src/rust/components/main_panel/msg.rs +6 -4
  78. package/src/rust/components/main_panel/reconcile.rs +6 -1
  79. package/src/rust/components/main_panel/render.rs +70 -51
  80. package/src/rust/components/main_panel/update.rs +39 -1
  81. package/src/rust/components/main_panel.rs +97 -59
  82. package/src/rust/components/new_panel_menu.rs +69 -1
  83. package/src/rust/components/panel_menu.rs +63 -12
  84. package/src/rust/components/plugin_tab.rs +3 -2
  85. package/src/rust/components/render_warning.rs +2 -1
  86. package/src/rust/components/settings_panel.rs +4 -4
  87. package/src/rust/components/status_bar.rs +74 -24
  88. package/src/rust/components/status_indicator.rs +8 -1
  89. package/src/rust/components/viewer/filters.rs +13 -1
  90. package/src/rust/components/viewer/msg.rs +4 -0
  91. package/src/rust/components/viewer/panels.rs +41 -1
  92. package/src/rust/components/viewer/render.rs +1 -0
  93. package/src/rust/components/viewer/settings.rs +0 -22
  94. package/src/rust/components/viewer/snapshots.rs +2 -2
  95. package/src/rust/components/viewer/wiring.rs +14 -0
  96. package/src/rust/components/viewer.rs +45 -25
  97. package/src/rust/components/window_editor.rs +5 -4
  98. package/src/rust/config/column_config_schema.rs +38 -377
  99. package/src/rust/config/number_string_format.rs +0 -94
  100. package/src/rust/config/options.rs +3 -7
  101. package/src/rust/config/plugin_static_config.rs +0 -42
  102. package/src/rust/config/viewer_config.rs +47 -17
  103. package/src/rust/config/workspace_config.rs +158 -128
  104. package/src/rust/custom_elements/viewer.rs +187 -131
  105. package/src/rust/custom_events.rs +33 -18
  106. package/src/rust/derived.rs +266 -0
  107. package/src/rust/exprtk/tokenize.rs +0 -54
  108. package/src/rust/js/plugin.rs +1 -1
  109. package/src/rust/lib.rs +3 -0
  110. package/src/rust/presentation.rs +23 -4
  111. package/src/rust/queries/column_locator.rs +55 -48
  112. package/src/rust/queries/get_viewer_config.rs +1 -1
  113. package/src/rust/queries/plugin_column_styles.rs +22 -23
  114. package/src/rust/renderer/dispatch.rs +2 -0
  115. package/src/rust/renderer/plugin_config.rs +652 -487
  116. package/src/rust/renderer/props.rs +4 -0
  117. package/src/rust/renderer/registry.rs +2 -6
  118. package/src/rust/renderer/state.rs +205 -0
  119. package/src/rust/renderer.rs +201 -59
  120. package/src/rust/session/metadata.rs +30 -9
  121. package/src/rust/session/op_queue.rs +430 -0
  122. package/src/rust/session/panel_state.rs +327 -0
  123. package/src/rust/session/view_subscription.rs +42 -3
  124. package/src/rust/session.rs +900 -395
  125. package/src/rust/tasks/apply_global_filters.rs +49 -35
  126. package/src/rust/tasks/auto_pause.rs +22 -12
  127. package/src/rust/tasks/copy_export.rs +5 -0
  128. package/src/rust/tasks/create_panel.rs +34 -9
  129. package/src/rust/tasks/derive.rs +312 -0
  130. package/src/rust/tasks/mod.rs +6 -2
  131. package/src/rust/tasks/pipeline.rs +145 -89
  132. package/src/rust/tasks/reset_all.rs +31 -20
  133. package/src/rust/tasks/restore_derived.rs +200 -0
  134. package/src/rust/tasks/restore_panel.rs +153 -141
  135. package/src/rust/tasks/send_column_config.rs +56 -29
  136. package/src/rust/tasks/send_plugin_config.rs +52 -23
  137. package/src/rust/tasks/set_edit_mode.rs +33 -25
  138. package/src/rust/tasks/sync_update_panels.rs +39 -12
  139. package/src/rust/tasks/table_lifecycle.rs +70 -34
  140. package/src/rust/tasks/transactional_restore.rs +387 -0
  141. package/src/rust/tasks/update_theme.rs +45 -6
  142. package/src/rust/ui/containers/scroll_panel.rs +17 -10
  143. package/src/rust/ui/containers/split_panel.rs +0 -41
  144. package/src/rust/ui/context_menu.rs +157 -18
  145. package/src/rust/ui/font_loader.rs +23 -35
  146. package/src/rust/ui/form/mirrored_textarea.rs +0 -12
  147. package/src/rust/ui/form/multi_stop_gradient_selector.rs +0 -89
  148. package/src/rust/ui/form/palette_selector.rs +0 -51
  149. package/src/rust/ui/portal.rs +4 -10
  150. package/src/rust/utils/browser/mod.rs +2 -3
  151. package/src/{ts/perspective-viewer.inline.ts → rust/utils/browser/mouse.rs} +11 -10
  152. package/src/rust/utils/browser/request_animation_frame.rs +27 -8
  153. package/src/rust/utils/css.rs +0 -360
  154. package/src/rust/utils/local_fonts.rs +0 -36
  155. package/src/rust/utils/mod.rs +2 -3
  156. package/src/rust/utils/modal_position.rs +15 -21
  157. package/src/rust/utils/{browser/tests/mod.rs → plugin_host_state.rs} +31 -1
  158. package/src/rust/utils/pubsub.rs +0 -1
  159. package/src/rust/workspace.rs +187 -140
  160. package/src/svg/derived.svg +6 -0
  161. package/src/themes/icons.css +1 -0
  162. package/src/themes/intl/ar.css +10 -0
  163. package/src/themes/intl/de.css +10 -0
  164. package/src/themes/intl/es.css +10 -0
  165. package/src/themes/intl/fr.css +10 -0
  166. package/src/themes/intl/ja.css +10 -0
  167. package/src/themes/intl/pt.css +10 -0
  168. package/src/themes/intl/zh.css +10 -0
  169. package/src/themes/intl.css +10 -0
  170. package/src/ts/extensions.ts +3 -36
  171. package/src/ts/perspective-viewer.ts +1 -1
  172. package/src/ts/plugin.ts +4 -1
  173. package/src/{rust/utils/tests/request_animation_frame.rs → ts/select-detail.ts} +32 -26
  174. package/src/ts/ts-rs/DerivedTableConfig.ts +22 -0
  175. package/src/ts/ts-rs/PanelViewerConfig.ts +7 -6
  176. package/src/ts/ts-rs/RestoreOptions.ts +3 -7
  177. package/src/ts/ts-rs/TableRef.ts +9 -0
  178. package/src/ts/ts-rs/ViewerConfig.ts +7 -6
  179. package/src/ts/ts-rs/ViewerConfigUpdate.ts +7 -5
  180. package/src/ts/ts-rs/WorkspaceConfig.ts +7 -1
  181. package/src/ts/ts-rs/WorkspaceConfigUpdate.ts +7 -0
  182. package/dist/esm/perspective-viewer.inline.d.ts +0 -3
  183. package/dist/esm/perspective-viewer.inline.js +0 -21
  184. package/dist/esm/perspective-viewer.inline.js.map +0 -7
  185. package/src/rust/tasks/restore_and_render.rs +0 -107
  186. package/src/rust/utils/browser/tests/debounce.rs +0 -163
  187. package/src/rust/utils/tests/mod.rs +0 -14
  188. package/src/rust/utils/tests/pubsub.rs +0 -95
  189. /package/dist/wasm/snippets/{perspective-viewer-6813b568219c6763 → perspective-viewer-d0d525977a1acba2}/inline0.js +0 -0
  190. /package/dist/wasm/snippets/{perspective-viewer-6813b568219c6763 → perspective-viewer-d0d525977a1acba2}/inline1.js +0 -0
  191. /package/dist/wasm/snippets/{perspective-viewer-6813b568219c6763 → perspective-viewer-d0d525977a1acba2}/inline2.js +0 -0
  192. /package/dist/wasm/snippets/{perspective-viewer-6813b568219c6763 → perspective-viewer-d0d525977a1acba2}/inline3.js +0 -0
  193. /package/dist/wasm/snippets/{perspective-viewer-6813b568219c6763 → perspective-viewer-d0d525977a1acba2}/inline4.js +0 -0
  194. /package/dist/wasm/snippets/{perspective-viewer-6813b568219c6763 → perspective-viewer-d0d525977a1acba2}/inline5.js +0 -0
@@ -10,17 +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::{
20
- LoadGuard, MissingTable, ResetOptions, Session, TableIntermediateState, probe_table,
21
- };
21
+ use crate::session::{BindPlan, MissingTable, OpCtx, OpKind, Session, probe_table};
22
+ use crate::tasks::transactional_restore::{Outcome, commit_and_render, prepare};
22
23
  use crate::tasks::*;
23
- use crate::workspace::Workspace;
24
+ use crate::workspace::{PanelId, Workspace};
24
25
  use crate::*;
25
26
 
26
27
  /// How a [`restore_panel`] call reached the pipeline — the only two genuine
@@ -30,7 +31,7 @@ pub(crate) enum RestoreMode {
30
31
  Fresh,
31
32
  }
32
33
 
33
- /// Where a failed restore's error goes.
34
+ /// Where the error of a restore that COMMITTED and then failed to render goes.
34
35
  #[derive(Clone, Copy)]
35
36
  pub(crate) enum RestoreErrors {
36
37
  // Raise errors in the UI.
@@ -64,77 +65,67 @@ async fn probe(
64
65
  Ok((client, table))
65
66
  }
66
67
 
67
- /// Bind an unbound or same-named session to `name`.
68
- pub(crate) async fn bind_table_task(
69
- session: &Session,
70
- workspace: &Workspace,
71
- name: String,
72
- missing: MissingTable,
73
- ) -> ApiResult<()> {
74
- if session
75
- .get_table()
76
- .is_some_and(|t| t.get_name() == name.as_str())
77
- {
78
- return Ok(());
79
- }
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,
80
74
 
81
- let (client, table) = probe(session, workspace, &name, missing).await?;
82
- session.set_client(client);
83
- match table {
84
- Some(table) => session.bind_table(table).await?,
85
- None => session.pend_table(name).await?,
86
- }
87
-
88
- session.commit_table_defaults();
89
- Ok(())
75
+ /// The table lifecycle completing a pending bind: the committed config is
76
+ /// the intent the bind exists to honor.
77
+ Complete,
90
78
  }
91
79
 
92
- /// Rebind a session to `name`, probing the incoming table before the
93
- /// outgoing binding is dropped and replaying `load`'s journal over the
94
- /// incoming table's defaults.
95
- 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,
96
85
  session: &Session,
97
86
  renderer: &Renderer,
98
87
  workspace: &Workspace,
99
- name: String,
88
+ table: &TableUpdate,
89
+ fresh: bool,
90
+ rebind: Rebind,
100
91
  missing: MissingTable,
101
- load: &LoadGuard,
102
- ) -> ApiResult<()> {
103
- let probed = probe(session, workspace, &name, missing).await;
104
- let journal = load.claim();
105
- let (client, table) = probed?;
106
- let Some(journal) = journal else {
107
- return Ok(());
92
+ ) -> ApiResult<Option<BindPlan>> {
93
+ let OptionalUpdate::Update(table) = table else {
94
+ return Ok(Some(BindPlan::Keep));
108
95
  };
109
96
 
110
- session
111
- .reset(ResetOptions {
112
- config: true,
113
- expressions: true,
114
- stats: true,
115
- table: Some(TableIntermediateState::Reloaded),
116
- })
117
- .await?;
118
-
119
- session.set_client(client);
120
- match table {
121
- Some(table) => session.bind_table(table).await?,
122
- 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));
123
102
  }
124
103
 
125
- session.commit_table_defaults();
126
- for delta in journal {
127
- 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);
128
107
  }
129
108
 
130
- session.commit_table_defaults();
131
- if session.get_table().is_none()
132
- && let Some(plugin) = renderer.active_plugin()
109
+ if table.is_none()
110
+ && rebind == Rebind::Complete
111
+ && session.pending_table().as_deref() == Some(name)
133
112
  {
134
- plugin.clear().await?;
113
+ return Ok(None);
135
114
  }
136
115
 
137
- 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
+ }))
138
129
  }
139
130
 
140
131
  /// Apply a [`ViewerConfigUpdate`] to a single panel and re-draw — the one
@@ -147,109 +138,130 @@ pub(crate) async fn restore_panel(
147
138
  presentation: &Presentation,
148
139
  workspace: &Workspace,
149
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,
150
193
  mut update: ViewerConfigUpdate,
151
194
  errors: RestoreErrors,
152
195
  missing: MissingTable,
153
196
  ) -> ApiResult<()> {
154
197
  let active = matches!(mode, RestoreMode::Existing { active: true });
155
198
  let fresh = matches!(mode, RestoreMode::Fresh);
156
- match &update.theme {
157
- OptionalUpdate::Update(theme) => renderer.set_theme_stamped(Some(theme.clone())),
158
- // `SetDefault` resolves to a CONCRETE registry default here, rather
159
- // than clearing the panel's theme — nothing downstream re-resolves.
160
- OptionalUpdate::SetDefault => {
161
- 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
162
212
  },
163
- OptionalUpdate::Missing => {},
164
- }
213
+ _ => missing,
214
+ };
165
215
 
166
- if !active {
167
- update.theme = OptionalUpdate::Missing;
168
- }
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
+ };
169
231
 
170
232
  if !fresh {
171
233
  tracing::info!("Restoring {update}");
172
234
  }
173
235
 
174
- // NOTE: `update.settings` is deliberately NOT applied here. It is
175
- // element-level chrome rather than panel state, so `restore()` — the
176
- // only caller that can carry it — applies it before dispatching, and
177
- // this pipeline stays per-panel. Applying it here reached only the
178
- // `Existing { active: true }` mode, which is why a freshly created
179
- // panel silently ignored it.
180
-
181
- // Under `Suppress` the restore is TRANSACTIONAL, so snapshot the config
182
- // it is about to overwrite — see the failure tail below for why.
183
- let rollback =
184
- matches!(errors, RestoreErrors::Suppress).then(|| session.get_view_config().clone());
185
-
186
- let binding_before = (
187
- session.get_table().map(|t| t.get_name().to_owned()),
188
- session.pending_table(),
189
- );
190
-
191
- let load = match &update.table {
192
- OptionalUpdate::Update(name)
193
- if session.is_errored()
194
- || (!fresh
195
- && session
196
- .get_table()
197
- .map(|t| t.get_name() != name.as_str())
198
- .unwrap_or(true)) =>
199
- {
200
- Some(session.begin_pending_load())
201
- },
202
- _ => None,
203
- };
236
+ let overlay = renderer
237
+ .slot_name()
238
+ .map(|id| overlay_for(workspace, &PanelId::from(id)));
204
239
 
205
- let result = restore_and_render(
240
+ let rebound = !matches!(plan, BindPlan::Keep);
241
+ let prepared = prepare(
206
242
  session,
207
243
  renderer,
208
244
  presentation,
209
- RunOrigin::Public,
210
- update.clone(),
211
- {
212
- clone!(session, renderer, update.table, workspace, load);
213
- async move {
214
- let OptionalUpdate::Update(name) = table else {
215
- return Ok(());
216
- };
217
-
218
- match &load {
219
- Some(load) => {
220
- rebind_table_task(&session, &renderer, &workspace, name, missing, load)
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(load) = &load {
231
- load.close();
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
  }