@perspective-dev/viewer 4.5.2 → 5.1.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 (227) hide show
  1. package/dist/cdn/perspective-viewer.js +20 -1
  2. package/dist/cdn/perspective-viewer.js.map +4 -4
  3. package/dist/css/botanical.css +1 -1
  4. package/dist/css/dracula.css +1 -1
  5. package/dist/css/gruvbox-dark.css +1 -1
  6. package/dist/css/gruvbox.css +1 -1
  7. package/dist/css/icons.css +1 -1
  8. package/dist/css/intl/de.css +1 -1
  9. package/dist/css/intl/es.css +1 -1
  10. package/dist/css/intl/fr.css +1 -1
  11. package/dist/css/intl/ja.css +1 -1
  12. package/dist/css/intl/pt.css +1 -1
  13. package/dist/css/intl/zh.css +1 -1
  14. package/dist/css/intl.css +1 -1
  15. package/dist/css/monokai.css +1 -1
  16. package/dist/css/phosphor.css +1 -1
  17. package/dist/css/pro-dark.css +1 -1
  18. package/dist/css/pro.css +1 -1
  19. package/dist/css/solarized-dark.css +1 -1
  20. package/dist/css/solarized.css +1 -1
  21. package/dist/css/themes.css +1 -1
  22. package/dist/css/vaporwave.css +1 -1
  23. package/dist/esm/extensions.d.ts +22 -3
  24. package/dist/esm/perspective-viewer.d.ts +9 -0
  25. package/dist/esm/perspective-viewer.inline.js +20 -1
  26. package/dist/esm/perspective-viewer.inline.js.map +4 -4
  27. package/dist/esm/perspective-viewer.js +20 -1
  28. package/dist/esm/perspective-viewer.js.map +4 -4
  29. package/dist/esm/plugin.d.ts +71 -5
  30. package/dist/esm/ts-rs/ClientOptions.d.ts +7 -0
  31. package/dist/esm/ts-rs/ExportMethod.d.ts +1 -0
  32. package/dist/esm/ts-rs/ExportOptions.d.ts +9 -0
  33. package/dist/esm/ts-rs/GetClientOptions.d.ts +8 -0
  34. package/dist/esm/ts-rs/GetTableOptions.d.ts +8 -0
  35. package/dist/esm/ts-rs/Layout.d.ts +11 -0
  36. package/dist/esm/ts-rs/Orientation.d.ts +4 -0
  37. package/dist/esm/ts-rs/PanelOptions.d.ts +7 -0
  38. package/dist/esm/ts-rs/PanelViewerConfig.d.ts +41 -0
  39. package/dist/esm/ts-rs/SplitLayout.d.ts +7 -0
  40. package/dist/esm/ts-rs/TabLayout.d.ts +10 -0
  41. package/dist/esm/ts-rs/ViewerConfig.d.ts +8 -2
  42. package/dist/esm/ts-rs/ViewerConfigUpdate.d.ts +9 -0
  43. package/dist/esm/ts-rs/WindowAggregate.d.ts +1 -0
  44. package/dist/esm/ts-rs/WindowSort.d.ts +7 -0
  45. package/dist/esm/ts-rs/WindowSortDir.d.ts +6 -0
  46. package/dist/esm/ts-rs/WindowSpec.d.ts +46 -0
  47. package/dist/esm/ts-rs/Windows.d.ts +10 -0
  48. package/dist/esm/ts-rs/WorkspaceConfig.d.ts +40 -0
  49. package/dist/esm/ts-rs/WorkspaceConfigUpdate.d.ts +28 -0
  50. package/dist/wasm/perspective-viewer.d.ts +299 -78
  51. package/dist/wasm/perspective-viewer.js +544 -186
  52. package/dist/wasm/perspective-viewer.wasm +0 -0
  53. package/dist/wasm/perspective-viewer.wasm.d.ts +34 -26
  54. package/dist/wasm/snippets/perspective-viewer-8a8c7d5d696b43ae/inline0.js +6 -0
  55. package/dist/wasm/snippets/{perspective-viewer-93064d0d79855d97/inline3.js → perspective-viewer-8a8c7d5d696b43ae/inline4.js} +5 -0
  56. package/package.json +3 -2
  57. package/src/css/_context_menu.css +14 -0
  58. package/src/css/column-selector.css +1 -0
  59. package/src/css/column-settings-panel.css +68 -3
  60. package/src/css/column-style.css +4 -3
  61. package/src/css/config-selector.css +22 -1
  62. package/src/css/containers/context-menu.css +107 -0
  63. package/src/css/containers/tabs.css +2 -2
  64. package/src/css/panel-tab.css +193 -0
  65. package/src/css/plugin-selector.css +7 -10
  66. package/src/css/status-bar.css +75 -81
  67. package/src/css/viewer.css +188 -70
  68. package/src/rust/components/column_dropdown.rs +40 -30
  69. package/src/rust/components/column_selector/active_column.rs +28 -45
  70. package/src/rust/components/column_selector/aggregate_selector.rs +4 -7
  71. package/src/rust/components/column_selector/column_selector_column_row.rs +73 -0
  72. package/src/rust/components/column_selector/config_selector.rs +40 -82
  73. package/src/rust/components/column_selector/expr_edit_button.rs +7 -1
  74. package/src/rust/components/column_selector/filter_column.rs +5 -15
  75. package/src/rust/components/column_selector/inactive_column.rs +11 -9
  76. package/src/rust/components/column_selector/sort_column.rs +3 -6
  77. package/src/rust/components/column_selector.rs +25 -14
  78. package/src/rust/components/column_settings_sidebar/style_tab.rs +2 -1
  79. package/src/rust/components/column_settings_sidebar/window_tab.rs +34 -0
  80. package/src/rust/components/column_settings_sidebar.rs +163 -16
  81. package/src/rust/components/containers/dragdrop_list.rs +73 -3
  82. package/src/rust/components/containers/sidebar.rs +99 -22
  83. package/src/rust/components/containers/split_panel.rs +181 -62
  84. package/src/rust/components/context_menu.rs +130 -0
  85. package/src/rust/components/editable_header.rs +2 -0
  86. package/src/rust/components/form/debug.rs +3 -0
  87. package/src/rust/components/global_filter_bar.rs +76 -0
  88. package/src/rust/components/main_panel/frame_theme.rs +186 -0
  89. package/src/rust/components/main_panel/msg.rs +62 -0
  90. package/src/rust/components/main_panel/presize.rs +220 -0
  91. package/src/rust/components/main_panel/reconcile.rs +224 -0
  92. package/src/rust/components/main_panel/render.rs +385 -0
  93. package/src/rust/components/main_panel/update.rs +184 -0
  94. package/src/rust/components/main_panel.rs +316 -71
  95. package/src/rust/components/mod.rs +5 -0
  96. package/src/rust/components/panel_menu.rs +493 -0
  97. package/src/rust/components/panel_tab.rs +510 -0
  98. package/src/rust/components/plugin_tab.rs +1 -1
  99. package/src/rust/components/portal.rs +7 -0
  100. package/src/rust/components/settings_panel.rs +13 -5
  101. package/src/rust/components/status_bar.rs +110 -178
  102. package/src/rust/components/status_indicator.rs +6 -9
  103. package/src/rust/components/style/mod.rs +5 -0
  104. package/src/rust/components/style/style_provider.rs +2 -0
  105. package/src/rust/components/viewer/filters.rs +133 -0
  106. package/src/rust/components/viewer/msg.rs +182 -0
  107. package/src/rust/components/viewer/panels.rs +326 -0
  108. package/src/rust/components/viewer/render.rs +255 -0
  109. package/src/rust/components/viewer/settings.rs +434 -0
  110. package/src/rust/components/viewer/snapshots.rs +134 -0
  111. package/src/rust/components/viewer/wiring.rs +505 -0
  112. package/src/rust/components/viewer.rs +245 -756
  113. package/src/rust/components/window_editor.rs +1013 -0
  114. package/src/rust/config/export_method.rs +1 -1
  115. package/src/rust/config/mod.rs +4 -0
  116. package/src/rust/config/options.rs +71 -0
  117. package/src/rust/config/viewer_config.rs +24 -2
  118. package/src/rust/config/workspace_config.rs +92 -0
  119. package/src/rust/custom_elements/copy_dropdown.rs +3 -1
  120. package/src/rust/custom_elements/debug_plugin.rs +20 -1
  121. package/src/rust/custom_elements/export_dropdown.rs +3 -1
  122. package/src/rust/custom_elements/viewer.rs +1015 -286
  123. package/src/rust/custom_events.rs +212 -61
  124. package/src/rust/js/mod.rs +2 -0
  125. package/src/rust/js/plugin.rs +297 -10
  126. package/src/rust/js/regular_layout.rs +258 -0
  127. package/src/rust/lib.rs +24 -2
  128. package/src/rust/presentation/column_locator.rs +16 -1
  129. package/src/rust/presentation.rs +66 -33
  130. package/src/rust/queries/columns_iter_set.rs +7 -1
  131. package/src/rust/queries/get_viewer_config.rs +29 -11
  132. package/src/rust/queries/plugin_column_styles.rs +2 -2
  133. package/src/rust/renderer/activate.rs +41 -14
  134. package/src/rust/renderer/dispatch.rs +463 -0
  135. package/src/rust/renderer/plugin_config.rs +495 -0
  136. package/src/rust/renderer/registry.rs +18 -6
  137. package/src/rust/renderer/render_timer.rs +29 -10
  138. package/src/rust/renderer.rs +384 -838
  139. package/src/rust/session/column_defaults_update.rs +26 -8
  140. package/src/rust/session/drag_drop_update.rs +18 -0
  141. package/src/rust/session/metadata.rs +66 -9
  142. package/src/rust/session/replace_expression_update.rs +1 -0
  143. package/src/rust/session/view_subscription.rs +30 -15
  144. package/src/rust/session.rs +755 -264
  145. package/src/rust/tasks/apply_global_filters.rs +89 -0
  146. package/src/rust/tasks/auto_pause.rs +171 -0
  147. package/src/rust/tasks/copy_export.rs +29 -19
  148. package/src/rust/tasks/create_panel.rs +243 -0
  149. package/src/rust/tasks/dismiss_render_warning.rs +3 -1
  150. package/src/rust/tasks/edit_expression.rs +4 -4
  151. package/src/rust/tasks/edit_window.rs +155 -0
  152. package/src/rust/tasks/eject.rs +64 -23
  153. package/src/rust/tasks/mod.rs +30 -4
  154. package/src/rust/tasks/pipeline.rs +411 -0
  155. package/src/rust/tasks/presize_panels.rs +324 -0
  156. package/src/rust/tasks/reset_all.rs +22 -12
  157. package/src/rust/tasks/resize_observer.rs +124 -87
  158. package/src/rust/tasks/restore_and_render.rs +80 -86
  159. package/src/rust/tasks/restore_panel.rs +145 -0
  160. package/src/rust/tasks/send_column_config.rs +5 -2
  161. package/src/rust/tasks/send_plugin_config.rs +5 -2
  162. package/src/rust/tasks/set_edit_mode.rs +59 -0
  163. package/src/rust/tasks/sync_update_panels.rs +152 -0
  164. package/src/rust/tasks/table_lifecycle.rs +158 -0
  165. package/src/rust/tasks/update_theme.rs +56 -12
  166. package/src/rust/utils/browser/dragdrop.rs +12 -0
  167. package/src/rust/utils/browser/tests/debounce.rs +49 -0
  168. package/src/rust/utils/completion.rs +56 -0
  169. package/src/rust/utils/custom_element.rs +5 -0
  170. package/src/rust/utils/debounce.rs +189 -17
  171. package/src/rust/utils/mod.rs +6 -0
  172. package/src/rust/utils/pending_effects.rs +62 -0
  173. package/src/rust/utils/spawn.rs +33 -0
  174. package/src/rust/utils/weak_scope.rs +0 -10
  175. package/src/rust/workspace.rs +846 -0
  176. package/src/svg/active-indicator.svg +3 -0
  177. package/src/svg/pin-icon.svg +5 -0
  178. package/src/themes/botanical.css +5 -2
  179. package/src/themes/defaults.css +54 -2
  180. package/src/themes/dracula.css +6 -3
  181. package/src/themes/gruvbox-dark.css +5 -2
  182. package/src/themes/gruvbox.css +5 -2
  183. package/src/themes/icons.css +5 -2
  184. package/src/themes/intl/de.css +7 -0
  185. package/src/themes/intl/es.css +7 -0
  186. package/src/themes/intl/fr.css +7 -0
  187. package/src/themes/intl/ja.css +7 -0
  188. package/src/themes/intl/pt.css +7 -0
  189. package/src/themes/intl/zh.css +7 -0
  190. package/src/themes/intl.css +10 -1
  191. package/src/themes/monokai.css +5 -2
  192. package/src/themes/phosphor.css +5 -2
  193. package/src/themes/pro-dark.css +5 -2
  194. package/src/themes/pro.css +2 -1
  195. package/src/themes/solarized-dark.css +5 -2
  196. package/src/themes/solarized.css +5 -2
  197. package/src/themes/vaporwave.css +5 -2
  198. package/src/ts/bootstrap.ts +8 -7
  199. package/src/ts/extensions.ts +30 -2
  200. package/src/ts/perspective-viewer.ts +10 -0
  201. package/src/ts/plugin.ts +72 -5
  202. package/src/ts/ts-rs/ClientOptions.ts +7 -0
  203. package/src/ts/ts-rs/ExportMethod.ts +3 -0
  204. package/src/ts/ts-rs/ExportOptions.ts +8 -0
  205. package/src/ts/ts-rs/GetClientOptions.ts +7 -0
  206. package/src/ts/ts-rs/GetTableOptions.ts +7 -0
  207. package/src/ts/ts-rs/Layout.ts +9 -0
  208. package/src/ts/ts-rs/Orientation.ts +6 -0
  209. package/src/ts/ts-rs/PanelOptions.ts +7 -0
  210. package/src/ts/ts-rs/PanelViewerConfig.ts +16 -0
  211. package/src/ts/ts-rs/SplitLayout.ts +5 -0
  212. package/src/ts/ts-rs/TabLayout.ts +11 -0
  213. package/src/ts/ts-rs/ViewerConfig.ts +7 -2
  214. package/src/ts/ts-rs/ViewerConfigUpdate.ts +9 -0
  215. package/src/ts/ts-rs/WindowAggregate.ts +3 -0
  216. package/src/ts/ts-rs/WindowSort.ts +9 -0
  217. package/src/ts/ts-rs/WindowSortDir.ts +8 -0
  218. package/src/ts/ts-rs/WindowSpec.ts +46 -0
  219. package/src/ts/ts-rs/Windows.ts +10 -0
  220. package/src/ts/ts-rs/WorkspaceConfig.ts +36 -0
  221. package/src/ts/ts-rs/WorkspaceConfigUpdate.ts +24 -0
  222. package/src/rust/tasks/intersection_observer.rs +0 -98
  223. package/src/rust/tasks/update_and_render.rs +0 -110
  224. /package/dist/wasm/snippets/{perspective-viewer-93064d0d79855d97/inline0.js → perspective-viewer-8a8c7d5d696b43ae/inline1.js} +0 -0
  225. /package/dist/wasm/snippets/{perspective-viewer-93064d0d79855d97/inline1.js → perspective-viewer-8a8c7d5d696b43ae/inline2.js} +0 -0
  226. /package/dist/wasm/snippets/{perspective-viewer-93064d0d79855d97/inline2.js → perspective-viewer-8a8c7d5d696b43ae/inline3.js} +0 -0
  227. /package/dist/wasm/snippets/{perspective-viewer-93064d0d79855d97/inline4.js → perspective-viewer-8a8c7d5d696b43ae/inline5.js} +0 -0
@@ -10,6 +10,7 @@
10
10
  // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11
11
  // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
12
 
13
+ use perspective_client::config::Filter;
13
14
  use wasm_bindgen_futures::spawn_local;
14
15
  use web_sys::*;
15
16
  use yew::prelude::*;
@@ -18,6 +19,7 @@ use super::status_indicator::StatusIndicator;
18
19
  use crate::components::containers::select::*;
19
20
  use crate::components::copy_dropdown::CopyDropDownMenu;
20
21
  use crate::components::export_dropdown::ExportDropDownMenu;
22
+ use crate::components::global_filter_bar::GlobalFilterBar;
21
23
  use crate::components::portal::PortalModal;
22
24
  use crate::components::status_bar_counter::StatusBarRowsCounter;
23
25
  use crate::components::style::StyleSurface;
@@ -28,6 +30,7 @@ use crate::renderer::*;
28
30
  use crate::session::*;
29
31
  use crate::tasks::*;
30
32
  use crate::utils::*;
33
+ use crate::workspace::Workspace;
31
34
  use crate::*;
32
35
 
33
36
  #[derive(Clone, Properties)]
@@ -38,10 +41,6 @@ pub struct StatusBarProps {
38
41
  /// Fired when the reset button is clicked.
39
42
  pub on_reset: Callback<bool>,
40
43
 
41
- /// Fires when the settings button is clicked
42
- #[prop_or_default]
43
- pub on_settings: Option<Callback<()>>,
44
-
45
44
  /// Snapshots threaded from root. Component reads `has_table`, `stats`,
46
45
  /// `error`, `title` from session_props; `selected_theme`,
47
46
  /// `available_themes`, `is_workspace` from presentation_props.
@@ -56,10 +55,25 @@ pub struct StatusBarProps {
56
55
  /// In-flight render counter, threaded to `StatusIndicator`.
57
56
  pub update_count: u32,
58
57
 
58
+ /// Element-level global filters (fed by master/detail selection); rendered
59
+ /// as removable chips between the row stats and the menu icons.
60
+ pub global_filters: Vec<Filter>,
61
+
62
+ /// Remove the global filter at this index (a chip's ×).
63
+ pub on_remove_global_filter: Callback<usize>,
64
+
65
+ /// Clear all global filters (the "Clear" affordance).
66
+ pub on_clear_global_filters: Callback<()>,
67
+
59
68
  // State
60
69
  pub session: Session,
61
70
  pub renderer: Renderer,
62
71
  pub presentation: Presentation,
72
+
73
+ /// The multi-panel model, so a theme change can restyle EVERY panel (not
74
+ /// just the active one this status bar targets) — non-active panels that
75
+ /// inherit the host theme otherwise render stale CSS until they redraw.
76
+ pub workspace: Workspace,
63
77
  }
64
78
 
65
79
  impl PartialEq for StatusBarProps {
@@ -69,6 +83,7 @@ impl PartialEq for StatusBarProps {
69
83
  && self.presentation_props == other.presentation_props
70
84
  && self.is_settings_open == other.is_settings_open
71
85
  && self.update_count == other.update_count
86
+ && self.global_filters == other.global_filters
72
87
  }
73
88
  }
74
89
 
@@ -78,25 +93,17 @@ pub enum StatusBarMsg {
78
93
  Copy,
79
94
  CloseExport,
80
95
  CloseCopy,
81
- Noop,
82
96
  Eject,
83
97
  SetTheme(String),
84
98
  ResetTheme,
85
99
  PointerEvent(web_sys::PointerEvent),
86
- TitleInputEvent,
87
- TitleChangeEvent,
88
100
  }
89
101
 
90
102
  /// A toolbar with buttons, and `Table` & `View` status information.
91
103
  pub struct StatusBar {
92
104
  copy_ref: NodeRef,
93
105
  export_ref: NodeRef,
94
- input_ref: NodeRef,
95
106
  statusbar_ref: NodeRef,
96
- /// Local title tracks the live `<input>` value before the user commits the
97
- /// change (blur / Enter). Reset to the prop value whenever the prop
98
- /// changes.
99
- title: Option<String>,
100
107
  copy_target: Option<HtmlElement>,
101
108
  export_target: Option<HtmlElement>,
102
109
  }
@@ -105,127 +112,76 @@ impl Component for StatusBar {
105
112
  type Message = StatusBarMsg;
106
113
  type Properties = StatusBarProps;
107
114
 
108
- fn create(ctx: &Context<Self>) -> Self {
115
+ fn create(_ctx: &Context<Self>) -> Self {
109
116
  Self {
110
117
  copy_ref: NodeRef::default(),
111
118
  export_ref: NodeRef::default(),
112
- input_ref: NodeRef::default(),
113
119
  statusbar_ref: NodeRef::default(),
114
- title: ctx.props().session_props.title.clone(),
115
120
  copy_target: None,
116
121
  export_target: None,
117
122
  }
118
123
  }
119
124
 
120
- fn changed(&mut self, ctx: &Context<Self>, old_props: &Self::Properties) -> bool {
121
- // Keep the local title in sync with the prop whenever the session title
122
- // changes externally (e.g. restore() call) or the settings panel opens /
123
- // closes (which resets the input element).
124
- if ctx.props().session_props.title != old_props.session_props.title
125
- || ctx.props().is_settings_open != old_props.is_settings_open
126
- {
127
- self.title = ctx.props().session_props.title.clone();
128
- }
129
- true
130
- }
131
-
132
125
  fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
133
- let r: ApiResult<bool> = (|| {
134
- Ok(match msg {
135
- StatusBarMsg::Reset(event) => {
136
- let all = event.shift_key();
137
- ctx.props().on_reset.emit(all);
138
- false
139
- },
140
- StatusBarMsg::ResetTheme => {
141
- update_theme(
142
- &ctx.props().session,
143
- &ctx.props().renderer,
144
- &ctx.props().presentation,
145
- None,
146
- );
147
- true
148
- },
149
- StatusBarMsg::SetTheme(theme_name) => {
150
- update_theme(
151
- &ctx.props().session,
152
- &ctx.props().renderer,
153
- &ctx.props().presentation,
154
- Some(theme_name),
155
- );
156
- false
157
- },
158
- StatusBarMsg::Export => {
159
- self.export_target = self.export_ref.cast::<HtmlElement>();
160
- true
161
- },
162
- StatusBarMsg::Copy => {
163
- self.copy_target = self.copy_ref.cast::<HtmlElement>();
164
- true
165
- },
166
- StatusBarMsg::CloseExport => {
167
- self.export_target = None;
168
- true
169
- },
170
- StatusBarMsg::CloseCopy => {
171
- self.copy_target = None;
172
- true
173
- },
174
- StatusBarMsg::Eject => {
175
- ctx.props().presentation.on_eject.emit(());
176
- false
177
- },
178
- StatusBarMsg::Noop => {
179
- self.title = ctx.props().session_props.title.clone();
180
- true
181
- },
182
- StatusBarMsg::TitleInputEvent => {
183
- let elem = self.input_ref.cast::<HtmlInputElement>().into_apierror()?;
184
- let title = elem.value();
185
- let title = if title.trim().is_empty() {
186
- None
187
- } else {
188
- Some(title)
189
- };
190
-
191
- self.title = title;
192
- true
193
- },
194
- StatusBarMsg::TitleChangeEvent => {
195
- let elem = self.input_ref.cast::<HtmlInputElement>().into_apierror()?;
196
- let title = elem.value();
197
- let title = if title.trim().is_empty() {
198
- None
199
- } else {
200
- Some(title)
201
- };
202
-
203
- ctx.props().session.set_title(title);
204
- false
205
- },
206
- StatusBarMsg::PointerEvent(event) => {
207
- if event.target().map(JsValue::from)
208
- == self.statusbar_ref.cast::<HtmlElement>().map(JsValue::from)
209
- {
210
- ctx.props().presentation.statusbar_pointer_event.emit(event);
211
- }
126
+ match msg {
127
+ StatusBarMsg::Reset(event) => {
128
+ let all = event.shift_key();
129
+ ctx.props().on_reset.emit(all);
130
+ false
131
+ },
132
+ StatusBarMsg::ResetTheme => {
133
+ update_theme(
134
+ &ctx.props().renderer,
135
+ &ctx.props().presentation,
136
+ &ctx.props().workspace,
137
+ None,
138
+ );
139
+ true
140
+ },
141
+ StatusBarMsg::SetTheme(theme_name) => {
142
+ update_theme(
143
+ &ctx.props().renderer,
144
+ &ctx.props().presentation,
145
+ &ctx.props().workspace,
146
+ Some(theme_name),
147
+ );
148
+ false
149
+ },
150
+ StatusBarMsg::Export => {
151
+ self.export_target = self.export_ref.cast::<HtmlElement>();
152
+ true
153
+ },
154
+ StatusBarMsg::Copy => {
155
+ self.copy_target = self.copy_ref.cast::<HtmlElement>();
156
+ true
157
+ },
158
+ StatusBarMsg::CloseExport => {
159
+ self.export_target = None;
160
+ true
161
+ },
162
+ StatusBarMsg::CloseCopy => {
163
+ self.copy_target = None;
164
+ true
165
+ },
166
+ StatusBarMsg::Eject => {
167
+ ctx.props().presentation.on_eject.emit(());
168
+ false
169
+ },
170
+ StatusBarMsg::PointerEvent(event) => {
171
+ if event.target().map(JsValue::from)
172
+ == self.statusbar_ref.cast::<HtmlElement>().map(JsValue::from)
173
+ {
174
+ ctx.props().presentation.statusbar_pointer_event.emit(event);
175
+ }
212
176
 
213
- false
214
- },
215
- })
216
- })();
217
- r.unwrap_or_else(|e| {
218
- web_sys::console::warn_1(&e.into());
219
- Default::default()
220
- })
177
+ false
178
+ },
179
+ }
221
180
  }
222
181
 
223
182
  fn view(&self, ctx: &Context<Self>) -> Html {
224
183
  let Self::Properties {
225
- presentation,
226
- renderer,
227
- session,
228
- ..
184
+ renderer, session, ..
229
185
  } = ctx.props();
230
186
 
231
187
  let has_table = ctx.props().session_props.has_table.clone();
@@ -234,12 +190,8 @@ impl Component for StatusBar {
234
190
  let title = &ctx.props().session_props.title;
235
191
 
236
192
  let mut is_updating_class_name = classes!();
237
- if title.is_some() {
238
- is_updating_class_name.push("titled");
239
- };
240
-
241
193
  if !is_settings_open {
242
- is_updating_class_name.push(["settings-closed", "titled"]);
194
+ is_updating_class_name.push("settings-closed");
243
195
  };
244
196
 
245
197
  if !matches!(has_table, Some(TableLoadState::Loaded)) {
@@ -247,34 +199,31 @@ impl Component for StatusBar {
247
199
  }
248
200
 
249
201
  // TODO Memoizing these would reduce some vdom diffing later on
250
- let onblur = ctx.link().callback(|_| StatusBarMsg::Noop);
251
202
  let onclose = ctx.link().callback(|_| StatusBarMsg::Eject);
252
203
  let onpointerdown = ctx.link().callback(StatusBarMsg::PointerEvent);
253
204
  let onexport = ctx.link().callback(|_: MouseEvent| StatusBarMsg::Export);
254
205
  let oncopy = ctx.link().callback(|_: MouseEvent| StatusBarMsg::Copy);
255
206
  let onreset = ctx.link().callback(StatusBarMsg::Reset);
256
- let onchange = ctx
257
- .link()
258
- .callback(|_: Event| StatusBarMsg::TitleChangeEvent);
259
-
260
- let oninput = ctx
261
- .link()
262
- .callback(|_: InputEvent| StatusBarMsg::TitleInputEvent);
263
-
264
- let is_menu = matches!(has_table, Some(TableLoadState::Loaded))
265
- && ctx.props().on_settings.as_ref().is_none();
266
- let is_title = is_menu
267
- || ctx.props().presentation_props.is_workspace
268
- || title.is_some()
269
- || is_errored
270
- || presentation.is_active(&self.input_ref.cast::<Element>());
271
-
272
- let is_settings = title.is_some()
273
- || ctx.props().presentation_props.is_workspace
274
- || !matches!(has_table, Some(TableLoadState::Loaded))
275
- || is_errored
276
- || is_settings_open
277
- || presentation.is_active(&self.input_ref.cast::<Element>());
207
+
208
+ // Project only the *active* panel's plugin toolbar into the shared status
209
+ // bar. Each panel's toolbar slots into `statusbar-extra-{its-panel-id}`
210
+ // (see datagrid `toolbar.ts`); the active panel's id comes from the
211
+ // active renderer this status bar is bound to.
212
+ let extra_slot = ctx
213
+ .props()
214
+ .renderer
215
+ .slot_name()
216
+ .map(|id| format!("statusbar-extra-{id}"))
217
+ .unwrap_or_else(|| "statusbar-extra".to_owned());
218
+ let is_menu = matches!(has_table, Some(TableLoadState::Loaded)) && is_settings_open;
219
+ // The editable title moved to the per-panel `<PanelTab>` headers; the
220
+ // status bar no longer hosts an `<input>`, so its `is_active` factor is
221
+ // gone. `is_title` now only gates the row counter.
222
+ let is_title =
223
+ is_menu || ctx.props().presentation_props.is_workspace || title.is_some() || is_errored;
224
+
225
+ let is_settings =
226
+ !matches!(has_table, Some(TableLoadState::Loaded)) || is_errored || is_settings_open;
278
227
 
279
228
  let on_copy_select = {
280
229
  let props = ctx.props().clone();
@@ -344,26 +293,18 @@ impl Component for StatusBar {
344
293
  update_count={ctx.props().update_count}
345
294
  session_props={ctx.props().session_props.clone()}
346
295
  />
347
- if is_title {
348
- <label
349
- class="input-sizer"
350
- data-value={self.title.clone().unwrap_or_default()}
351
- >
352
- <input
353
- ref={&self.input_ref}
354
- placeholder=""
355
- value={self.title.clone().unwrap_or_default()}
356
- size="10"
357
- {onblur}
358
- {onchange}
359
- {oninput}
360
- />
361
- <span id="status-bar-placeholder" />
362
- </label>
363
- }
364
296
  if is_title {
365
297
  <StatusBarRowsCounter stats={ctx.props().session_props.stats.clone()} />
366
298
  }
299
+ // Global-filter chips sit between the row stats and the
300
+ // menu icons (rendered only when there are filters).
301
+ if !ctx.props().global_filters.is_empty() {
302
+ <GlobalFilterBar
303
+ filters={ctx.props().global_filters.clone()}
304
+ on_remove={ctx.props().on_remove_global_filter.clone()}
305
+ on_clear={ctx.props().on_clear_global_filters.clone()}
306
+ />
307
+ }
367
308
  <div id="spacer" />
368
309
  if is_menu {
369
310
  <div id="menu-bar" class="section">
@@ -373,7 +314,7 @@ impl Component for StatusBar {
373
314
  on_change={ctx.link().callback(StatusBarMsg::SetTheme)}
374
315
  on_reset={ctx.link().callback(|_| StatusBarMsg::ResetTheme)}
375
316
  />
376
- <div id="plugin-settings"><slot name="statusbar-extra" /></div>
317
+ <div id="plugin-settings"><slot name={extra_slot} /></div>
377
318
  <span class="hover-target">
378
319
  <span id="reset" class="button" onmousedown={&onreset}>
379
320
  <span class="icon shift-alt-icon" />
@@ -402,14 +343,7 @@ impl Component for StatusBar {
402
343
  </span>
403
344
  </div>
404
345
  }
405
- if let Some(x) = ctx.props().on_settings.as_ref() {
406
- <div
407
- id="settings_button"
408
- class="noselect"
409
- onmousedown={x.reform(|_| ())}
410
- >
411
- <span class="icon" />
412
- </div>
346
+ if !is_settings_open {
413
347
  <div id="close_button" class="noselect" onmousedown={onclose}>
414
348
  <span class="icon" />
415
349
  </div>
@@ -441,18 +375,16 @@ impl Component for StatusBar {
441
375
  </PortalModal>
442
376
  </>
443
377
  }
444
- } else if let Some(x) = ctx.props().on_settings.as_ref() {
378
+ } else {
379
+ // Settings closed + loaded: no docked status bar. The open-settings
380
+ // affordance lives on the `PanelTab`s; only the (default-hidden)
381
+ // eject button floats here.
445
382
  let class = classes!(is_updating_class_name, "floating");
446
383
  html! {
447
384
  <div id={ctx.props().id.clone()} {class}>
448
- <div id="settings_button" class="noselect" onmousedown={x.reform(|_| ())}>
449
- <span class="icon" />
450
- </div>
451
385
  <div id="close_button" class="noselect" onmousedown={&onclose} />
452
386
  </div>
453
387
  }
454
- } else {
455
- html! {}
456
388
  }
457
389
  }
458
390
  }
@@ -17,18 +17,20 @@ use yew::prelude::*;
17
17
 
18
18
  use crate::renderer::Renderer;
19
19
  use crate::session::{Session, SessionProps, TableLoadState};
20
+ use crate::tasks::apply_and_render;
20
21
  use crate::utils::*;
21
22
 
22
23
  /// Value-prop version: no PubSub subscriptions, no reducer.
23
24
  /// The parent (`StatusBar`) re-renders this component whenever
24
25
  /// `session_props.error/has_table/stats` or `update_count` change (via
25
- /// root's `IncrementUpdateCount` / `DecrementUpdateCount` / `UpdateSession`
26
- /// messages).
26
+ /// root's `UpdateInFlight` / `UpdateSession` messages).
27
27
  #[derive(PartialEq, Properties)]
28
28
  pub struct StatusIndicatorProps {
29
29
  pub renderer: Renderer,
30
30
  pub session: Session,
31
- /// Number of in-flight renders (>0 → "updating" spinner).
31
+ /// Number of in-flight CONFIG-DRIVEN runs (>0 → "updating" spinner) —
32
+ /// a level-triggered snapshot of `Session::in_flight_config_runs`
33
+ /// (RAII-settled; see `UPDATE_COUNT_REGRESSION_PLAN.md`).
32
34
  pub update_count: u32,
33
35
  /// Snapshot of session value props — read for `error`, `has_table`,
34
36
  /// `stats` to derive the icon state.
@@ -81,12 +83,7 @@ pub fn StatusIndicator(props: &StatusIndicatorProps) -> Html {
81
83
  match &state {
82
84
  StatusIconState::Errored(..) => {
83
85
  session.reconnect().await?;
84
- let cfg = ViewConfigUpdate::default();
85
- session.update_view_config(cfg)?;
86
- renderer.apply_pending_plugin()?;
87
- renderer
88
- .draw(session.validate().await?.create_view())
89
- .await?;
86
+ apply_and_render(session, renderer, ViewConfigUpdate::default())?.await?;
90
87
  },
91
88
  StatusIconState::Normal => {
92
89
  session.status_indicator_clicked.emit(());
@@ -58,6 +58,9 @@ fn function_dropdown_bundle() -> &'static str {
58
58
  fn dropdown_menu_bundle() -> &'static str {
59
59
  include_str!(concat!(env!("OUT_DIR"), "/css/_dropdown_menu.css"))
60
60
  }
61
+ fn context_menu_bundle() -> &'static str {
62
+ include_str!(concat!(env!("OUT_DIR"), "/css/_context_menu.css"))
63
+ }
61
64
 
62
65
  thread_local! {
63
66
  static VIEWER_SHEET: web_sys::CssStyleSheet = make_sheet(viewer_bundle());
@@ -65,6 +68,7 @@ thread_local! {
65
68
  static FILTER_DROPDOWN_SHEET: web_sys::CssStyleSheet = make_sheet(filter_dropdown_bundle());
66
69
  static FUNCTION_DROPDOWN_SHEET: web_sys::CssStyleSheet = make_sheet(function_dropdown_bundle());
67
70
  static DROPDOWN_MENU_SHEET: web_sys::CssStyleSheet = make_sheet(dropdown_menu_bundle());
71
+ static CONTEXT_MENU_SHEET: web_sys::CssStyleSheet = make_sheet(context_menu_bundle());
68
72
  }
69
73
 
70
74
  fn make_sheet(css: &str) -> web_sys::CssStyleSheet {
@@ -81,5 +85,6 @@ pub(crate) fn surface_sheet(surface: StyleSurface) -> web_sys::CssStyleSheet {
81
85
  StyleSurface::FilterDropdown => FILTER_DROPDOWN_SHEET.with(|x| x.clone()),
82
86
  StyleSurface::FunctionDropdown => FUNCTION_DROPDOWN_SHEET.with(|x| x.clone()),
83
87
  StyleSurface::DropdownMenu => DROPDOWN_MENU_SHEET.with(|x| x.clone()),
88
+ StyleSurface::ContextMenu => CONTEXT_MENU_SHEET.with(|x| x.clone()),
84
89
  }
85
90
  }
@@ -29,6 +29,8 @@ pub enum StyleSurface {
29
29
  FunctionDropdown,
30
30
  /// Copy / Export menu (non-inverted color scheme).
31
31
  DropdownMenu,
32
+ /// Panel context menu (right-click a panel).
33
+ ContextMenu,
32
34
  }
33
35
 
34
36
  #[derive(Properties, PartialEq)]
@@ -0,0 +1,133 @@
1
+ // ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
2
+ // ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
3
+ // ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
4
+ // ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
5
+ // ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
6
+ // ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
7
+ // ┃ Copyright (c) 2017, the Perspective Authors. ┃
8
+ // ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
9
+ // ┃ This file is part of the Perspective library, distributed under the terms ┃
10
+ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11
+ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
+
13
+ //! Master/detail cross-filtering handlers. The filter set itself is OWNED BY
14
+ //! THE [`Workspace`] model (fed by master-panel selections via the
15
+ //! `perspective-global-filter` listener in [`super::wiring`], surfaced as
16
+ //! chips in the `StatusBar`); these handlers mutate it and run the
17
+ //! [`apply_global_filters`] task. Re-renders flow through the workspace's
18
+ //! `filters_changed` PubSub → `UpdateGlobalFilters` snapshot refresh.
19
+ //!
20
+ //! [`Workspace`]: crate::workspace::Workspace
21
+
22
+ use yew::prelude::*;
23
+
24
+ use super::PerspectiveViewer;
25
+ use super::msg::MasterSelection;
26
+ use crate::tasks::*;
27
+ use crate::workspace::PanelId;
28
+
29
+ impl PerspectiveViewer {
30
+ pub(super) fn on_toggle_master(&mut self, ctx: &Context<Self>, id: String) -> bool {
31
+ let id = PanelId::from(id);
32
+ let is_master = ctx.props().workspace.toggle_master(&id);
33
+ // Drive the panel's plugin into (out of) a selection mode so a
34
+ // master emits `perspective-global-filter` selections via the
35
+ // plugin's built-in select mechanism. No-op for plugins without
36
+ // an `edit_mode` (e.g. charts, which select intrinsically).
37
+ if let Some(panel) = ctx.props().workspace.panel(&id) {
38
+ let mode = if is_master {
39
+ "SELECT_ROW_TREE"
40
+ } else {
41
+ "READ_ONLY"
42
+ };
43
+ set_edit_mode(&panel.session, &panel.renderer, mode);
44
+ }
45
+ // Demoting a master back to detail retracts (only) its own
46
+ // contribution — other masters' clauses and any restored bucket
47
+ // survive. Promotion needs no filter mutation: the re-stamp below
48
+ // grants the new master immunity (and details keep filtering).
49
+ if !is_master {
50
+ ctx.props().workspace.clear_contribution(&id);
51
+ }
52
+ apply_global_filters(&ctx.props().workspace);
53
+ // Re-render regardless of a filter change — the context menu's
54
+ // "Master"/"Detail" label reads the toggled state.
55
+ true
56
+ }
57
+
58
+ pub(super) fn on_master_contribution(
59
+ &mut self,
60
+ ctx: &Context<Self>,
61
+ panel_id: String,
62
+ selection: Option<MasterSelection>,
63
+ ) -> bool {
64
+ // A master's selection state REPLACES its per-panel contribution to
65
+ // the element-level global filter set (`None` = deselected = clears
66
+ // it) — then surfaces in the bar and applies to the details. The
67
+ // re-render arrives via `filters_changed` → `UpdateGlobalFilters`.
68
+ // Contributions are keyed by the ORIGINATING panel, so removing them
69
+ // from the bar can clear its selection state. Non-master events
70
+ // (plain detail-panel selections/clicks) don't broadcast.
71
+ let workspace = &ctx.props().workspace;
72
+ let id = PanelId::from(panel_id);
73
+ if !workspace.is_master(&id) {
74
+ return false;
75
+ }
76
+
77
+ let filters = match selection {
78
+ None => Vec::new(),
79
+ Some(selection) => {
80
+ // Broadcast only the selection-DERIVED clauses: the event's
81
+ // filters embed the master's own stored `filter` field (a
82
+ // select detail's configs extend the panel config), which
83
+ // must not leak into the details — subtract it. (Live `Ref`
84
+ // guard — scope it to this statement.)
85
+ let own = workspace
86
+ .panel(&id)
87
+ .map(|p| p.session.get_view_config().filter.clone())
88
+ .unwrap_or_default();
89
+
90
+ let derived = selection
91
+ .filters
92
+ .into_iter()
93
+ .filter(|x| !own.contains(x))
94
+ .collect::<Vec<_>>();
95
+
96
+ if !derived.is_empty() {
97
+ derived
98
+ } else {
99
+ // Nothing derivable from the event's configs — fall back
100
+ // to the clicked-cell clause if the detail carried one,
101
+ // else leave the current contribution untouched.
102
+ match selection.cell_fallback {
103
+ Some(x) => vec![x],
104
+ None => return false,
105
+ }
106
+ }
107
+ },
108
+ };
109
+
110
+ workspace.set_contribution(&id, filters);
111
+ apply_global_filters(workspace);
112
+ false
113
+ }
114
+
115
+ pub(super) fn on_remove_global_filter(&mut self, ctx: &Context<Self>, index: usize) -> bool {
116
+ let workspace = &ctx.props().workspace;
117
+ let origins = workspace.remove_global_filter(index);
118
+ apply_global_filters(workspace);
119
+ // The chip's clause is gone — the master selection(s) that produced
120
+ // it must not stay highlighted, implying a filter that no longer
121
+ // exists. (Restored/unattributed clauses have no origin — no-op.)
122
+ clear_master_selections(workspace, origins);
123
+ false
124
+ }
125
+
126
+ pub(super) fn on_clear_global_filters(&mut self, ctx: &Context<Self>) -> bool {
127
+ let workspace = &ctx.props().workspace;
128
+ let origins = workspace.clear_global_filters();
129
+ apply_global_filters(workspace);
130
+ clear_master_selections(workspace, origins);
131
+ false
132
+ }
133
+ }