@perspective-dev/viewer 4.5.1 → 5.0.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.
- package/dist/cdn/perspective-viewer.js +20 -1
- package/dist/cdn/perspective-viewer.js.map +4 -4
- package/dist/css/botanical.css +1 -1
- package/dist/css/dracula.css +1 -1
- package/dist/css/gruvbox-dark.css +1 -1
- package/dist/css/gruvbox.css +1 -1
- package/dist/css/icons.css +1 -1
- package/dist/css/monokai.css +1 -1
- package/dist/css/phosphor.css +1 -1
- package/dist/css/pro-dark.css +1 -1
- package/dist/css/pro.css +1 -1
- package/dist/css/solarized-dark.css +1 -1
- package/dist/css/solarized.css +1 -1
- package/dist/css/themes.css +1 -1
- package/dist/css/vaporwave.css +1 -1
- package/dist/esm/extensions.d.ts +22 -3
- package/dist/esm/perspective-viewer.cdn.d.ts +3 -0
- package/dist/esm/perspective-viewer.d.ts +9 -0
- package/dist/esm/perspective-viewer.inline.d.ts +3 -0
- package/dist/esm/perspective-viewer.inline.js +20 -1
- package/dist/esm/perspective-viewer.inline.js.map +4 -4
- package/dist/esm/perspective-viewer.js +20 -1
- package/dist/esm/perspective-viewer.js.map +4 -4
- package/dist/esm/plugin.d.ts +71 -5
- package/dist/esm/ts-rs/ClientOptions.d.ts +7 -0
- package/dist/esm/ts-rs/ExportMethod.d.ts +1 -0
- package/dist/esm/ts-rs/ExportOptions.d.ts +9 -0
- package/dist/esm/ts-rs/GetClientOptions.d.ts +8 -0
- package/dist/esm/ts-rs/GetTableOptions.d.ts +8 -0
- package/dist/esm/ts-rs/Layout.d.ts +11 -0
- package/dist/esm/ts-rs/OnUpdateData.d.ts +6 -0
- package/dist/esm/ts-rs/Orientation.d.ts +4 -0
- package/dist/esm/ts-rs/PanelOptions.d.ts +7 -0
- package/dist/esm/ts-rs/PanelViewerConfig.d.ts +39 -0
- package/dist/esm/ts-rs/SplitLayout.d.ts +7 -0
- package/dist/esm/ts-rs/TabLayout.d.ts +10 -0
- package/dist/esm/ts-rs/ViewOnUpdateResp.d.ts +4 -0
- package/dist/esm/ts-rs/ViewerConfig.d.ts +6 -2
- package/dist/esm/ts-rs/WorkspaceConfig.d.ts +40 -0
- package/dist/esm/ts-rs/WorkspaceConfigUpdate.d.ts +28 -0
- package/dist/wasm/perspective-viewer.d.ts +213 -79
- package/dist/wasm/perspective-viewer.js +537 -189
- package/dist/wasm/perspective-viewer.wasm +0 -0
- package/dist/wasm/perspective-viewer.wasm.d.ts +36 -28
- package/dist/wasm/snippets/perspective-viewer-daeef4ceb066e6c7/inline0.js +6 -0
- package/dist/wasm/snippets/{perspective-viewer-3cd58f0374935772/inline3.js → perspective-viewer-daeef4ceb066e6c7/inline4.js} +5 -0
- package/package.json +3 -2
- package/src/css/_column_dropdown.css +14 -0
- package/src/css/_context_menu.css +14 -0
- package/src/css/_dropdown_menu.css +14 -0
- package/src/css/_filter_dropdown.css +14 -0
- package/src/css/_function_dropdown.css +14 -0
- package/src/css/_viewer_bundle.css +41 -0
- package/src/css/column-settings-panel.css +1 -1
- package/src/css/containers/context-menu.css +101 -0
- package/src/css/panel-tab.css +193 -0
- package/src/css/plugin-selector.css +7 -10
- package/src/css/status-bar.css +75 -81
- package/src/css/viewer.css +181 -71
- package/src/rust/components/column_dropdown.rs +43 -33
- package/src/rust/components/column_selector/active_column.rs +7 -18
- package/src/rust/components/column_selector/aggregate_selector.rs +4 -10
- package/src/rust/components/column_selector/config_selector.rs +33 -84
- package/src/rust/components/column_selector/empty_column.rs +0 -3
- package/src/rust/components/column_selector/filter_column.rs +5 -18
- package/src/rust/components/column_selector/inactive_column.rs +3 -8
- package/src/rust/components/column_selector/invalid_column.rs +0 -4
- package/src/rust/components/column_selector/sort_column.rs +3 -6
- package/src/rust/components/column_selector.rs +5 -14
- package/src/rust/components/column_settings_sidebar/style_tab/symbol/symbol_pairs.rs +0 -3
- package/src/rust/components/column_settings_sidebar/style_tab/symbol.rs +0 -3
- package/src/rust/components/column_settings_sidebar.rs +0 -3
- package/src/rust/components/containers/dropdown_menu.rs +1 -3
- package/src/rust/components/containers/scroll_panel.rs +1 -3
- package/src/rust/components/containers/split_panel.rs +175 -66
- package/src/rust/components/containers/tab_list.rs +0 -3
- package/src/rust/components/context_menu.rs +130 -0
- package/src/rust/components/datetime_column_style.rs +0 -3
- package/src/rust/components/empty_row.rs +0 -3
- package/src/rust/components/expression_editor.rs +2 -3
- package/src/rust/components/filter_dropdown.rs +3 -3
- package/src/rust/components/font_loader.rs +2 -2
- package/src/rust/components/form/code_editor.rs +0 -3
- package/src/rust/components/form/debug.rs +3 -4
- package/src/rust/components/function_dropdown.rs +3 -3
- package/src/rust/components/global_filter_bar.rs +76 -0
- package/src/rust/components/main_panel/frame_theme.rs +186 -0
- package/src/rust/components/main_panel/msg.rs +62 -0
- package/src/rust/components/main_panel/presize.rs +220 -0
- package/src/rust/components/main_panel/reconcile.rs +224 -0
- package/src/rust/components/main_panel/render.rs +385 -0
- package/src/rust/components/main_panel/update.rs +184 -0
- package/src/rust/components/main_panel.rs +311 -71
- package/src/rust/components/mod.rs +4 -0
- package/src/rust/components/number_series_style.rs +0 -3
- package/src/rust/components/panel_menu.rs +486 -0
- package/src/rust/components/panel_tab.rs +505 -0
- package/src/rust/components/plugin_selector.rs +0 -3
- package/src/rust/components/plugin_tab.rs +1 -6
- package/src/rust/components/portal.rs +5 -2
- package/src/rust/components/render_warning.rs +0 -3
- package/src/rust/components/settings_panel.rs +13 -5
- package/src/rust/components/status_bar.rs +113 -180
- package/src/rust/components/status_indicator.rs +6 -9
- package/src/rust/components/string_column_style.rs +0 -3
- package/src/rust/components/style/mod.rs +65 -27
- package/src/rust/components/style/style_provider.rs +51 -16
- package/src/rust/components/style_controls/number_string_format.rs +1 -8
- package/src/rust/components/type_icon.rs +1 -4
- package/src/rust/components/viewer/filters.rs +133 -0
- package/src/rust/components/viewer/msg.rs +180 -0
- package/src/rust/components/viewer/panels.rs +326 -0
- package/src/rust/components/viewer/render.rs +243 -0
- package/src/rust/components/viewer/settings.rs +402 -0
- package/src/rust/components/viewer/snapshots.rs +134 -0
- package/src/rust/components/viewer/wiring.rs +505 -0
- package/src/rust/components/viewer.rs +243 -758
- package/src/rust/config/export_method.rs +1 -1
- package/src/rust/config/mod.rs +4 -0
- package/src/rust/config/options.rs +71 -0
- package/src/rust/config/viewer_config.rs +24 -2
- package/src/rust/config/workspace_config.rs +92 -0
- package/src/rust/custom_elements/copy_dropdown.rs +9 -3
- package/src/rust/custom_elements/debug_plugin.rs +20 -1
- package/src/rust/custom_elements/export_dropdown.rs +9 -3
- package/src/rust/custom_elements/viewer.rs +1015 -286
- package/src/rust/custom_events.rs +212 -61
- package/src/rust/js/mod.rs +2 -0
- package/src/rust/js/plugin.rs +297 -10
- package/src/rust/js/regular_layout.rs +258 -0
- package/src/rust/lib.rs +20 -2
- package/src/rust/presentation.rs +66 -33
- package/src/rust/queries/get_viewer_config.rs +29 -11
- package/src/rust/queries/plugin_column_styles.rs +2 -2
- package/src/rust/renderer/activate.rs +41 -14
- package/src/rust/renderer/dispatch.rs +436 -0
- package/src/rust/renderer/plugin_config.rs +495 -0
- package/src/rust/renderer/registry.rs +18 -6
- package/src/rust/renderer/render_timer.rs +29 -10
- package/src/rust/renderer.rs +350 -827
- package/src/rust/session/column_defaults_update.rs +26 -8
- package/src/rust/session/metadata.rs +11 -1
- package/src/rust/session/view_subscription.rs +30 -15
- package/src/rust/session.rs +753 -251
- package/src/rust/tasks/apply_global_filters.rs +89 -0
- package/src/rust/tasks/auto_pause.rs +171 -0
- package/src/rust/tasks/copy_export.rs +29 -19
- package/src/rust/tasks/create_panel.rs +243 -0
- package/src/rust/tasks/dismiss_render_warning.rs +3 -1
- package/src/rust/tasks/edit_expression.rs +4 -4
- package/src/rust/tasks/eject.rs +64 -23
- package/src/rust/tasks/mod.rs +28 -4
- package/src/rust/tasks/pipeline.rs +411 -0
- package/src/rust/tasks/presize_panels.rs +324 -0
- package/src/rust/tasks/reset_all.rs +22 -12
- package/src/rust/tasks/resize_observer.rs +124 -87
- package/src/rust/tasks/restore_and_render.rs +80 -86
- package/src/rust/tasks/restore_panel.rs +145 -0
- package/src/rust/tasks/send_column_config.rs +5 -2
- package/src/rust/tasks/send_plugin_config.rs +5 -2
- package/src/rust/tasks/set_edit_mode.rs +59 -0
- package/src/rust/tasks/sync_update_panels.rs +152 -0
- package/src/rust/tasks/table_lifecycle.rs +158 -0
- package/src/rust/tasks/update_theme.rs +56 -12
- package/src/rust/utils/completion.rs +56 -0
- package/src/rust/utils/custom_element.rs +5 -0
- package/src/rust/utils/debounce.rs +130 -7
- package/src/rust/utils/mod.rs +6 -0
- package/src/rust/utils/pending_effects.rs +62 -0
- package/src/rust/{components/style/local_style.rs → utils/spawn.rs} +17 -19
- package/src/rust/utils/weak_scope.rs +0 -10
- package/src/rust/workspace.rs +846 -0
- package/src/svg/active-indicator.svg +3 -0
- package/src/themes/botanical.css +7 -2
- package/src/themes/defaults.css +54 -2
- package/src/themes/dracula.css +8 -3
- package/src/themes/gruvbox-dark.css +7 -2
- package/src/themes/gruvbox.css +5 -2
- package/src/themes/icons.css +4 -2
- package/src/themes/monokai.css +7 -2
- package/src/themes/phosphor.css +7 -2
- package/src/themes/pro-dark.css +7 -2
- package/src/themes/pro.css +2 -1
- package/src/themes/solarized-dark.css +5 -2
- package/src/themes/solarized.css +5 -2
- package/src/themes/vaporwave.css +7 -2
- package/src/ts/bootstrap.ts +8 -7
- package/src/ts/extensions.ts +30 -2
- package/src/ts/perspective-viewer.ts +10 -0
- package/src/ts/plugin.ts +72 -5
- package/src/ts/ts-rs/ClientOptions.ts +7 -0
- package/src/ts/ts-rs/ExportMethod.ts +3 -0
- package/src/ts/ts-rs/ExportOptions.ts +8 -0
- package/src/ts/ts-rs/GetClientOptions.ts +7 -0
- package/src/ts/ts-rs/GetTableOptions.ts +7 -0
- package/src/ts/ts-rs/Layout.ts +9 -0
- package/src/ts/ts-rs/Orientation.ts +6 -0
- package/src/ts/ts-rs/PanelOptions.ts +7 -0
- package/src/ts/ts-rs/PanelViewerConfig.ts +15 -0
- package/src/ts/ts-rs/SplitLayout.ts +5 -0
- package/src/ts/ts-rs/TabLayout.ts +11 -0
- package/src/ts/ts-rs/ViewerConfig.ts +6 -2
- package/src/ts/ts-rs/WorkspaceConfig.ts +36 -0
- package/src/ts/ts-rs/WorkspaceConfigUpdate.ts +24 -0
- package/tsconfig.json +1 -1
- package/src/rust/components/style/style_cache.rs +0 -136
- package/src/rust/tasks/intersection_observer.rs +0 -98
- package/src/rust/tasks/update_and_render.rs +0 -110
- /package/dist/wasm/snippets/{perspective-viewer-3cd58f0374935772/inline0.js → perspective-viewer-daeef4ceb066e6c7/inline1.js} +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-3cd58f0374935772/inline1.js → perspective-viewer-daeef4ceb066e6c7/inline2.js} +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-3cd58f0374935772/inline2.js → perspective-viewer-daeef4ceb066e6c7/inline3.js} +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-3cd58f0374935772/inline4.js → perspective-viewer-daeef4ceb066e6c7/inline5.js} +0 -0
|
@@ -0,0 +1,505 @@
|
|
|
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
|
+
//! Engine → root wiring: the PubSub subscriptions and direct callbacks that
|
|
14
|
+
//! reflect [`Session`]/[`Renderer`]/[`Presentation`] state into the root's
|
|
15
|
+
//! value-semantic snapshots, plus the raw window/host DOM listeners the root
|
|
16
|
+
//! owns (shift-key affordance, master/detail selection).
|
|
17
|
+
|
|
18
|
+
use std::collections::HashMap;
|
|
19
|
+
|
|
20
|
+
use perspective_client::config::{Filter, FilterTerm, Scalar};
|
|
21
|
+
use perspective_js::utils::*;
|
|
22
|
+
use wasm_bindgen::JsCast;
|
|
23
|
+
use wasm_bindgen::prelude::*;
|
|
24
|
+
use web_sys::{FocusEvent, KeyboardEvent};
|
|
25
|
+
use yew::prelude::*;
|
|
26
|
+
|
|
27
|
+
use super::PerspectiveViewer;
|
|
28
|
+
use super::msg::MasterSelection;
|
|
29
|
+
use super::msg::PerspectiveViewerMsg::{self, *};
|
|
30
|
+
use crate::js::JsPerspectiveViewerPlugin;
|
|
31
|
+
use crate::queries::*;
|
|
32
|
+
use crate::renderer::*;
|
|
33
|
+
use crate::session::*;
|
|
34
|
+
use crate::utils::*;
|
|
35
|
+
|
|
36
|
+
/// Window listeners that toggle the `.shift-active` class on the host element
|
|
37
|
+
/// while the Shift key is held, making Shift-modified affordances (e.g.
|
|
38
|
+
/// inactive column add, active column remove, status-bar reset) visually
|
|
39
|
+
/// discoverable. Stored so the closures outlive `create`.
|
|
40
|
+
pub(super) struct ShiftListeners {
|
|
41
|
+
elem: web_sys::HtmlElement,
|
|
42
|
+
keydown: Closure<dyn FnMut(KeyboardEvent)>,
|
|
43
|
+
keyup: Closure<dyn FnMut(KeyboardEvent)>,
|
|
44
|
+
blur: Closure<dyn FnMut(FocusEvent)>,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
impl Drop for ShiftListeners {
|
|
48
|
+
fn drop(&mut self) {
|
|
49
|
+
let win = global::window();
|
|
50
|
+
let _ = win
|
|
51
|
+
.remove_event_listener_with_callback("keydown", self.keydown.as_ref().unchecked_ref());
|
|
52
|
+
let _ =
|
|
53
|
+
win.remove_event_listener_with_callback("keyup", self.keyup.as_ref().unchecked_ref());
|
|
54
|
+
let _ = win.remove_event_listener_with_callback("blur", self.blur.as_ref().unchecked_ref());
|
|
55
|
+
let _ = self.elem.class_list().remove_1("shift-active");
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
pub(super) fn install_shift_listeners(elem: web_sys::HtmlElement) -> ShiftListeners {
|
|
60
|
+
let keydown = {
|
|
61
|
+
let elem = elem.clone();
|
|
62
|
+
Closure::wrap(Box::new(move |event: KeyboardEvent| {
|
|
63
|
+
if event.key() == "Shift" {
|
|
64
|
+
let _ = elem.class_list().add_1("shift-active");
|
|
65
|
+
}
|
|
66
|
+
}) as Box<dyn FnMut(KeyboardEvent)>)
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
let keyup = {
|
|
70
|
+
let elem = elem.clone();
|
|
71
|
+
Closure::wrap(Box::new(move |event: KeyboardEvent| {
|
|
72
|
+
if event.key() == "Shift" {
|
|
73
|
+
let _ = elem.class_list().remove_1("shift-active");
|
|
74
|
+
}
|
|
75
|
+
}) as Box<dyn FnMut(KeyboardEvent)>)
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
let blur = {
|
|
79
|
+
let elem = elem.clone();
|
|
80
|
+
Closure::wrap(Box::new(move |_: FocusEvent| {
|
|
81
|
+
let _ = elem.class_list().remove_1("shift-active");
|
|
82
|
+
}) as Box<dyn FnMut(FocusEvent)>)
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
let win = global::window();
|
|
86
|
+
let _ = win.add_event_listener_with_callback("keydown", keydown.as_ref().unchecked_ref());
|
|
87
|
+
let _ = win.add_event_listener_with_callback("keyup", keyup.as_ref().unchecked_ref());
|
|
88
|
+
let _ = win.add_event_listener_with_callback("blur", blur.as_ref().unchecked_ref());
|
|
89
|
+
|
|
90
|
+
ShiftListeners {
|
|
91
|
+
elem,
|
|
92
|
+
keydown,
|
|
93
|
+
keyup,
|
|
94
|
+
blur,
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/// The `perspective-global-filter` host listener (master/detail): routes a
|
|
99
|
+
/// master panel's selection state to
|
|
100
|
+
/// [`PerspectiveViewerMsg::MasterContribution`] — `selected: true` REPLACES
|
|
101
|
+
/// the panel's contribution with the event's filters, `selected: false`
|
|
102
|
+
/// clears it. Kept alive on the root; attached once in `rendered`.
|
|
103
|
+
pub(super) fn global_filter_listener(
|
|
104
|
+
ctx: &Context<PerspectiveViewer>,
|
|
105
|
+
) -> Closure<dyn FnMut(web_sys::Event)> {
|
|
106
|
+
let cb = ctx
|
|
107
|
+
.link()
|
|
108
|
+
.callback(|(panel, selection)| PerspectiveViewerMsg::MasterContribution(panel, selection));
|
|
109
|
+
|
|
110
|
+
Closure::wrap(Box::new(move |event: web_sys::Event| {
|
|
111
|
+
// `PerspectiveSelectDetail` exposes `selected`/`insertFilters` as
|
|
112
|
+
// prototype *getters*; struct deserialization reads each field by
|
|
113
|
+
// named property access (not own-key enumeration), so the getters are
|
|
114
|
+
// invoked — a map target would see none of them. Field names mirror
|
|
115
|
+
// the detail EXACTLY — it mixes `insertFilters` (camel) with
|
|
116
|
+
// `column_names` (snake), so no `rename_all`.
|
|
117
|
+
#[derive(serde::Deserialize)]
|
|
118
|
+
struct SelectDetail {
|
|
119
|
+
// Only multi-panel selections (carrying a source `panel` id) route.
|
|
120
|
+
panel: Option<String>,
|
|
121
|
+
|
|
122
|
+
#[serde(default)]
|
|
123
|
+
selected: bool,
|
|
124
|
+
|
|
125
|
+
#[serde(default, rename = "insertFilters")]
|
|
126
|
+
insert_filters: Vec<Filter>,
|
|
127
|
+
|
|
128
|
+
#[serde(default)]
|
|
129
|
+
row: HashMap<String, serde_json::Value>,
|
|
130
|
+
|
|
131
|
+
#[serde(default)]
|
|
132
|
+
column_names: Vec<serde_json::Value>,
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
let Some(ce) = event.dyn_ref::<web_sys::CustomEvent>() else {
|
|
136
|
+
return;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
let Ok(SelectDetail {
|
|
140
|
+
panel: Some(panel),
|
|
141
|
+
selected,
|
|
142
|
+
insert_filters,
|
|
143
|
+
row,
|
|
144
|
+
column_names,
|
|
145
|
+
}) = ce.detail().into_serde_ext()
|
|
146
|
+
else {
|
|
147
|
+
return;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
let selection = selected.then(|| MasterSelection {
|
|
151
|
+
cell_fallback: cell_fallback(&row, &column_names),
|
|
152
|
+
filters: insert_filters,
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
cb.emit((panel, selection));
|
|
156
|
+
}) as Box<dyn FnMut(web_sys::Event)>)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/// The `perspective-click` host listener: a click on a MASTER panel is a
|
|
160
|
+
/// selection too (chart bars, flat datagrids — plugins whose click carries
|
|
161
|
+
/// the clicked datum but which have no select mode), routed as an
|
|
162
|
+
/// always-`Some` [`PerspectiveViewerMsg::MasterContribution`]. Clicks have no
|
|
163
|
+
/// deselect arm — deselection arrives via the `perspective-global-filter`
|
|
164
|
+
/// listener or the bar's chip ×. Non-master panels' clicks are dropped by the
|
|
165
|
+
/// handler's `is_master` guard, and the datagrid suppresses clicks entirely
|
|
166
|
+
/// in its row-select mode so a select/deselect never races a click echo.
|
|
167
|
+
pub(super) fn master_click_listener(
|
|
168
|
+
ctx: &Context<PerspectiveViewer>,
|
|
169
|
+
) -> Closure<dyn FnMut(web_sys::Event)> {
|
|
170
|
+
let cb = ctx
|
|
171
|
+
.link()
|
|
172
|
+
.callback(|(panel, selection)| PerspectiveViewerMsg::MasterContribution(panel, selection));
|
|
173
|
+
|
|
174
|
+
Closure::wrap(Box::new(move |event: web_sys::Event| {
|
|
175
|
+
#[derive(serde::Deserialize)]
|
|
176
|
+
struct ClickConfig {
|
|
177
|
+
#[serde(default)]
|
|
178
|
+
filter: Vec<Filter>,
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
#[derive(serde::Deserialize)]
|
|
182
|
+
struct ClickDetail {
|
|
183
|
+
// Only multi-panel clicks (carrying a source `panel` id) route.
|
|
184
|
+
panel: Option<String>,
|
|
185
|
+
|
|
186
|
+
#[serde(default)]
|
|
187
|
+
config: Option<ClickConfig>,
|
|
188
|
+
|
|
189
|
+
#[serde(default)]
|
|
190
|
+
row: HashMap<String, serde_json::Value>,
|
|
191
|
+
|
|
192
|
+
#[serde(default)]
|
|
193
|
+
column_names: Vec<serde_json::Value>,
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
let Some(ce) = event.dyn_ref::<web_sys::CustomEvent>() else {
|
|
197
|
+
return;
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
let Ok(ClickDetail {
|
|
201
|
+
panel: Some(panel),
|
|
202
|
+
config,
|
|
203
|
+
row,
|
|
204
|
+
column_names,
|
|
205
|
+
}) = ce.detail().into_serde_ext()
|
|
206
|
+
else {
|
|
207
|
+
return;
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
let selection = MasterSelection {
|
|
211
|
+
cell_fallback: cell_fallback(&row, &column_names),
|
|
212
|
+
filters: config.map(|x| x.filter).unwrap_or_default(),
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
cb.emit((panel, Some(selection)));
|
|
216
|
+
}) as Box<dyn FnMut(web_sys::Event)>)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/// Synthesize the clicked-cell `[column, "==", value]` fallback clause from a
|
|
220
|
+
/// select/click detail's `row` + `column_names` — the broadcast for masters
|
|
221
|
+
/// whose events carry no derivable filters (e.g. a FLAT datagrid, whose rows
|
|
222
|
+
/// have no group-by path). The first real (non-`__`-internal, string-named)
|
|
223
|
+
/// column with a scalar value in the row wins.
|
|
224
|
+
fn cell_fallback(
|
|
225
|
+
row: &HashMap<String, serde_json::Value>,
|
|
226
|
+
column_names: &[serde_json::Value],
|
|
227
|
+
) -> Option<Filter> {
|
|
228
|
+
let column = column_names
|
|
229
|
+
.iter()
|
|
230
|
+
.filter_map(|x| x.as_str())
|
|
231
|
+
.find(|x| !x.starts_with("__"))?;
|
|
232
|
+
|
|
233
|
+
let term = match row.get(column)? {
|
|
234
|
+
serde_json::Value::Bool(x) => Scalar::Bool(*x),
|
|
235
|
+
serde_json::Value::Number(x) => Scalar::Float(x.as_f64()?),
|
|
236
|
+
serde_json::Value::String(x) => Scalar::String(x.clone()),
|
|
237
|
+
_ => return None,
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
Some(Filter::new(column, "==", FilterTerm::Scalar(term)))
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/// Subscribe to *every* panel's `title_changed` so a title change on any panel
|
|
244
|
+
/// (not just the active one — e.g. a non-active panel restored via
|
|
245
|
+
/// `restorePanel`) re-renders the tab titles. Recreated when the panel set
|
|
246
|
+
/// changes.
|
|
247
|
+
pub(super) fn subscribe_panel_titles(ctx: &Context<PerspectiveViewer>) -> Vec<Subscription> {
|
|
248
|
+
let cb = ctx.link().callback(|_| PerspectiveViewerMsg::TitlesChanged);
|
|
249
|
+
ctx.props()
|
|
250
|
+
.workspace
|
|
251
|
+
.panel_ids()
|
|
252
|
+
.iter()
|
|
253
|
+
.filter_map(|id| ctx.props().workspace.panel(id))
|
|
254
|
+
.flat_map(|p| {
|
|
255
|
+
// Re-collect titles on a title change *or* a table (un)load — the
|
|
256
|
+
// latter so the untitled table-name fallback appears once a table
|
|
257
|
+
// binds to the panel.
|
|
258
|
+
[
|
|
259
|
+
p.session.title_changed.add_notify_listener(&cb),
|
|
260
|
+
p.session.table_loaded.add_notify_listener(&cb),
|
|
261
|
+
]
|
|
262
|
+
})
|
|
263
|
+
.collect()
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/// Per-active-panel subscriptions: reflect the *active* panel's
|
|
267
|
+
/// session/renderer PubSub events into the root's snapshots. Dropped +
|
|
268
|
+
/// recreated against the new active panel on `SetActivePanel` (pair with
|
|
269
|
+
/// [`clear_active_callbacks`] for the direct-callback half).
|
|
270
|
+
pub(super) fn create_active_subscriptions(
|
|
271
|
+
ctx: &Context<PerspectiveViewer>,
|
|
272
|
+
session: &Session,
|
|
273
|
+
renderer: &Renderer,
|
|
274
|
+
) -> Vec<Subscription> {
|
|
275
|
+
let session_props_sub = {
|
|
276
|
+
let s = session.clone();
|
|
277
|
+
let cb = ctx
|
|
278
|
+
.link()
|
|
279
|
+
.callback(move |_: ()| UpdateSession(Box::new(s.to_props())));
|
|
280
|
+
|
|
281
|
+
let sub1 = session.table_loaded.add_notify_listener(&cb);
|
|
282
|
+
let sub2 = session.table_unloaded.add_notify_listener(&cb);
|
|
283
|
+
// Snapshot refresh on every RECONCILED commit (SKIP/REUSE/paused
|
|
284
|
+
// included), not just `View` construction.
|
|
285
|
+
let sub3 = session.commit_reconciled.add_notify_listener(&cb);
|
|
286
|
+
let sub4 = session.view_config_changed.add_notify_listener(&cb);
|
|
287
|
+
let sub5 = session.title_changed.add_notify_listener(&cb);
|
|
288
|
+
|
|
289
|
+
// The `StatusIndicator` spinner count. LEVEL-triggered: the payload
|
|
290
|
+
// is the ABSOLUTE in-flight config-run count (RAII-settled by
|
|
291
|
+
// `ConfigRunToken`), which the handler ASSIGNS — replacing the
|
|
292
|
+
// edge-counted `view_config_changed`+1/`view_created`−1 pair, whose
|
|
293
|
+
// pairing the pipeline does not guarantee (stuck-spinner
|
|
294
|
+
// regression — see `UPDATE_COUNT_REGRESSION_PLAN.md`).
|
|
295
|
+
let sub6 = session
|
|
296
|
+
.run_state_changed
|
|
297
|
+
.add_listener(ctx.link().callback(UpdateInFlight));
|
|
298
|
+
|
|
299
|
+
// Stats fetch resolution (populates session.column_stats) triggers
|
|
300
|
+
// a fresh `SessionProps` so `column_stats` reaches downstream
|
|
301
|
+
// components and the StyleTab re-queries the schema with the
|
|
302
|
+
// new value.
|
|
303
|
+
let sub7 = session.column_stats_changed.add_notify_listener(&cb);
|
|
304
|
+
|
|
305
|
+
vec![sub1, sub2, sub3, sub4, sub5, sub6, sub7]
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
let renderer_props_sub = {
|
|
309
|
+
let cb_plugin = ctx.link().callback({
|
|
310
|
+
let renderer = renderer.clone();
|
|
311
|
+
move |_: JsPerspectiveViewerPlugin| UpdateRenderer(Box::new(renderer.to_props(None)))
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
// Re-snapshot RendererProps when the plugin_config bucket
|
|
315
|
+
// changes (in-tab edit via `send_plugin_config`, JSON paste via
|
|
316
|
+
// `restore_and_render`, full clear via `reset_all` with
|
|
317
|
+
// `all=true`). Without this, `RendererProps.plugin_config`
|
|
318
|
+
// would stay frozen at its construct-time value and `PluginTab`
|
|
319
|
+
// would render stale.
|
|
320
|
+
let cb_plugin_config = ctx.link().callback({
|
|
321
|
+
let renderer = renderer.clone();
|
|
322
|
+
move |_: serde_json::Map<String, serde_json::Value>| {
|
|
323
|
+
UpdateRenderer(Box::new(renderer.to_props(None)))
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
let sub1 = renderer.plugin_changed.add_listener(cb_plugin);
|
|
328
|
+
let sub2 = renderer
|
|
329
|
+
.plugin_config_changed
|
|
330
|
+
.add_listener(cb_plugin_config);
|
|
331
|
+
|
|
332
|
+
vec![sub1, sub2]
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
let mut subscriptions = Vec::new();
|
|
336
|
+
subscriptions.extend(session_props_sub);
|
|
337
|
+
subscriptions.extend(renderer_props_sub);
|
|
338
|
+
subscriptions
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/// Element-level (shared, not per-panel) subscriptions: presentation +
|
|
342
|
+
/// drag/drop. Created once at construction.
|
|
343
|
+
pub(super) fn create_shared_subscriptions(ctx: &Context<PerspectiveViewer>) -> Vec<Subscription> {
|
|
344
|
+
let presentation_props_sub = {
|
|
345
|
+
let presentation = ctx.props().presentation.clone();
|
|
346
|
+
let cb_settings = ctx.link().callback(UpdateSettingsOpen);
|
|
347
|
+
let cb_theme = {
|
|
348
|
+
let pres = presentation.clone();
|
|
349
|
+
ctx.link()
|
|
350
|
+
.callback(move |(themes, _): (PtrEqRc<Vec<String>>, _)| {
|
|
351
|
+
UpdatePresentation(Box::new(pres.to_props(themes)))
|
|
352
|
+
})
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
let cb_column_settings = {
|
|
356
|
+
let pres = presentation.clone();
|
|
357
|
+
ctx.link().callback(move |_: (bool, Option<String>)| {
|
|
358
|
+
UpdateColumnSettings(Box::new(pres.get_open_column_settings()))
|
|
359
|
+
})
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
let sub1 = presentation.settings_open_changed.add_listener(cb_settings);
|
|
363
|
+
let sub2 = presentation.theme_config_updated.add_listener(cb_theme);
|
|
364
|
+
let sub3 = presentation
|
|
365
|
+
.column_settings_open_changed
|
|
366
|
+
.add_listener(cb_column_settings);
|
|
367
|
+
|
|
368
|
+
vec![sub1, sub2, sub3]
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
let dragdrop_props_sub = {
|
|
372
|
+
let cb_clear = ctx.link().callback(|_: ()| UpdateDragDrop(Box::default()));
|
|
373
|
+
let sub1 = ctx
|
|
374
|
+
.props()
|
|
375
|
+
.presentation
|
|
376
|
+
.drop_received
|
|
377
|
+
.add_notify_listener(&cb_clear);
|
|
378
|
+
|
|
379
|
+
vec![sub1]
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
// The `Workspace`-owned global filter set → the root's render snapshot.
|
|
383
|
+
let filters_sub = {
|
|
384
|
+
let cb = ctx.link().callback(|_: ()| UpdateGlobalFilters);
|
|
385
|
+
ctx.props()
|
|
386
|
+
.workspace
|
|
387
|
+
.filters_changed()
|
|
388
|
+
.add_notify_listener(&cb)
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
// Staged-set transitions (`Workspace::stage_panel`/`clear_staged`) → a
|
|
392
|
+
// root re-render for the stage-level chrome (`only-child` class,
|
|
393
|
+
// binding resync). `MainPanel` inserts/reveals promoted panels via its
|
|
394
|
+
// OWN `staged_changed` subscription (`MainPanelMsg::StagedChanged`) —
|
|
395
|
+
// see `WorkspaceData::staged_changed`.
|
|
396
|
+
let staged_sub = {
|
|
397
|
+
let cb = ctx.link().callback(|_: ()| LayoutChanged);
|
|
398
|
+
ctx.props()
|
|
399
|
+
.workspace
|
|
400
|
+
.staged_changed()
|
|
401
|
+
.add_notify_listener(&cb)
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
let mut subscriptions = Vec::new();
|
|
405
|
+
subscriptions.extend(presentation_props_sub);
|
|
406
|
+
subscriptions.extend(dragdrop_props_sub);
|
|
407
|
+
subscriptions.push(filters_sub);
|
|
408
|
+
subscriptions.push(staged_sub);
|
|
409
|
+
subscriptions
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/// Inject the *active* panel's session/renderer direct callbacks into its
|
|
413
|
+
/// engine handles. Cleared via [`clear_active_callbacks`] before re-targeting
|
|
414
|
+
/// on `SetActivePanel`.
|
|
415
|
+
pub(super) fn inject_active_callbacks(
|
|
416
|
+
ctx: &Context<PerspectiveViewer>,
|
|
417
|
+
session: &Session,
|
|
418
|
+
renderer: &Renderer,
|
|
419
|
+
) {
|
|
420
|
+
// Session: on_stats_changed
|
|
421
|
+
{
|
|
422
|
+
let s = session.clone();
|
|
423
|
+
let cb = ctx
|
|
424
|
+
.link()
|
|
425
|
+
.callback(move |_: ()| UpdateSessionStats(s.get_table_stats(), s.has_table()));
|
|
426
|
+
|
|
427
|
+
*session.on_stats_changed.borrow_mut() = Some(cb);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Session: on_table_errored
|
|
431
|
+
{
|
|
432
|
+
let s = session.clone();
|
|
433
|
+
let cb = ctx
|
|
434
|
+
.link()
|
|
435
|
+
.callback(move |_: ()| UpdateSession(Box::new(s.to_props())));
|
|
436
|
+
|
|
437
|
+
*session.on_table_errored.borrow_mut() = Some(cb);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Renderer: on_render_limits_changed (combines UpdateRenderer + column
|
|
441
|
+
// locator recheck that were previously two separate PubSub subscriptions).
|
|
442
|
+
{
|
|
443
|
+
let presentation = ctx.props().presentation.clone();
|
|
444
|
+
let r = renderer.clone();
|
|
445
|
+
let s = session.clone();
|
|
446
|
+
let cb = ctx.link().batch_callback(move |limits: RenderLimits| {
|
|
447
|
+
let mut msgs = vec![UpdateRenderer(Box::new(r.to_props(Some(limits))))];
|
|
448
|
+
if !limits.is_update {
|
|
449
|
+
let locator = get_current_column_locator(
|
|
450
|
+
&presentation.get_open_column_settings(),
|
|
451
|
+
&r,
|
|
452
|
+
&s.get_view_config(),
|
|
453
|
+
&s.metadata(),
|
|
454
|
+
);
|
|
455
|
+
|
|
456
|
+
msgs.push(OpenColumnSettings {
|
|
457
|
+
locator,
|
|
458
|
+
sender: None,
|
|
459
|
+
toggle: false,
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
msgs
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
*renderer.on_render_limits_changed.borrow_mut() = Some(cb);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/// Clear the active panel's session/renderer direct callbacks so a
|
|
471
|
+
/// no-longer-active panel stops driving the root's snapshots.
|
|
472
|
+
pub(super) fn clear_active_callbacks(session: &Session, renderer: &Renderer) {
|
|
473
|
+
*session.on_stats_changed.borrow_mut() = None;
|
|
474
|
+
*session.on_table_errored.borrow_mut() = None;
|
|
475
|
+
*renderer.on_render_limits_changed.borrow_mut() = None;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/// Inject element-level (shared) presentation callbacks. Created once at
|
|
479
|
+
/// construction.
|
|
480
|
+
pub(super) fn inject_shared_callbacks(ctx: &Context<PerspectiveViewer>) {
|
|
481
|
+
// Presentation: on_is_workspace_changed
|
|
482
|
+
{
|
|
483
|
+
let cb = ctx.link().callback(UpdateIsWorkspace);
|
|
484
|
+
*ctx.props()
|
|
485
|
+
.presentation
|
|
486
|
+
.on_is_workspace_changed
|
|
487
|
+
.borrow_mut() = Some(cb);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Drag/drop: on_dragstart (post-merge: lives on Presentation)
|
|
491
|
+
{
|
|
492
|
+
let presentation = ctx.props().presentation.clone();
|
|
493
|
+
let cb = ctx.link().callback(move |_: DragEffect| {
|
|
494
|
+
UpdateDragDrop(Box::new(presentation.drag_drop_props()))
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
*ctx.props().presentation.on_dragstart.borrow_mut() = Some(cb);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// Drag/drop: on_dragend
|
|
501
|
+
{
|
|
502
|
+
let cb = ctx.link().callback(|_: ()| UpdateDragDrop(Box::default()));
|
|
503
|
+
*ctx.props().presentation.on_dragend.borrow_mut() = Some(cb);
|
|
504
|
+
}
|
|
505
|
+
}
|