@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
@@ -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 {
@@ -190,7 +191,7 @@ impl PerspectiveViewer {
190
191
  pub(super) fn on_panel_closed(&mut self, ctx: &Context<Self>, id: String) -> bool {
191
192
  let id = PanelId::from(id);
192
193
  if let Some(panel) = ctx.props().workspace.take_closing(&id) {
193
- let eject = eject_panel(panel);
194
+ let eject = eject_panel(panel, Disposal::Reject);
194
195
  match self.pending_closes.remove(&id) {
195
196
  Some(completion) => completion.resolve_after(eject),
196
197
  None => spawn_owned("close-panel", eject),
@@ -201,7 +202,7 @@ impl PerspectiveViewer {
201
202
 
202
203
  match self.detach_panel(ctx, &id) {
203
204
  Some(panel) => {
204
- spawn_owned("close-panel", eject_panel(panel));
205
+ spawn_owned("close-panel", eject_panel(panel, Disposal::Reject));
205
206
  true
206
207
  },
207
208
  None => false,
@@ -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,
@@ -337,7 +377,7 @@ impl PerspectiveViewer {
337
377
  self._active_subscriptions = create_active_subscriptions(ctx, &session, &renderer);
338
378
  self.session_props = session.to_props();
339
379
  self.renderer_props = renderer.to_props(None);
340
- self.update_count = session.in_flight_config_runs();
380
+ self.update_count = session.config_runs.count();
341
381
  self.active_session = session;
342
382
  self.active_renderer = renderer;
343
383
  let presentation = ctx.props().presentation.clone();
@@ -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
 
@@ -150,6 +154,7 @@ pub(super) fn global_filter_listener(
150
154
  let selection = selected.then(|| MasterSelection {
151
155
  cell_fallback: cell_fallback(&row, &column_names),
152
156
  filters: insert_filters,
157
+ clear_if_underivable: true,
153
158
  });
154
159
 
155
160
  cb.emit((panel, selection));
@@ -210,6 +215,7 @@ pub(super) fn master_click_listener(
210
215
  let selection = MasterSelection {
211
216
  cell_fallback: cell_fallback(&row, &column_names),
212
217
  filters: config.map(|x| x.filter).unwrap_or_default(),
218
+ clear_if_underivable: false,
213
219
  };
214
220
 
215
221
  cb.emit((panel, Some(selection)));
@@ -284,7 +290,8 @@ pub(super) fn create_active_subscriptions(
284
290
  let sub4 = session.view_config_changed.add_notify_listener(&cb);
285
291
  let sub5 = session.title_changed.add_notify_listener(&cb);
286
292
  let sub6 = session
287
- .run_state_changed
293
+ .config_runs
294
+ .changed()
288
295
  .add_listener(ctx.link().callback(UpdateInFlight));
289
296
 
290
297
  let sub7 = session.column_stats_changed.add_notify_listener(&cb);
@@ -443,6 +450,15 @@ pub(super) fn inject_active_callbacks(
443
450
 
444
451
  *renderer.on_render_limits_changed.borrow_mut() = Some(cb);
445
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
+ }
446
462
  }
447
463
 
448
464
  /// Clear the active panel's session/renderer direct callbacks so a
@@ -451,6 +467,7 @@ pub(super) fn clear_active_callbacks(session: &Session, renderer: &Renderer) {
451
467
  *session.on_stats_changed.borrow_mut() = None;
452
468
  *session.on_table_errored.borrow_mut() = None;
453
469
  *renderer.on_render_limits_changed.borrow_mut() = None;
470
+ *renderer.on_render_failure_changed.borrow_mut() = None;
454
471
  }
455
472
 
456
473
  /// Inject element-level (shared) presentation callbacks. Created once at
@@ -124,8 +124,8 @@ pub struct PerspectiveViewer {
124
124
  dragdrop_props: crate::presentation::DragDropProps,
125
125
 
126
126
  /// The active panel's in-flight config-run count — a LEVEL-triggered
127
- /// snapshot of `Session::in_flight_config_runs` (RAII-settled;
128
- /// assigned by `UpdateInFlight`, re-read on retarget). Threaded to
127
+ /// snapshot of `Session::config_runs` (RAII-settled; assigned by
128
+ /// `UpdateInFlight`, re-read on retarget). Threaded to
129
129
  /// `StatusIndicator` as the "updating" spinner.
130
130
  update_count: u32,
131
131
 
@@ -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