@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
@@ -28,11 +28,12 @@ use super::wiring::{
28
28
  subscribe_panel_titles,
29
29
  };
30
30
  use crate::config::ViewerConfigInitial;
31
+ use crate::derived::suggest_name;
31
32
  use crate::queries::*;
32
33
  use crate::renderer::Renderer;
33
34
  use crate::session::*;
34
35
  use crate::tasks::*;
35
- use crate::utils::{Completion, spawn_owned};
36
+ use crate::utils::{Completion, PtrEqRc, spawn_owned};
36
37
  use crate::workspace::{Panel, PanelId};
37
38
 
38
39
  impl PerspectiveViewer {
@@ -215,7 +216,9 @@ impl PerspectiveViewer {
215
216
  let workspace = ctx.props().workspace.clone();
216
217
  let notify = ctx.link().callback(|_: ()| LayoutChanged);
217
218
  let activate = ctx.link().callback(|id| SetActivePanel(id, None));
219
+ let effect = workspace.effects().guard();
218
220
  ApiFuture::spawn(async move {
221
+ let _effect = effect;
219
222
  let config = panel
220
223
  .renderer
221
224
  .clone()
@@ -247,6 +250,39 @@ impl PerspectiveViewer {
247
250
  false
248
251
  }
249
252
 
253
+ pub(super) fn on_derive_panel(&mut self, ctx: &Context<Self>, id: String) -> bool {
254
+ if let Some(panel) = ctx.props().workspace.panel(&PanelId::from(id)) {
255
+ let presentation = ctx.props().presentation.clone();
256
+ let workspace = ctx.props().workspace.clone();
257
+ let effect = workspace.effects().guard();
258
+ ApiFuture::spawn(async move {
259
+ let _effect = effect;
260
+ let config = PtrEqRc::new(panel.session.get_view_config().clone());
261
+ let table = panel.session.get_table().map(|x| x.get_name().to_owned());
262
+ let name = suggest_name(
263
+ &fetch_hosted_tables(&workspace)
264
+ .await
265
+ .into_iter()
266
+ .flat_map(|(_, tables)| tables)
267
+ .collect(),
268
+ &config,
269
+ &table,
270
+ );
271
+
272
+ create_derived(
273
+ &panel.session,
274
+ &panel.renderer,
275
+ &presentation,
276
+ &workspace,
277
+ name,
278
+ )
279
+ .await
280
+ });
281
+ }
282
+
283
+ false
284
+ }
285
+
250
286
  pub(super) fn on_new_panel(&mut self, ctx: &Context<Self>, id: String) -> bool {
251
287
  if let Some(panel) = ctx.props().workspace.panel(&PanelId::from(id)) {
252
288
  let Some(table_name) = panel.session.get_table().map(|t| t.get_name().to_owned())
@@ -260,7 +296,9 @@ impl PerspectiveViewer {
260
296
  let workspace = ctx.props().workspace.clone();
261
297
  let notify = ctx.link().callback(|_: ()| LayoutChanged);
262
298
  let activate = ctx.link().callback(|id| SetActivePanel(id, None));
299
+ let effect = workspace.effects().guard();
263
300
  ApiFuture::spawn(async move {
301
+ let _effect = effect;
264
302
  let client = panel.session.get_client();
265
303
  let new_id = create_panel(
266
304
  &elem,
@@ -306,7 +344,9 @@ impl PerspectiveViewer {
306
344
  let workspace = ctx.props().workspace.clone();
307
345
  let notify = ctx.link().callback(|_: ()| LayoutChanged);
308
346
  let activate = ctx.link().callback(|id| SetActivePanel(id, None));
347
+ let effect = workspace.effects().guard();
309
348
  ApiFuture::spawn(async move {
349
+ let _effect = effect;
310
350
  let new_id = create_panel(
311
351
  &elem,
312
352
  &presentation,
@@ -179,6 +179,7 @@ impl PerspectiveViewer {
179
179
  vec![NewPanelFrom { client, table }]
180
180
  },
181
181
  PanelCommand::Duplicate => vec![DuplicatePanel(id)],
182
+ PanelCommand::Derive => vec![DerivePanel(id)],
182
183
  PanelCommand::NewFromPanel(source) => vec![DuplicatePanel(source)],
183
184
  PanelCommand::Reset => vec![ResetPanel(Some(id), false, None)],
184
185
  PanelCommand::ToggleMaster => vec![ToggleMaster(id)],
@@ -200,8 +200,6 @@ impl PerspectiveViewer {
200
200
  ToggleSettingsComplete(update, resolve)
201
201
  });
202
202
 
203
- // P2: at CLOSE time the open-state geometry is measurable —
204
- // cache the deltas the next OPEN needs to presize with.
205
203
  if is_open {
206
204
  self.settings_geometry.open_deltas =
207
205
  measure_settings_open_deltas(&ctx.props().elem);
@@ -213,16 +211,6 @@ impl PerspectiveViewer {
213
211
  let elem = ctx.props().elem.clone();
214
212
 
215
213
  ApiFuture::spawn(async move {
216
- // Resize every visible plugin (not just the active one) to its
217
- // new cell as the settings pane toggles. The pane is the outer
218
- // SplitPanel, which emits no `before-resize`, so this is driven
219
- // explicitly here. CLOSING grows the cells, so pre-size each
220
- // plugin to its grown box BEFORE collapsing the pane; OPENING
221
- // shrinks them, so pre-size to the shrunk box (from the deltas
222
- // cached at the last close; the first-ever open has none and
223
- // stays reactive) BEFORE the pane mounts — either way, one
224
- // clean resize with the reactive pass after as the exactness
225
- // finalizer.
226
214
  let result: ApiResult<JsValue> = async {
227
215
  if is_open {
228
216
  let presents = presize_visible_panels_grown(&workspace, &elem).await;
@@ -230,17 +218,7 @@ impl PerspectiveViewer {
230
218
  callback.emit(notify);
231
219
  presentation.set_settings_open(false, announce);
232
220
  rendered.await?;
233
- // `notify` fires in `rendered()` (same task as the
234
- // Yew DOM patch) and this future resumes as one of
235
- // its microtasks — so the staged reveal below and
236
- // the pane's geometry change reach the screen in a
237
- // single paint.
238
221
  presents.reveal();
239
- // I6: the exactness-finalizer resize is part of
240
- // what this toggle caused — await it here rather
241
- // than leaving it to the ResizeObserver (whose
242
- // continuous pass remains as the reactive
243
- // backstop).
244
222
  resize_visible_panels(&workspace).await;
245
223
  } else {
246
224
  let presents = if let Some((delta_w, delta_h)) = open_deltas {
@@ -23,7 +23,7 @@ use super::PerspectiveViewer;
23
23
  use crate::presentation::{DragDropProps, PresentationProps};
24
24
  use crate::renderer::RendererProps;
25
25
  use crate::session::{SessionProps, TableLoadState};
26
- use crate::tasks::resize_visible_panels;
26
+ use crate::tasks::{resize_visible_panels, submit_theme};
27
27
 
28
28
  impl PerspectiveViewer {
29
29
  pub(super) fn refresh_session_snapshot(&mut self, ctx: &Context<Self>) {
@@ -105,7 +105,7 @@ impl PerspectiveViewer {
105
105
  continue;
106
106
  }
107
107
 
108
- panel.renderer.set_theme(new_default.clone());
108
+ submit_theme(&panel.session, &panel.renderer, new_default.clone());
109
109
  if panel.renderer.needs_restyle() {
110
110
  let renderer = panel.renderer.clone();
111
111
  crate::utils::spawn_owned("default-theme-restyle", async move {
@@ -53,6 +53,7 @@ impl Drop for ShiftListeners {
53
53
  win.remove_event_listener_with_callback("keyup", self.keyup.as_ref().unchecked_ref());
54
54
  let _ = win.remove_event_listener_with_callback("blur", self.blur.as_ref().unchecked_ref());
55
55
  let _ = self.elem.class_list().remove_1("shift-active");
56
+ crate::utils::stamp_plugin_host_state_all(&self.elem);
56
57
  }
57
58
  }
58
59
 
@@ -62,6 +63,7 @@ pub(super) fn install_shift_listeners(elem: web_sys::HtmlElement) -> ShiftListen
62
63
  Closure::wrap(Box::new(move |event: KeyboardEvent| {
63
64
  if event.key() == "Shift" {
64
65
  let _ = elem.class_list().add_1("shift-active");
66
+ crate::utils::stamp_plugin_host_state_all(&elem);
65
67
  }
66
68
  }) as Box<dyn FnMut(KeyboardEvent)>)
67
69
  };
@@ -71,6 +73,7 @@ pub(super) fn install_shift_listeners(elem: web_sys::HtmlElement) -> ShiftListen
71
73
  Closure::wrap(Box::new(move |event: KeyboardEvent| {
72
74
  if event.key() == "Shift" {
73
75
  let _ = elem.class_list().remove_1("shift-active");
76
+ crate::utils::stamp_plugin_host_state_all(&elem);
74
77
  }
75
78
  }) as Box<dyn FnMut(KeyboardEvent)>)
76
79
  };
@@ -79,6 +82,7 @@ pub(super) fn install_shift_listeners(elem: web_sys::HtmlElement) -> ShiftListen
79
82
  let elem = elem.clone();
80
83
  Closure::wrap(Box::new(move |_: FocusEvent| {
81
84
  let _ = elem.class_list().remove_1("shift-active");
85
+ crate::utils::stamp_plugin_host_state_all(&elem);
82
86
  }) as Box<dyn FnMut(FocusEvent)>)
83
87
  };
84
88
 
@@ -446,6 +450,15 @@ pub(super) fn inject_active_callbacks(
446
450
 
447
451
  *renderer.on_render_limits_changed.borrow_mut() = Some(cb);
448
452
  }
453
+
454
+ {
455
+ let r = renderer.clone();
456
+ let cb = ctx
457
+ .link()
458
+ .callback(move |_: ()| UpdateRenderer(Box::new(r.to_props(None))));
459
+
460
+ *renderer.on_render_failure_changed.borrow_mut() = Some(cb);
461
+ }
449
462
  }
450
463
 
451
464
  /// Clear the active panel's session/renderer direct callbacks so a
@@ -454,6 +467,7 @@ pub(super) fn clear_active_callbacks(session: &Session, renderer: &Renderer) {
454
467
  *session.on_stats_changed.borrow_mut() = None;
455
468
  *session.on_table_errored.borrow_mut() = None;
456
469
  *renderer.on_render_limits_changed.borrow_mut() = None;
470
+ *renderer.on_render_failure_changed.borrow_mut() = None;
457
471
  }
458
472
 
459
473
  /// Inject element-level (shared) presentation callbacks. Created once at
@@ -246,26 +246,37 @@ impl Component for PerspectiveViewer {
246
246
  // config, so they survive. The `Completion`
247
247
  // resolves only after ALL panels' reset runs complete
248
248
  // (invariant I6 — previously it rode the active panel only,
249
- // and the other panels' runs were unowned).
250
- let workspace = &ctx.props().workspace;
249
+ // and the other panels' runs were unowned). An "all" reset
250
+ // FIRST purges the element's derived state (releasing any
251
+ // derived-bound panel to its root table), so the per-panel
252
+ // resets run against the surviving base bindings.
253
+ let workspace = ctx.props().workspace.clone();
254
+ let presentation = ctx.props().presentation.clone();
251
255
  let origins = workspace.clear_global_filters();
252
- clear_master_selections(workspace, origins);
253
- let mut tasks = Vec::new();
254
- for (id, panel) in workspace
255
- .panel_ids()
256
- .into_iter()
257
- .filter_map(|id| workspace.panel(&id).map(|p| (id, p)))
258
- {
259
- stamp_global_overlay(workspace, &id, &panel.session);
260
- tasks.push(reset_all(
261
- &panel.session,
262
- &panel.renderer,
263
- &ctx.props().presentation,
264
- all,
265
- ));
266
- }
267
-
256
+ clear_master_selections(&workspace, origins);
257
+ let effect = workspace.effects().guard();
268
258
  let run = async move {
259
+ let _effect = effect;
260
+ if all {
261
+ crate::tasks::purge_derived(&workspace, &presentation).await?;
262
+ }
263
+
264
+ let mut tasks = Vec::new();
265
+ for (id, panel) in workspace
266
+ .panel_ids()
267
+ .into_iter()
268
+ .filter_map(|id| workspace.panel(&id).map(|p| (id, p)))
269
+ {
270
+ let _ = id;
271
+ drop(broadcast_overlay(&workspace, &panel));
272
+ tasks.push(reset_all(
273
+ &panel.session,
274
+ &panel.renderer,
275
+ &presentation,
276
+ all,
277
+ ));
278
+ }
279
+
269
280
  futures::future::join_all(tasks)
270
281
  .await
271
282
  .into_iter()
@@ -288,15 +299,23 @@ impl Component for PerspectiveViewer {
288
299
  // rebuilt view re-applies it via `effective_view_config`,
289
300
  // keeping a detail panel consistent with the rest of the
290
301
  // workspace. A dangling id no-ops (dropping `sender` rejects
291
- // the API promise).
302
+ // the API promise). An "all" reset FIRST purges the
303
+ // element's derived state (releasing every derived-bound
304
+ // panel to its root table) — "all" means the same thing from
305
+ // the status bar gesture and the `resetPanel()` API.
292
306
  let id = id.map(crate::workspace::PanelId::from);
293
307
  if let Some(panel) = ctx.props().workspace.panel_or_active(id.as_ref()) {
294
- let run = reset_all(
295
- &panel.session,
296
- &panel.renderer,
297
- &ctx.props().presentation,
298
- all,
299
- );
308
+ let workspace = ctx.props().workspace.clone();
309
+ let presentation = ctx.props().presentation.clone();
310
+ let effect = workspace.effects().guard();
311
+ let run = async move {
312
+ let _effect = effect;
313
+ if all {
314
+ crate::tasks::purge_derived(&workspace, &presentation).await?;
315
+ }
316
+
317
+ reset_all(&panel.session, &panel.renderer, &presentation, all).await
318
+ };
300
319
 
301
320
  match sender {
302
321
  Some(completion) => completion.resolve_after(run),
@@ -314,6 +333,7 @@ impl Component for PerspectiveViewer {
314
333
  PanelClosed(id) => self.on_panel_closed(ctx, id),
315
334
  CommitWorkspaceRestore(id) => self.on_commit_workspace_restore(ctx, id),
316
335
  DuplicatePanel(id) => self.on_duplicate_panel(ctx, id),
336
+ DerivePanel(id) => self.on_derive_panel(ctx, id),
317
337
  NewPanel(id) => self.on_new_panel(ctx, id),
318
338
  NewPanelFrom { client, table } => self.on_new_panel_from(ctx, client, table),
319
339
 
@@ -24,7 +24,7 @@ use crate::components::dragdrop_list::{DragContext, DragDropList, DragDropListIt
24
24
  use crate::presentation::Presentation;
25
25
  use crate::session::{Session, SessionMetadataRc};
26
26
  use crate::ui::{IntlLabel, Select, SelectItem};
27
- use crate::utils::{AddListener, DragEffect, DragTarget, Subscription};
27
+ use crate::utils::{AddListener, DragEffect, DragTarget, Subscription, left_click};
28
28
 
29
29
  /// The declared capabilities of one window aggregate, for a `source` column
30
30
  /// type. Which controls an aggregate needs is the data model's to state - the
@@ -846,9 +846,10 @@ impl Component for WindowEditor {
846
846
  WindowSortDir::Asc
847
847
  };
848
848
 
849
- let onmousedown = ctx
850
- .link()
851
- .callback(|_: MouseEvent| WindowEditorMsg::ToggleOrderDir);
849
+ let onmousedown = left_click(
850
+ ctx.link()
851
+ .callback(|_: MouseEvent| WindowEditorMsg::ToggleOrderDir),
852
+ );
852
853
 
853
854
  yew::html_nested! {
854
855
  <WindowSlotColumn