@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.
- 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/intl/de.css +1 -1
- package/dist/css/intl/es.css +1 -1
- package/dist/css/intl/fr.css +1 -1
- package/dist/css/intl/ja.css +1 -1
- package/dist/css/intl/pt.css +1 -1
- package/dist/css/intl/zh.css +1 -1
- package/dist/css/intl.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.d.ts +9 -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/Orientation.d.ts +4 -0
- package/dist/esm/ts-rs/PanelOptions.d.ts +7 -0
- package/dist/esm/ts-rs/PanelViewerConfig.d.ts +41 -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/ViewerConfig.d.ts +8 -2
- package/dist/esm/ts-rs/ViewerConfigUpdate.d.ts +9 -0
- package/dist/esm/ts-rs/WindowAggregate.d.ts +1 -0
- package/dist/esm/ts-rs/WindowSort.d.ts +7 -0
- package/dist/esm/ts-rs/WindowSortDir.d.ts +6 -0
- package/dist/esm/ts-rs/WindowSpec.d.ts +46 -0
- package/dist/esm/ts-rs/Windows.d.ts +10 -0
- 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 +299 -78
- package/dist/wasm/perspective-viewer.js +544 -186
- package/dist/wasm/perspective-viewer.wasm +0 -0
- package/dist/wasm/perspective-viewer.wasm.d.ts +34 -26
- package/dist/wasm/snippets/perspective-viewer-8a8c7d5d696b43ae/inline0.js +6 -0
- package/dist/wasm/snippets/{perspective-viewer-93064d0d79855d97/inline3.js → perspective-viewer-8a8c7d5d696b43ae/inline4.js} +5 -0
- package/package.json +3 -2
- package/src/css/_context_menu.css +14 -0
- package/src/css/column-selector.css +1 -0
- package/src/css/column-settings-panel.css +68 -3
- package/src/css/column-style.css +4 -3
- package/src/css/config-selector.css +22 -1
- package/src/css/containers/context-menu.css +107 -0
- package/src/css/containers/tabs.css +2 -2
- 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 +188 -70
- package/src/rust/components/column_dropdown.rs +40 -30
- package/src/rust/components/column_selector/active_column.rs +28 -45
- package/src/rust/components/column_selector/aggregate_selector.rs +4 -7
- package/src/rust/components/column_selector/column_selector_column_row.rs +73 -0
- package/src/rust/components/column_selector/config_selector.rs +40 -82
- package/src/rust/components/column_selector/expr_edit_button.rs +7 -1
- package/src/rust/components/column_selector/filter_column.rs +5 -15
- package/src/rust/components/column_selector/inactive_column.rs +11 -9
- package/src/rust/components/column_selector/sort_column.rs +3 -6
- package/src/rust/components/column_selector.rs +25 -14
- package/src/rust/components/column_settings_sidebar/style_tab.rs +2 -1
- package/src/rust/components/column_settings_sidebar/window_tab.rs +34 -0
- package/src/rust/components/column_settings_sidebar.rs +163 -16
- package/src/rust/components/containers/dragdrop_list.rs +73 -3
- package/src/rust/components/containers/sidebar.rs +99 -22
- package/src/rust/components/containers/split_panel.rs +181 -62
- package/src/rust/components/context_menu.rs +130 -0
- package/src/rust/components/editable_header.rs +2 -0
- package/src/rust/components/form/debug.rs +3 -0
- 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 +316 -71
- package/src/rust/components/mod.rs +5 -0
- package/src/rust/components/panel_menu.rs +493 -0
- package/src/rust/components/panel_tab.rs +510 -0
- package/src/rust/components/plugin_tab.rs +1 -1
- package/src/rust/components/portal.rs +7 -0
- package/src/rust/components/settings_panel.rs +13 -5
- package/src/rust/components/status_bar.rs +110 -178
- package/src/rust/components/status_indicator.rs +6 -9
- package/src/rust/components/style/mod.rs +5 -0
- package/src/rust/components/style/style_provider.rs +2 -0
- package/src/rust/components/viewer/filters.rs +133 -0
- package/src/rust/components/viewer/msg.rs +182 -0
- package/src/rust/components/viewer/panels.rs +326 -0
- package/src/rust/components/viewer/render.rs +255 -0
- package/src/rust/components/viewer/settings.rs +434 -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 +245 -756
- package/src/rust/components/window_editor.rs +1013 -0
- 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 +3 -1
- package/src/rust/custom_elements/debug_plugin.rs +20 -1
- package/src/rust/custom_elements/export_dropdown.rs +3 -1
- 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 +24 -2
- package/src/rust/presentation/column_locator.rs +16 -1
- package/src/rust/presentation.rs +66 -33
- package/src/rust/queries/columns_iter_set.rs +7 -1
- 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 +463 -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 +384 -838
- package/src/rust/session/column_defaults_update.rs +26 -8
- package/src/rust/session/drag_drop_update.rs +18 -0
- package/src/rust/session/metadata.rs +66 -9
- package/src/rust/session/replace_expression_update.rs +1 -0
- package/src/rust/session/view_subscription.rs +30 -15
- package/src/rust/session.rs +755 -264
- 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/edit_window.rs +155 -0
- package/src/rust/tasks/eject.rs +64 -23
- package/src/rust/tasks/mod.rs +30 -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/browser/dragdrop.rs +12 -0
- package/src/rust/utils/browser/tests/debounce.rs +49 -0
- package/src/rust/utils/completion.rs +56 -0
- package/src/rust/utils/custom_element.rs +5 -0
- package/src/rust/utils/debounce.rs +189 -17
- package/src/rust/utils/mod.rs +6 -0
- package/src/rust/utils/pending_effects.rs +62 -0
- package/src/rust/utils/spawn.rs +33 -0
- 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/svg/pin-icon.svg +5 -0
- package/src/themes/botanical.css +5 -2
- package/src/themes/defaults.css +54 -2
- package/src/themes/dracula.css +6 -3
- package/src/themes/gruvbox-dark.css +5 -2
- package/src/themes/gruvbox.css +5 -2
- package/src/themes/icons.css +5 -2
- package/src/themes/intl/de.css +7 -0
- package/src/themes/intl/es.css +7 -0
- package/src/themes/intl/fr.css +7 -0
- package/src/themes/intl/ja.css +7 -0
- package/src/themes/intl/pt.css +7 -0
- package/src/themes/intl/zh.css +7 -0
- package/src/themes/intl.css +10 -1
- package/src/themes/monokai.css +5 -2
- package/src/themes/phosphor.css +5 -2
- package/src/themes/pro-dark.css +5 -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 +5 -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 +16 -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 +7 -2
- package/src/ts/ts-rs/ViewerConfigUpdate.ts +9 -0
- package/src/ts/ts-rs/WindowAggregate.ts +3 -0
- package/src/ts/ts-rs/WindowSort.ts +9 -0
- package/src/ts/ts-rs/WindowSortDir.ts +8 -0
- package/src/ts/ts-rs/WindowSpec.ts +46 -0
- package/src/ts/ts-rs/Windows.ts +10 -0
- package/src/ts/ts-rs/WorkspaceConfig.ts +36 -0
- package/src/ts/ts-rs/WorkspaceConfigUpdate.ts +24 -0
- 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-93064d0d79855d97/inline0.js → perspective-viewer-8a8c7d5d696b43ae/inline1.js} +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-93064d0d79855d97/inline1.js → perspective-viewer-8a8c7d5d696b43ae/inline2.js} +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-93064d0d79855d97/inline2.js → perspective-viewer-8a8c7d5d696b43ae/inline3.js} +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-93064d0d79855d97/inline4.js → perspective-viewer-8a8c7d5d696b43ae/inline5.js} +0 -0
|
@@ -16,15 +16,18 @@ use std::cell::RefCell;
|
|
|
16
16
|
use std::rc::Rc;
|
|
17
17
|
|
|
18
18
|
use futures::channel::oneshot::channel;
|
|
19
|
+
use futures::future::join_all;
|
|
19
20
|
use js_sys::{Array, JsString};
|
|
20
21
|
use perspective_client::config::ViewConfigUpdate;
|
|
21
22
|
use perspective_client::utils::PerspectiveResultExt;
|
|
23
|
+
use perspective_js::utils::global;
|
|
22
24
|
use perspective_js::{JsViewConfig, JsViewWindow, Table, View, apierror};
|
|
23
25
|
use wasm_bindgen::JsCast;
|
|
24
26
|
use wasm_bindgen::prelude::*;
|
|
25
27
|
use wasm_bindgen_derive::try_from_js_option;
|
|
26
28
|
use wasm_bindgen_futures::JsFuture;
|
|
27
29
|
use web_sys::HtmlElement;
|
|
30
|
+
use yew::Callback;
|
|
28
31
|
|
|
29
32
|
use crate::components::viewer::{PerspectiveViewerMsg, PerspectiveViewerProps};
|
|
30
33
|
use crate::config::*;
|
|
@@ -32,20 +35,59 @@ use crate::custom_events::*;
|
|
|
32
35
|
use crate::js::*;
|
|
33
36
|
use crate::presentation::*;
|
|
34
37
|
use crate::queries::*;
|
|
35
|
-
use crate::renderer::*;
|
|
36
38
|
use crate::root::Root;
|
|
37
|
-
use crate::session::{ResetOptions,
|
|
39
|
+
use crate::session::{ResetOptions, TableLoadState};
|
|
38
40
|
use crate::tasks::*;
|
|
39
41
|
use crate::utils::*;
|
|
42
|
+
use crate::workspace::{Panel, PanelId, Workspace};
|
|
40
43
|
use crate::*;
|
|
41
44
|
|
|
42
45
|
#[wasm_bindgen]
|
|
43
46
|
extern "C" {
|
|
47
|
+
/// `load()` argument: a [`Client`], a (deprecated) [`Table`], or a
|
|
48
|
+
/// `Promise` resolving to either. Typed rather than `any` so callers get
|
|
49
|
+
/// completion; the `Table` forms remain runtime-deprecated.
|
|
50
|
+
#[wasm_bindgen(typescript_type = "Client | Table | Promise<Client | Table>")]
|
|
51
|
+
pub type JsClientLoad;
|
|
52
|
+
|
|
53
|
+
/// `eject()` argument dict (`{ client?: string }`).
|
|
54
|
+
#[wasm_bindgen(typescript_type = "ClientOptions")]
|
|
55
|
+
pub type JsClientOptions;
|
|
56
|
+
|
|
57
|
+
/// Panel-selector dict (`{ panel?: string }`) for the active/base
|
|
58
|
+
/// accessor methods.
|
|
59
|
+
#[wasm_bindgen(typescript_type = "PanelOptions")]
|
|
60
|
+
pub type JsPanelOptions;
|
|
61
|
+
|
|
62
|
+
/// `download`/`export`/`copy` options dict
|
|
63
|
+
/// (`{ method?: ExportMethod, panel?: string }`).
|
|
64
|
+
#[wasm_bindgen(typescript_type = "ExportOptions")]
|
|
65
|
+
pub type JsExportOptions;
|
|
66
|
+
|
|
67
|
+
/// `getTable` options dict (`{ wait?: boolean, panel?: string }`).
|
|
68
|
+
#[wasm_bindgen(typescript_type = "GetTableOptions")]
|
|
69
|
+
pub type JsGetTableOptions;
|
|
70
|
+
|
|
71
|
+
/// `getClient` options dict (`{ wait?: boolean, panel?: string }`).
|
|
72
|
+
#[wasm_bindgen(typescript_type = "GetClientOptions")]
|
|
73
|
+
pub type JsGetClientOptions;
|
|
74
|
+
|
|
75
|
+
/// `restoreWorkspace()` argument: a whole-element config update.
|
|
76
|
+
#[wasm_bindgen(typescript_type = "WorkspaceConfigUpdate")]
|
|
77
|
+
pub type JsWorkspaceConfigUpdate;
|
|
78
|
+
|
|
79
|
+
/// `saveWorkspace()` return: a whole-element config.
|
|
80
|
+
#[wasm_bindgen(typescript_type = "Promise<WorkspaceConfig>")]
|
|
81
|
+
pub type JsWorkspaceConfigPromise;
|
|
82
|
+
|
|
83
|
+
/// A `Promise<void>` return, used by the `restore` family (whose
|
|
84
|
+
/// `ApiFuture<()>` would otherwise erase to `Promise<any>`).
|
|
85
|
+
#[wasm_bindgen(typescript_type = "Promise<void>")]
|
|
86
|
+
pub type JsVoidPromise;
|
|
87
|
+
|
|
88
|
+
/// `save()` return: a single-panel config.
|
|
44
89
|
#[wasm_bindgen(typescript_type = "Promise<ViewerConfig>")]
|
|
45
90
|
pub type JsViewerConfigPromise;
|
|
46
|
-
|
|
47
|
-
#[wasm_bindgen(typescript_type = "ViewerConfigUpdate")]
|
|
48
|
-
pub type JsViewerConfigUpdate;
|
|
49
91
|
}
|
|
50
92
|
|
|
51
93
|
#[derive(serde::Deserialize, Default)]
|
|
@@ -59,6 +101,20 @@ struct ResizeDimensions {
|
|
|
59
101
|
height: f64,
|
|
60
102
|
}
|
|
61
103
|
|
|
104
|
+
/// Leniently deserialize an optional JS options dict into a serde struct,
|
|
105
|
+
/// falling back to `Default` on absence or a malformed argument (matching the
|
|
106
|
+
/// `ResizeOptions` precedent — an options bag is a best-effort convenience,
|
|
107
|
+
/// not a hard-validated payload).
|
|
108
|
+
fn parse_options<T, U>(options: Option<T>) -> U
|
|
109
|
+
where
|
|
110
|
+
T: Into<JsValue>,
|
|
111
|
+
U: Default + for<'a> serde::Deserialize<'a>,
|
|
112
|
+
{
|
|
113
|
+
options
|
|
114
|
+
.and_then(|o| o.into_serde_ext().ok())
|
|
115
|
+
.unwrap_or_default()
|
|
116
|
+
}
|
|
117
|
+
|
|
62
118
|
/// The `<perspective-viewer>` custom element.
|
|
63
119
|
///
|
|
64
120
|
/// # JavaScript Examples
|
|
@@ -86,13 +142,13 @@ struct ResizeDimensions {
|
|
|
86
142
|
#[derive(Clone)]
|
|
87
143
|
#[wasm_bindgen]
|
|
88
144
|
pub struct PerspectiveViewerElement {
|
|
89
|
-
elem: HtmlElement,
|
|
90
|
-
root: Root<components::viewer::PerspectiveViewer>,
|
|
91
|
-
resize_handle: Rc<RefCell<Option<ResizeObserverHandle>>>,
|
|
92
|
-
intersection_handle: Rc<RefCell<Option<IntersectionObserverHandle>>>,
|
|
93
|
-
pub(crate) session: Session,
|
|
94
|
-
pub(crate) renderer: Renderer,
|
|
95
145
|
pub(crate) presentation: Presentation,
|
|
146
|
+
pub(crate) workspace: Workspace,
|
|
147
|
+
pub(crate) elem: HtmlElement,
|
|
148
|
+
pub(crate) root: Root<components::viewer::PerspectiveViewer>,
|
|
149
|
+
resize_handle: Rc<RefCell<Option<ResizeObserverHandle>>>,
|
|
150
|
+
intersection_handle: Rc<RefCell<Option<AutoPauseHandle>>>,
|
|
151
|
+
hosted_table_subs: HostedTableSubs,
|
|
96
152
|
_subscriptions: Rc<[Subscription; 2]>,
|
|
97
153
|
_custom_event_subs: Rc<Vec<Subscription>>,
|
|
98
154
|
}
|
|
@@ -103,6 +159,111 @@ impl CustomElementMetadata for PerspectiveViewerElement {
|
|
|
103
159
|
["registerPlugin", "get_wasm_module", "get_worker_url"].as_slice();
|
|
104
160
|
}
|
|
105
161
|
|
|
162
|
+
impl PerspectiveViewerElement {
|
|
163
|
+
fn layout_changed_notify(&self) -> Callback<()> {
|
|
164
|
+
let root = self.root.clone();
|
|
165
|
+
Callback::from(move |_: ()| {
|
|
166
|
+
if let Some(app) = root.borrow().as_ref() {
|
|
167
|
+
app.send_message(PerspectiveViewerMsg::LayoutChanged);
|
|
168
|
+
}
|
|
169
|
+
})
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
fn resolve_panel(&self, name: Option<String>) -> ApiResult<Panel> {
|
|
173
|
+
let id = name.map(PanelId::from);
|
|
174
|
+
self.workspace.panel_or_active(id.as_ref()).ok_or_else(|| {
|
|
175
|
+
format!(
|
|
176
|
+
"No panel named \"{}\"",
|
|
177
|
+
id.as_ref().map(PanelId::as_str).unwrap_or_default()
|
|
178
|
+
)
|
|
179
|
+
.into()
|
|
180
|
+
})
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
fn layout_element(&self) -> Option<RegularLayout> {
|
|
184
|
+
self.elem
|
|
185
|
+
.shadow_root()?
|
|
186
|
+
.query_selector(RegularLayout::TAG_NAME)
|
|
187
|
+
.ok()
|
|
188
|
+
.flatten()
|
|
189
|
+
.map(|el| el.unchecked_into())
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async fn workspace_config(this: Self) -> ApiResult<JsValue> {
|
|
193
|
+
let mut panels: std::collections::BTreeMap<String, PanelViewerConfig> = Default::default();
|
|
194
|
+
for id in &this.workspace.panel_ids() {
|
|
195
|
+
let panel = this.workspace.panel(id).into_apierror()?;
|
|
196
|
+
let config = panel
|
|
197
|
+
.renderer
|
|
198
|
+
.clone()
|
|
199
|
+
.with_lock(async {
|
|
200
|
+
get_viewer_config(&panel.session, &panel.renderer, &this.presentation).await
|
|
201
|
+
})
|
|
202
|
+
.await?;
|
|
203
|
+
|
|
204
|
+
panels.insert(id.as_str().to_owned(), config.panel);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
let active = this
|
|
208
|
+
.presentation
|
|
209
|
+
.is_settings_open()
|
|
210
|
+
.then(|| this.workspace.active_id())
|
|
211
|
+
.flatten()
|
|
212
|
+
.map(|id| id.as_str().to_owned());
|
|
213
|
+
|
|
214
|
+
let layout = this
|
|
215
|
+
.layout_element()
|
|
216
|
+
.map(|l| l.save().into_serde_ext::<crate::js::Layout>())
|
|
217
|
+
.transpose()?;
|
|
218
|
+
|
|
219
|
+
Ok(JsValue::from_serde_ext(&WorkspaceConfig {
|
|
220
|
+
version: API_VERSION.to_string(),
|
|
221
|
+
active,
|
|
222
|
+
layout,
|
|
223
|
+
panels,
|
|
224
|
+
global_filters: this.workspace.global_filters(),
|
|
225
|
+
masters: this
|
|
226
|
+
.workspace
|
|
227
|
+
.masters()
|
|
228
|
+
.iter()
|
|
229
|
+
.map(|id| id.as_str().to_owned())
|
|
230
|
+
.collect(),
|
|
231
|
+
})?)
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
fn eject_client_panels(
|
|
236
|
+
workspace: &Workspace,
|
|
237
|
+
root: &Root<crate::components::viewer::PerspectiveViewer>,
|
|
238
|
+
target: String,
|
|
239
|
+
ids: Vec<PanelId>,
|
|
240
|
+
) -> ApiFuture<()> {
|
|
241
|
+
clone!(workspace, root);
|
|
242
|
+
let effect = workspace.effects().guard();
|
|
243
|
+
ApiFuture::new_throttled(async move {
|
|
244
|
+
let _effect = effect;
|
|
245
|
+
for id in ids {
|
|
246
|
+
let (completion, receiver) = Completion::new();
|
|
247
|
+
root.borrow()
|
|
248
|
+
.as_ref()
|
|
249
|
+
.into_apierror()?
|
|
250
|
+
.send_message(PerspectiveViewerMsg::ClosePanel(
|
|
251
|
+
id.to_string(),
|
|
252
|
+
Some(completion),
|
|
253
|
+
));
|
|
254
|
+
|
|
255
|
+
receiver.await.map_err(|_| ApiError::new("Cancelled"))??;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
workspace.remove_client(&target);
|
|
259
|
+
Ok(())
|
|
260
|
+
})
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
#[rustfmt::skip]
|
|
264
|
+
const DEPRECATED_TABLE_MESSAGE: &str =
|
|
265
|
+
"`load(table)` is deprecated - use `load(client)` followed by `restore({table: \"name\"})` instead";
|
|
266
|
+
|
|
106
267
|
#[wasm_bindgen]
|
|
107
268
|
impl PerspectiveViewerElement {
|
|
108
269
|
#[doc(hidden)]
|
|
@@ -118,58 +279,58 @@ impl PerspectiveViewerElement {
|
|
|
118
279
|
}
|
|
119
280
|
|
|
120
281
|
fn new_from_shadow(elem: web_sys::HtmlElement, shadow_root: web_sys::Element) -> Self {
|
|
121
|
-
// Application State
|
|
122
|
-
let session = Session::new();
|
|
123
|
-
let renderer = Renderer::new(&elem);
|
|
282
|
+
// Application State.
|
|
124
283
|
let presentation = Presentation::new(&elem);
|
|
125
|
-
|
|
284
|
+
|
|
285
|
+
// Boot with ZERO panels — an unconfigured element is a blank stage. The
|
|
286
|
+
// first `load`/`restore`/`addPanel` creates the first panel, which
|
|
287
|
+
// adopts the element's `theme` attribute when set (see
|
|
288
|
+
// `create_panel_model`'s authored-theme boot).
|
|
289
|
+
let workspace = Workspace::new();
|
|
290
|
+
let custom_event_subs = wire_element_events(&elem, &presentation, &workspace);
|
|
126
291
|
|
|
127
292
|
// Create Yew App
|
|
128
293
|
let props = yew::props!(PerspectiveViewerProps {
|
|
129
294
|
elem: elem.clone(),
|
|
130
|
-
session: session.clone(),
|
|
131
|
-
renderer: renderer.clone(),
|
|
132
295
|
presentation: presentation.clone(),
|
|
296
|
+
workspace: workspace.clone(),
|
|
133
297
|
});
|
|
134
298
|
|
|
135
299
|
let state = props.clone();
|
|
136
300
|
let root = Root::new(shadow_root, props);
|
|
137
301
|
|
|
138
302
|
// Create callbacks
|
|
139
|
-
let
|
|
140
|
-
clone
|
|
303
|
+
let eject_sub = presentation.on_eject.add_listener({
|
|
304
|
+
let root = root.clone();
|
|
141
305
|
move |_| {
|
|
142
|
-
clone!(
|
|
306
|
+
clone!(state.workspace, root);
|
|
143
307
|
ApiFuture::spawn(async move {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
.
|
|
147
|
-
.
|
|
148
|
-
|
|
308
|
+
if let Some(target) = workspace.active_client().map(|c| c.get_name().to_owned())
|
|
309
|
+
{
|
|
310
|
+
let ids = workspace.panels_for_client(&target);
|
|
311
|
+
if ids.len() < workspace.panel_ids().len() {
|
|
312
|
+
return eject_client_panels(&workspace, &root, target, ids).await;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
delete_all(&workspace, &root).await
|
|
149
317
|
})
|
|
150
318
|
}
|
|
151
319
|
});
|
|
152
320
|
|
|
153
|
-
let
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
let resize_handle =
|
|
159
|
-
ResizeObserverHandle::new(&elem, &renderer, &session, &presentation, &root);
|
|
160
|
-
|
|
161
|
-
let intersect_handle =
|
|
162
|
-
IntersectionObserverHandle::new(&elem, &presentation, &session, &renderer);
|
|
321
|
+
let resize_handle = ResizeObserverHandle::new(&elem, &workspace, &presentation, &root);
|
|
322
|
+
let intersect_handle = AutoPauseHandle::new(&elem, &presentation, &workspace);
|
|
323
|
+
let (lifecycle_sub, hosted_table_subs) = wire_table_lifecycle(&workspace, &presentation);
|
|
163
324
|
|
|
164
325
|
Self {
|
|
165
326
|
elem,
|
|
166
327
|
root,
|
|
167
|
-
session,
|
|
168
|
-
renderer,
|
|
169
328
|
presentation,
|
|
329
|
+
workspace,
|
|
170
330
|
resize_handle: Rc::new(RefCell::new(Some(resize_handle))),
|
|
171
331
|
intersection_handle: Rc::new(RefCell::new(Some(intersect_handle))),
|
|
172
|
-
|
|
332
|
+
hosted_table_subs,
|
|
333
|
+
_subscriptions: Rc::new([eject_sub, lifecycle_sub]),
|
|
173
334
|
_custom_event_subs: Rc::new(custom_event_subs),
|
|
174
335
|
}
|
|
175
336
|
}
|
|
@@ -255,69 +416,206 @@ impl PerspectiveViewerElement {
|
|
|
255
416
|
/// const table = await worker.table("x\n1", {name: "table_one"});
|
|
256
417
|
/// await viewer.load(table);
|
|
257
418
|
/// ```
|
|
258
|
-
pub fn load(&self,
|
|
419
|
+
pub fn load(&self, client: JsClientLoad) -> ApiResult<ApiFuture<()>> {
|
|
420
|
+
let effect = self.workspace.effects().guard();
|
|
421
|
+
let table: JsValue = client.into();
|
|
259
422
|
let promise = table
|
|
260
423
|
.clone()
|
|
261
424
|
.dyn_into::<js_sys::Promise>()
|
|
262
425
|
.unwrap_or_else(|_| js_sys::Promise::resolve(&table));
|
|
263
426
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
427
|
+
// Resolve the target panel. On an EMPTY element (zero panels):
|
|
428
|
+
// - a synchronously-detectable `Client` registers inertly with NO panel (the
|
|
429
|
+
// common `load(client)` — no phantom panel is left behind);
|
|
430
|
+
// - otherwise (a resolved `Table`, or a `Promise` whose type isn't yet known)
|
|
431
|
+
// the first panel is RESERVED synchronously here — a full panel model held
|
|
432
|
+
// in the workspace's reservation slot, NOT placed — so its ordering position
|
|
433
|
+
// is fixed at the call site: a `restore()` fired right after an unawaited
|
|
434
|
+
// `load()` CLAIMS the reservation (placing it) and targets THIS panel, not a
|
|
435
|
+
// second one. The reservation is likewise placed when the payload proves to
|
|
436
|
+
// be a `Table` (or the load fails, surfacing its error), and discarded —
|
|
437
|
+
// only while still unclaimed — for an inert `Client` payload. Placement and
|
|
438
|
+
// discard are both atomic slot transfers (`Workspace::claim_reserved` /
|
|
439
|
+
// `Workspace::take_reserved`), so an inert payload disposing a panel a
|
|
440
|
+
// racing `restore` claimed is unrepresentable.
|
|
441
|
+
// A pre-existing active panel is used as-is (a `Client` registers
|
|
442
|
+
// inertly against it, never clearing its table).
|
|
443
|
+
let (panel, notify) = match self.workspace.active_panel() {
|
|
444
|
+
Some(panel) => (panel, None),
|
|
445
|
+
None => {
|
|
446
|
+
// Empty element — classify the payload synchronously where possible.
|
|
447
|
+
if let Ok(Some(client)) =
|
|
448
|
+
try_from_js_option::<perspective_js::Client>(table.clone())
|
|
283
449
|
{
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
450
|
+
// A resolved `Client` registers SYNCHRONOUSLY (so an unawaited
|
|
451
|
+
// `load(client)` is visible to a `restore()` fired right after,
|
|
452
|
+
// which creates the first panel and federates against loaded
|
|
453
|
+
// clients) and creates NO panel — inert.
|
|
454
|
+
self.workspace
|
|
455
|
+
.set_default_client(client.get_client().clone());
|
|
456
|
+
return Ok(ApiFuture::new(async { Ok(()) }));
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// A resolved `Table` (or a `Promise` whose type isn't yet known)
|
|
460
|
+
// adopts the pending reservation (a second `load()` on a
|
|
461
|
+
// still-empty element), else reserves a fresh panel model.
|
|
462
|
+
let panel = self.workspace.reserved_panel().unwrap_or_else(|| {
|
|
463
|
+
create_panel_model(
|
|
464
|
+
&self.elem,
|
|
465
|
+
&self.presentation,
|
|
466
|
+
&self.workspace,
|
|
467
|
+
None,
|
|
468
|
+
ViewerConfigUpdate::default(),
|
|
469
|
+
None,
|
|
470
|
+
Placement::Reserved,
|
|
291
471
|
);
|
|
472
|
+
self.workspace
|
|
473
|
+
.reserved_panel()
|
|
474
|
+
.expect("just-reserved panel is present")
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
// Carrying `Some(notify)` marks this load as the reservation's
|
|
478
|
+
// owner — the only call that may place or discard it below.
|
|
479
|
+
(panel, Some(self.layout_changed_notify()))
|
|
480
|
+
},
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
// A `Table` payload targets this panel's engines; a `Client` registers
|
|
484
|
+
// inertly against it. Selecting the panel here (not at construction)
|
|
485
|
+
// keeps the registry race safe — by `load()` time real plugins have
|
|
486
|
+
// registered.
|
|
487
|
+
let session = panel.session;
|
|
488
|
+
let renderer = panel.renderer;
|
|
489
|
+
|
|
490
|
+
// Open the pending-load window SYNCHRONOUSLY, at the call site — this
|
|
491
|
+
// is what fixes the ordering. The payload's RESET disposition (a
|
|
492
|
+
// `Table` resets the view; a `Client` does not) is unknown until the
|
|
493
|
+
// promise resolves, but the window's POSITION on the config-commit
|
|
494
|
+
// stream is fixed NOW. A `restore()` a caller fires immediately after
|
|
495
|
+
// this unawaited `load()` (the React prop-binding pattern, which has
|
|
496
|
+
// no async ordering guarantees) commits INTO this window's journal and
|
|
497
|
+
// is replayed over the reset base if the payload proves to be a
|
|
498
|
+
// `Table` — so a moved-async reset can no longer clobber a later
|
|
499
|
+
// commit. See `SESSION_CONFIG_COHERENCE_PLAN.md`.
|
|
500
|
+
let generation = session.begin_pending_load();
|
|
501
|
+
|
|
502
|
+
clone!(self.workspace, self.presentation);
|
|
503
|
+
Ok(ApiFuture::new_throttled(async move {
|
|
504
|
+
let _effect = effect;
|
|
505
|
+
renderer.set_throttle(None);
|
|
506
|
+
let _run_token = session.begin_config_run();
|
|
507
|
+
let result = {
|
|
508
|
+
clone!(session, renderer, workspace, notify);
|
|
509
|
+
renderer
|
|
510
|
+
.clone()
|
|
511
|
+
.render_task(|guard| async move {
|
|
512
|
+
renderer.set_default_theme(presentation.get_default_theme_name().await);
|
|
513
|
+
renderer.stamp_theme(None);
|
|
514
|
+
let jstable = JsFuture::from(promise)
|
|
515
|
+
.await
|
|
516
|
+
.map_err(|x| apierror!(TableError(x)))?;
|
|
517
|
+
|
|
518
|
+
if let Ok(Some(table)) =
|
|
519
|
+
try_from_js_option::<perspective_js::Table>(jstable.clone())
|
|
520
|
+
{
|
|
521
|
+
tracing::warn!("{}", DEPRECATED_TABLE_MESSAGE);
|
|
522
|
+
let Some(journal) = session.take_pending_load(generation) else {
|
|
523
|
+
return Ok(None);
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
if let Some(notify) = ¬ify {
|
|
527
|
+
place_reserved(&workspace, notify);
|
|
528
|
+
}
|
|
292
529
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
530
|
+
let _plugin = renderer.ensure_plugin_selected()?;
|
|
531
|
+
let _ = renderer.mount_active_plugin();
|
|
532
|
+
session
|
|
533
|
+
.reset(ResetOptions {
|
|
534
|
+
config: true,
|
|
535
|
+
expressions: true,
|
|
536
|
+
stats: true,
|
|
537
|
+
table: Some(session::TableIntermediateState::Reloaded),
|
|
538
|
+
})
|
|
539
|
+
.await
|
|
540
|
+
.unwrap_or_log();
|
|
541
|
+
|
|
542
|
+
let client = table.get_client().await;
|
|
543
|
+
let inner_client = client.get_client().clone();
|
|
544
|
+
session.set_client(inner_client.clone());
|
|
545
|
+
workspace.set_default_client(inner_client);
|
|
546
|
+
let name = table.get_name().await;
|
|
547
|
+
tracing::debug!(
|
|
548
|
+
"Loading {:.0} rows from `Table` {}",
|
|
549
|
+
table.size().await?,
|
|
550
|
+
name
|
|
551
|
+
);
|
|
552
|
+
|
|
553
|
+
session.set_table(name).await?;
|
|
554
|
+
for delta in journal {
|
|
555
|
+
session.commit_view_config(delta)?;
|
|
556
|
+
}
|
|
296
557
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
558
|
+
session.commit_table_defaults();
|
|
559
|
+
let (disposition, _pin) =
|
|
560
|
+
crate::tasks::bind_snapshot(&guard, &session, &renderer).await?;
|
|
561
|
+
|
|
562
|
+
crate::tasks::dispatch_bound(
|
|
563
|
+
&guard,
|
|
564
|
+
&renderer,
|
|
565
|
+
disposition,
|
|
566
|
+
false,
|
|
567
|
+
crate::tasks::RunOrigin::Public,
|
|
568
|
+
)
|
|
569
|
+
.await?;
|
|
570
|
+
|
|
571
|
+
Ok(None)
|
|
572
|
+
} else if let Ok(Some(client)) = wasm_bindgen_derive::try_from_js_option::<
|
|
573
|
+
perspective_js::Client,
|
|
574
|
+
>(jstable)
|
|
575
|
+
{
|
|
576
|
+
// INERT: register the client only — never rebind or
|
|
577
|
+
// reset the active panel (its table is preserved).
|
|
578
|
+
// Panels bind their client lazily at table-resolution
|
|
579
|
+
// time (`Workspace::resolve_client_for_table`). The
|
|
580
|
+
// window is discarded (not replayed): a `Client`
|
|
581
|
+
// performs no reset, and any racing `restore`'s
|
|
582
|
+
// commits already applied live (`commit_view_config`).
|
|
583
|
+
let owned_window = session.take_pending_load(generation).is_some();
|
|
584
|
+
let discard = if owned_window && notify.is_some() {
|
|
585
|
+
workspace.take_reserved()
|
|
586
|
+
} else {
|
|
587
|
+
None
|
|
588
|
+
};
|
|
589
|
+
|
|
590
|
+
workspace.set_default_client(client.get_client().clone());
|
|
591
|
+
Ok(discard)
|
|
592
|
+
} else {
|
|
593
|
+
session.take_pending_load(generation);
|
|
594
|
+
Err(ApiError::new("Invalid argument"))
|
|
595
|
+
}
|
|
596
|
+
})
|
|
597
|
+
.await
|
|
306
598
|
};
|
|
307
599
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
600
|
+
match result {
|
|
601
|
+
Err(e) => {
|
|
602
|
+
session.take_pending_load(generation);
|
|
603
|
+
if let Some(notify) = ¬ify {
|
|
604
|
+
place_reserved(&workspace, notify);
|
|
605
|
+
}
|
|
313
606
|
|
|
314
|
-
|
|
607
|
+
session.set_error(false, e.clone()).await?;
|
|
608
|
+
Err(e)
|
|
609
|
+
},
|
|
610
|
+
Ok(Some(panel)) => eject_panel(panel).await,
|
|
611
|
+
Ok(None) => Ok(()),
|
|
612
|
+
}
|
|
315
613
|
}))
|
|
316
614
|
}
|
|
317
615
|
|
|
318
|
-
/// Delete
|
|
616
|
+
/// Delete all internal [`View`]s and all associated state, rendering this
|
|
319
617
|
/// `<perspective-viewer>` unusable and freeing all associated resources.
|
|
320
|
-
/// Does not delete
|
|
618
|
+
/// Does not delete any supplied [`Table`] (as this is constructed by the
|
|
321
619
|
/// callee).
|
|
322
620
|
///
|
|
323
621
|
/// Calling _any_ method on a `<perspective-viewer>` after [`Self::delete`]
|
|
@@ -337,27 +635,67 @@ impl PerspectiveViewerElement {
|
|
|
337
635
|
/// await viewer.delete();
|
|
338
636
|
/// ```
|
|
339
637
|
pub fn delete(self) -> ApiFuture<()> {
|
|
340
|
-
|
|
638
|
+
let subs = std::mem::take(&mut *self.hosted_table_subs.borrow_mut());
|
|
639
|
+
let teardown = delete_all(&self.workspace, &self.root);
|
|
640
|
+
ApiFuture::new(async move {
|
|
641
|
+
for (client, id) in subs {
|
|
642
|
+
let _ = client.remove_hosted_tables_update(id).await;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
teardown.await
|
|
646
|
+
})
|
|
341
647
|
}
|
|
342
648
|
|
|
343
|
-
///
|
|
344
|
-
/// `
|
|
649
|
+
/// Remove a [`Client`] from this `<perspective-viewer>` and dispose every
|
|
650
|
+
/// panel bound to it (each panel's `View` is deleted and its `Table`
|
|
651
|
+
/// reference released).
|
|
652
|
+
///
|
|
653
|
+
/// # Arguments
|
|
345
654
|
///
|
|
346
|
-
///
|
|
347
|
-
///
|
|
348
|
-
///
|
|
349
|
-
|
|
350
|
-
|
|
655
|
+
/// - `options` - An optional `{client?: string}` dict naming the client to
|
|
656
|
+
/// eject; the active panel's client when omitted.
|
|
657
|
+
///
|
|
658
|
+
/// # JavaScript Examples
|
|
659
|
+
///
|
|
660
|
+
/// ```javascript
|
|
661
|
+
/// await viewer.eject();
|
|
662
|
+
/// await viewer.eject({client: "remote"});
|
|
663
|
+
/// ```
|
|
664
|
+
pub fn eject(&mut self, options: Option<JsClientOptions>) -> ApiFuture<()> {
|
|
665
|
+
let ClientOptions { client } = parse_options(options);
|
|
666
|
+
// Default target: the active panel's client, or — when the active panel
|
|
667
|
+
// is unbound (`load(Client)` is now inert) — the default client.
|
|
668
|
+
let Some(target) = client
|
|
669
|
+
.or_else(|| {
|
|
670
|
+
self.workspace
|
|
671
|
+
.active_client()
|
|
672
|
+
.map(|c| c.get_name().to_owned())
|
|
673
|
+
})
|
|
674
|
+
.or_else(|| {
|
|
675
|
+
self.workspace
|
|
676
|
+
.default_client()
|
|
677
|
+
.map(|c| c.get_name().to_owned())
|
|
678
|
+
})
|
|
679
|
+
else {
|
|
680
|
+
return ApiFuture::new_throttled(async move { Ok(()) });
|
|
681
|
+
};
|
|
682
|
+
|
|
683
|
+
let ids = self.workspace.panels_for_client(&target);
|
|
684
|
+
|
|
685
|
+
// The target client backs EVERY panel — reset the element to its
|
|
686
|
+
// pre-`load` state (dropping the client with it), as a `Workspace`
|
|
687
|
+
// must always keep at least one panel.
|
|
688
|
+
if !ids.is_empty() && ids.len() == self.workspace.panel_ids().len() {
|
|
351
689
|
let mut state = Self::new_from_shadow(
|
|
352
690
|
self.elem.clone(),
|
|
353
691
|
self.elem.shadow_root().unwrap().unchecked_into(),
|
|
354
692
|
);
|
|
355
693
|
|
|
356
694
|
std::mem::swap(self, &mut state);
|
|
357
|
-
ApiFuture::new_throttled(state.delete())
|
|
358
|
-
} else {
|
|
359
|
-
ApiFuture::new_throttled(async move { Ok(()) })
|
|
695
|
+
return ApiFuture::new_throttled(state.delete());
|
|
360
696
|
}
|
|
697
|
+
|
|
698
|
+
eject_client_panels(&self.workspace, &self.root, target, ids)
|
|
361
699
|
}
|
|
362
700
|
|
|
363
701
|
/// Get the underlying [`View`] for this viewer.
|
|
@@ -380,9 +718,13 @@ impl PerspectiveViewerElement {
|
|
|
380
718
|
/// const view = await viewer.getView();
|
|
381
719
|
/// ```
|
|
382
720
|
#[wasm_bindgen]
|
|
383
|
-
pub fn getView(&self) -> ApiFuture<View> {
|
|
384
|
-
let
|
|
385
|
-
|
|
721
|
+
pub fn getView(&self, options: Option<JsPanelOptions>) -> ApiFuture<View> {
|
|
722
|
+
let PanelOptions { panel: name } = parse_options(options);
|
|
723
|
+
let this = self.clone();
|
|
724
|
+
ApiFuture::new(async move {
|
|
725
|
+
let panel = this.resolve_panel(name)?;
|
|
726
|
+
Ok(panel.session.get_view().ok_or("No table set")?.into())
|
|
727
|
+
})
|
|
386
728
|
}
|
|
387
729
|
|
|
388
730
|
/// Get a copy of the [`ViewConfig`] for the current [`View`]. This is
|
|
@@ -390,21 +732,18 @@ impl PerspectiveViewerElement {
|
|
|
390
732
|
/// [`PerspectiveViewerElement::save`]), and also makes no API calls to the
|
|
391
733
|
/// server (unlike [`PerspectiveViewerElement::getView`] followed by
|
|
392
734
|
/// [`View::get_config`])
|
|
393
|
-
///
|
|
394
|
-
/// Returns the [`ViewConfig`] the currently-bound `View` was constructed
|
|
395
|
-
/// from, so the value is consistent with what the active plugin is
|
|
396
|
-
/// rendering even if a queued [`Self::restore`]/`update_and_render` has
|
|
397
|
-
/// already mutated the live config in anticipation of the next draw.
|
|
398
|
-
/// Falls back to the live session config when no `View` has yet been
|
|
399
|
-
/// created (e.g., after `load` but before the first render).
|
|
400
735
|
#[wasm_bindgen]
|
|
401
|
-
pub fn getViewConfig(&self) -> ApiFuture<JsViewConfig> {
|
|
402
|
-
let
|
|
736
|
+
pub fn getViewConfig(&self, options: Option<JsPanelOptions>) -> ApiFuture<JsViewConfig> {
|
|
737
|
+
let PanelOptions { panel: name } = parse_options(options);
|
|
738
|
+
let this = self.clone();
|
|
403
739
|
ApiFuture::new(async move {
|
|
404
|
-
let
|
|
740
|
+
let panel = this.resolve_panel(name)?;
|
|
741
|
+
let config = if let Some(ctx) = panel.renderer.render_context() {
|
|
742
|
+
(*ctx.view_config).clone()
|
|
743
|
+
} else if let Some(rendered) = panel.session.get_rendered_view_config() {
|
|
405
744
|
(*rendered).clone()
|
|
406
745
|
} else {
|
|
407
|
-
session.get_view_config().clone()
|
|
746
|
+
panel.session.get_view_config().clone()
|
|
408
747
|
};
|
|
409
748
|
|
|
410
749
|
Ok(JsValue::from_serde_ext(&config)?.unchecked_into())
|
|
@@ -427,9 +766,21 @@ impl PerspectiveViewerElement {
|
|
|
427
766
|
/// const table = await viewer.getTable();
|
|
428
767
|
/// ```
|
|
429
768
|
#[wasm_bindgen]
|
|
430
|
-
pub fn getTable(&self,
|
|
431
|
-
let
|
|
769
|
+
pub fn getTable(&self, options: Option<JsGetTableOptions>) -> ApiFuture<Table> {
|
|
770
|
+
let GetTableOptions {
|
|
771
|
+
wait: wait_for_table,
|
|
772
|
+
panel: name,
|
|
773
|
+
} = parse_options(options);
|
|
774
|
+
let this = self.clone();
|
|
432
775
|
ApiFuture::new(async move {
|
|
776
|
+
let panel = this.resolve_panel(name)?;
|
|
777
|
+
if !wait_for_table.unwrap_or_default()
|
|
778
|
+
&& let Some(ctx) = panel.renderer.render_context()
|
|
779
|
+
{
|
|
780
|
+
return Ok(ctx.table.clone().into());
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
let session = panel.session;
|
|
433
784
|
match session.get_table() {
|
|
434
785
|
Some(table) => Ok(table.into()),
|
|
435
786
|
None if !wait_for_table.unwrap_or_default() => Err("No `Table` set".into()),
|
|
@@ -457,9 +808,24 @@ impl PerspectiveViewerElement {
|
|
|
457
808
|
/// const client = await viewer.getClient();
|
|
458
809
|
/// ```
|
|
459
810
|
#[wasm_bindgen]
|
|
460
|
-
pub fn getClient(
|
|
461
|
-
|
|
811
|
+
pub fn getClient(
|
|
812
|
+
&self,
|
|
813
|
+
options: Option<JsGetClientOptions>,
|
|
814
|
+
) -> ApiFuture<perspective_js::Client> {
|
|
815
|
+
let GetClientOptions {
|
|
816
|
+
wait: wait_for_client,
|
|
817
|
+
panel: name,
|
|
818
|
+
} = parse_options(options);
|
|
819
|
+
let this = self.clone();
|
|
462
820
|
ApiFuture::new(async move {
|
|
821
|
+
let panel = this.resolve_panel(name)?;
|
|
822
|
+
if !wait_for_client.unwrap_or_default()
|
|
823
|
+
&& let Some(ctx) = panel.renderer.render_context()
|
|
824
|
+
{
|
|
825
|
+
return Ok(ctx.client.clone().into());
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
let session = panel.session;
|
|
463
829
|
match session.get_client() {
|
|
464
830
|
Some(client) => Ok(client.into()),
|
|
465
831
|
None if !wait_for_client.unwrap_or_default() => Err("No `Client` set".into()),
|
|
@@ -481,9 +847,11 @@ impl PerspectiveViewerElement {
|
|
|
481
847
|
/// const {virtual_fps, actual_fps} = await viewer.getRenderStats();
|
|
482
848
|
/// ```
|
|
483
849
|
#[wasm_bindgen]
|
|
484
|
-
pub fn getRenderStats(&self) -> ApiResult<JsValue> {
|
|
850
|
+
pub fn getRenderStats(&self, options: Option<JsPanelOptions>) -> ApiResult<JsValue> {
|
|
851
|
+
let PanelOptions { panel: name } = parse_options(options);
|
|
852
|
+
let panel = self.resolve_panel(name)?;
|
|
485
853
|
Ok(JsValue::from_serde_ext(
|
|
486
|
-
&
|
|
854
|
+
&panel.renderer.render_timer().get_stats(),
|
|
487
855
|
)?)
|
|
488
856
|
}
|
|
489
857
|
|
|
@@ -506,25 +874,62 @@ impl PerspectiveViewerElement {
|
|
|
506
874
|
/// await viewer.flush();
|
|
507
875
|
/// ```
|
|
508
876
|
pub fn flush(&self) -> ApiFuture<()> {
|
|
509
|
-
clone
|
|
877
|
+
let workspace = self.workspace.clone();
|
|
878
|
+
let presentation = self.presentation.clone();
|
|
510
879
|
ApiFuture::new_throttled(async move {
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
880
|
+
loop {
|
|
881
|
+
workspace.effects().settle().await;
|
|
882
|
+
let panels = workspace
|
|
883
|
+
.reserved_panel()
|
|
884
|
+
.into_iter()
|
|
885
|
+
.chain(workspace.panels())
|
|
886
|
+
.collect::<Vec<_>>();
|
|
887
|
+
|
|
888
|
+
let mut fulfilled = false;
|
|
889
|
+
for panel in &panels {
|
|
890
|
+
panel.renderer.clone().with_lock(async { Ok(()) }).await?;
|
|
891
|
+
panel.renderer.clone().with_lock(async { Ok(()) }).await?;
|
|
892
|
+
panel.session.settle_dispatches().await?;
|
|
893
|
+
if !global::document().hidden()
|
|
894
|
+
&& presentation.is_visible()
|
|
895
|
+
&& !panel.renderer.is_plugin_activated()?
|
|
896
|
+
&& panel.session.get_error().is_none()
|
|
897
|
+
&& matches!(panel.session.has_table(), Some(TableLoadState::Loaded))
|
|
898
|
+
{
|
|
899
|
+
set_panel_paused(&panel.session, &panel.renderer, &presentation, true)
|
|
900
|
+
.await?;
|
|
901
|
+
if !panel.renderer.is_plugin_activated()? {
|
|
902
|
+
just_render(&panel.session, &panel.renderer)?.await?;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
fulfilled = true;
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
if !fulfilled && workspace.effects().is_empty() {
|
|
910
|
+
return Ok(());
|
|
911
|
+
}
|
|
912
|
+
}
|
|
522
913
|
})
|
|
523
914
|
}
|
|
524
915
|
|
|
525
|
-
///
|
|
526
|
-
/// [`perspective_js::JsViewConfig`] (
|
|
527
|
-
///
|
|
916
|
+
/// Restore a single panel from a full/partial
|
|
917
|
+
/// [`perspective_js::JsViewConfig`] (its user-configurable state, including
|
|
918
|
+
/// the `Table` name) — the active panel, or a specific panel via the
|
|
919
|
+
/// optional `{panel}` selector.
|
|
920
|
+
///
|
|
921
|
+
/// If `panel` names no existing panel, a NEW panel is created with that id
|
|
922
|
+
/// and the config restored into it (an upsert), equivalent to
|
|
923
|
+
/// [`Self::addPanel`] but with a caller-chosen id. As with a created panel,
|
|
924
|
+
/// the element-level `settings`/`theme` fields are ignored in that case.
|
|
925
|
+
///
|
|
926
|
+
/// On an empty element with a pending [`Self::load`] whose payload is not
|
|
927
|
+
/// yet classified, the active-target form (no `panel`) instead claims and
|
|
928
|
+
/// restores into that load's reserved first panel — see [`Self::load`].
|
|
929
|
+
///
|
|
930
|
+
/// This restores a SINGLE panel; a whole-element config (with a `panels`
|
|
931
|
+
/// map) must be applied via [`Self::restoreWorkspace`] — its `panels` /
|
|
932
|
+
/// `layout` keys are ignored here.
|
|
528
933
|
///
|
|
529
934
|
/// One of the best ways to use [`Self::restore`] is by first configuring
|
|
530
935
|
/// a `<perspective-viewer>` as you wish, then using either the `Debug`
|
|
@@ -535,6 +940,7 @@ impl PerspectiveViewerElement {
|
|
|
535
940
|
///
|
|
536
941
|
/// - `update` - The config to restore to, as returned by [`Self::save`] in
|
|
537
942
|
/// either "json", "string" or "arraybuffer" format.
|
|
943
|
+
/// - `name` - The panel to target, or `None` for the active panel.
|
|
538
944
|
///
|
|
539
945
|
/// # JavaScript Examples
|
|
540
946
|
///
|
|
@@ -551,102 +957,199 @@ impl PerspectiveViewerElement {
|
|
|
551
957
|
/// ```javascript
|
|
552
958
|
/// await viewer.restore({group_by: ["State"]});
|
|
553
959
|
/// ```
|
|
554
|
-
pub fn restore(
|
|
960
|
+
pub fn restore(
|
|
961
|
+
&self,
|
|
962
|
+
update: JsViewerConfigUpdate,
|
|
963
|
+
options: Option<JsPanelOptions>,
|
|
964
|
+
) -> JsVoidPromise {
|
|
965
|
+
let PanelOptions { panel: name } = parse_options(options);
|
|
966
|
+
let effect = self.workspace.effects().guard();
|
|
555
967
|
let this = self.clone();
|
|
556
|
-
ApiFuture::new_throttled(async move {
|
|
557
|
-
let
|
|
558
|
-
|
|
559
|
-
let
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
968
|
+
let fut = ApiFuture::new_throttled(async move {
|
|
969
|
+
let _effect = effect;
|
|
970
|
+
let id = name.map(PanelId::from);
|
|
971
|
+
let update = ViewerConfigUpdate::decode(&update)?;
|
|
972
|
+
match this.workspace.panel_or_active(id.as_ref()) {
|
|
973
|
+
// An existing (or the active) panel — update it in place.
|
|
974
|
+
Some(panel) => {
|
|
975
|
+
let active = this.workspace.active_id().as_ref() == Some(&panel.id);
|
|
976
|
+
restore_panel(
|
|
977
|
+
&panel.session,
|
|
978
|
+
&panel.renderer,
|
|
979
|
+
&this.presentation,
|
|
980
|
+
&this.workspace,
|
|
981
|
+
Some(&this.root),
|
|
982
|
+
RestoreMode::Existing { active },
|
|
983
|
+
update,
|
|
984
|
+
)
|
|
985
|
+
.await
|
|
986
|
+
},
|
|
987
|
+
// No existing panel matched. The active-target form
|
|
988
|
+
// (`panel: None`) CLAIMS a pending `load()`'s reserved panel
|
|
989
|
+
// — placing it and restoring into it — so a `restore` fired
|
|
990
|
+
// right after an unawaited `load(promise)` configures the
|
|
991
|
+
// panel that load's payload will bind, per the call-site
|
|
992
|
+
// ordering contract in [`Self::load`]. Named upserts and
|
|
993
|
+
// reservation-less elements create a fresh panel instead,
|
|
994
|
+
// routing through the shared `create_panel`
|
|
995
|
+
// (`RestoreMode::Fresh`) pipeline so the new panel's id is
|
|
996
|
+
// the requested `panel`.
|
|
997
|
+
None => {
|
|
998
|
+
let notify = this.layout_changed_notify();
|
|
999
|
+
let claimed = id
|
|
1000
|
+
.is_none()
|
|
1001
|
+
.then(|| place_reserved(&this.workspace, ¬ify))
|
|
1002
|
+
.flatten();
|
|
1003
|
+
match claimed {
|
|
1004
|
+
Some(panel) => {
|
|
1005
|
+
restore_panel(
|
|
1006
|
+
&panel.session,
|
|
1007
|
+
&panel.renderer,
|
|
1008
|
+
&this.presentation,
|
|
1009
|
+
&this.workspace,
|
|
1010
|
+
Some(&this.root),
|
|
1011
|
+
RestoreMode::Existing { active: true },
|
|
1012
|
+
update,
|
|
1013
|
+
)
|
|
1014
|
+
.await
|
|
1015
|
+
},
|
|
1016
|
+
None => {
|
|
1017
|
+
create_panel(
|
|
1018
|
+
&this.elem,
|
|
1019
|
+
&this.presentation,
|
|
1020
|
+
&this.workspace,
|
|
1021
|
+
¬ify,
|
|
1022
|
+
id,
|
|
1023
|
+
update,
|
|
1024
|
+
None,
|
|
1025
|
+
)
|
|
1026
|
+
.await?;
|
|
1027
|
+
Ok(())
|
|
1028
|
+
},
|
|
1029
|
+
}
|
|
1030
|
+
},
|
|
1031
|
+
}
|
|
1032
|
+
});
|
|
589
1033
|
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
.update_column_defaults(&this.renderer.metadata());
|
|
593
|
-
};
|
|
1034
|
+
js_sys::Promise::from(fut).unchecked_into()
|
|
1035
|
+
}
|
|
594
1036
|
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
1037
|
+
/// Restore the ENTIRE element from a whole-element
|
|
1038
|
+
/// [`WorkspaceConfigUpdate`] (`{version, active?, layout, panels, ...}`) —
|
|
1039
|
+
/// the multi-panel counterpart of [`Self::restore`]. Every existing panel
|
|
1040
|
+
/// is replaced by the `panels` entries, and the layout tree + master/detail
|
|
1041
|
+
/// cross-filter state re-applied. Unlike [`Self::restore`], this never
|
|
1042
|
+
/// falls back to the single-panel path.
|
|
1043
|
+
///
|
|
1044
|
+
/// # JavaScript Examples
|
|
1045
|
+
///
|
|
1046
|
+
/// ```javascript
|
|
1047
|
+
/// await viewer.restoreWorkspace(await otherViewer.saveWorkspace());
|
|
1048
|
+
/// ```
|
|
1049
|
+
pub fn restoreWorkspace(&self, update: JsWorkspaceConfigUpdate) -> JsVoidPromise {
|
|
1050
|
+
let update: JsViewerConfigUpdate = update.unchecked_into();
|
|
1051
|
+
let effect = self.workspace.effects().guard();
|
|
1052
|
+
let this = self.clone();
|
|
1053
|
+
let fut = ApiFuture::new(async move {
|
|
1054
|
+
let _effect = effect;
|
|
1055
|
+
let (contents, eject_tasks) = sync_update_panels(&this, update)?;
|
|
1056
|
+
let results = join_all(contents.into_iter().map(|(id, session, renderer, config)| {
|
|
1057
|
+
let presentation = this.presentation.clone();
|
|
1058
|
+
let workspace = this.workspace.clone();
|
|
1059
|
+
async move {
|
|
1060
|
+
stamp_global_overlay(&workspace, &id, &session);
|
|
1061
|
+
restore_panel(
|
|
1062
|
+
&session,
|
|
1063
|
+
&renderer,
|
|
1064
|
+
&presentation,
|
|
1065
|
+
&workspace,
|
|
1066
|
+
None,
|
|
1067
|
+
RestoreMode::Fresh,
|
|
1068
|
+
config,
|
|
1069
|
+
)
|
|
1070
|
+
.await?;
|
|
1071
|
+
if workspace.is_master(&id) {
|
|
1072
|
+
set_edit_mode(&session, &renderer, "SELECT_ROW_TREE");
|
|
599
1073
|
}
|
|
600
|
-
|
|
601
|
-
|
|
1074
|
+
|
|
1075
|
+
Ok(())
|
|
1076
|
+
}
|
|
1077
|
+
}))
|
|
602
1078
|
.await;
|
|
603
1079
|
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
1080
|
+
results.into_iter().collect::<ApiResult<Vec<_>>>()?;
|
|
1081
|
+
join_all(eject_tasks)
|
|
1082
|
+
.await
|
|
1083
|
+
.into_iter()
|
|
1084
|
+
.collect::<ApiResult<Vec<_>>>()?;
|
|
1085
|
+
|
|
1086
|
+
Ok(())
|
|
1087
|
+
});
|
|
1088
|
+
|
|
1089
|
+
js_sys::Promise::from(fut).unchecked_into()
|
|
609
1090
|
}
|
|
610
1091
|
|
|
611
1092
|
/// If this element is in an _errored_ state, this method will clear it and
|
|
612
1093
|
/// re-render. Calling this method is equivalent to clicking the error reset
|
|
613
1094
|
/// button in the UI.
|
|
614
1095
|
pub fn resetError(&self) -> ApiFuture<()> {
|
|
615
|
-
|
|
616
|
-
|
|
1096
|
+
let Some(panel) = self.workspace.active_panel() else {
|
|
1097
|
+
return ApiFuture::new_throttled(async move { Ok(()) });
|
|
1098
|
+
};
|
|
1099
|
+
|
|
1100
|
+
let reset_effect = self.workspace.effects().guard();
|
|
1101
|
+
let reset_task = panel.session.reset(ResetOptions::default());
|
|
1102
|
+
ApiFuture::spawn(async move {
|
|
1103
|
+
let _effect = reset_effect;
|
|
1104
|
+
reset_task.await
|
|
1105
|
+
});
|
|
1106
|
+
|
|
1107
|
+
let effect = self.workspace.effects().guard();
|
|
617
1108
|
ApiFuture::new_throttled(async move {
|
|
618
|
-
|
|
1109
|
+
let _effect = effect;
|
|
1110
|
+
apply_and_render(&panel.session, &panel.renderer, ViewConfigUpdate::default())?.await?;
|
|
619
1111
|
Ok(())
|
|
620
1112
|
})
|
|
621
1113
|
}
|
|
622
1114
|
|
|
623
|
-
/// Save
|
|
624
|
-
///
|
|
1115
|
+
/// Save a single panel's user-configurable state as a [`ViewerConfig`], one
|
|
1116
|
+
/// which can be restored via [`Self::restore`] — the active panel, or a
|
|
1117
|
+
/// specific panel via the optional `{panel}` selector.
|
|
1118
|
+
///
|
|
1119
|
+
/// This saves a SINGLE panel; to snapshot the ENTIRE element (every panel +
|
|
1120
|
+
/// layout + cross-filters) use [`Self::saveWorkspace`].
|
|
1121
|
+
///
|
|
1122
|
+
/// # Arguments
|
|
1123
|
+
///
|
|
1124
|
+
/// - `options` - An optional `{panel?: string}`; the panel to save, or the
|
|
1125
|
+
/// active panel when omitted.
|
|
625
1126
|
///
|
|
626
1127
|
/// # JavaScript Examples
|
|
627
1128
|
///
|
|
628
1129
|
/// Get the current `group_by` setting:
|
|
629
1130
|
///
|
|
630
1131
|
/// ```javascript
|
|
631
|
-
/// const {group_by} = await viewer.
|
|
1132
|
+
/// const {group_by} = await viewer.save();
|
|
632
1133
|
/// ```
|
|
633
1134
|
///
|
|
634
1135
|
/// Reset workflow attached to an external button `myResetButton`:
|
|
635
1136
|
///
|
|
636
1137
|
/// ```javascript
|
|
637
1138
|
/// const token = await viewer.save();
|
|
638
|
-
/// myResetButton.addEventListener("
|
|
1139
|
+
/// myResetButton.addEventListener("click", async () => {
|
|
639
1140
|
/// await viewer.restore(token);
|
|
640
1141
|
/// });
|
|
641
1142
|
/// ```
|
|
642
|
-
pub fn save(&self) -> JsViewerConfigPromise {
|
|
1143
|
+
pub fn save(&self, options: Option<JsPanelOptions>) -> JsViewerConfigPromise {
|
|
1144
|
+
let PanelOptions { panel: name } = parse_options(options);
|
|
643
1145
|
let this = self.clone();
|
|
644
1146
|
let fut = ApiFuture::new(async move {
|
|
645
|
-
let
|
|
1147
|
+
let panel = this.resolve_panel(name)?;
|
|
1148
|
+
let viewer_config = panel
|
|
646
1149
|
.renderer
|
|
647
1150
|
.clone()
|
|
648
1151
|
.with_lock(async {
|
|
649
|
-
get_viewer_config(&
|
|
1152
|
+
get_viewer_config(&panel.session, &panel.renderer, &this.presentation).await
|
|
650
1153
|
})
|
|
651
1154
|
.await?;
|
|
652
1155
|
|
|
@@ -656,6 +1159,24 @@ impl PerspectiveViewerElement {
|
|
|
656
1159
|
js_sys::Promise::from(fut).unchecked_into()
|
|
657
1160
|
}
|
|
658
1161
|
|
|
1162
|
+
/// Save the ENTIRE element to a whole-element [`WorkspaceConfig`]
|
|
1163
|
+
/// (`{version, active?, layout, panels, ...}`) — the multi-panel
|
|
1164
|
+
/// counterpart of [`Self::save`]. Unlike [`Self::save`] (which emits a
|
|
1165
|
+
/// single `ViewerConfig` for one panel), this ALWAYS emits the
|
|
1166
|
+
/// whole-element format, restorable via [`Self::restoreWorkspace`].
|
|
1167
|
+
///
|
|
1168
|
+
/// # JavaScript Examples
|
|
1169
|
+
///
|
|
1170
|
+
/// ```javascript
|
|
1171
|
+
/// const token = await viewer.saveWorkspace();
|
|
1172
|
+
/// await viewer.restoreWorkspace(token);
|
|
1173
|
+
/// ```
|
|
1174
|
+
pub fn saveWorkspace(&self) -> JsWorkspaceConfigPromise {
|
|
1175
|
+
let this = self.clone();
|
|
1176
|
+
let fut = ApiFuture::new(Self::workspace_config(this));
|
|
1177
|
+
js_sys::Promise::from(fut).unchecked_into()
|
|
1178
|
+
}
|
|
1179
|
+
|
|
659
1180
|
/// Download this viewer's internal [`View`] data via a browser download
|
|
660
1181
|
/// event.
|
|
661
1182
|
///
|
|
@@ -670,7 +1191,12 @@ impl PerspectiveViewerElement {
|
|
|
670
1191
|
/// await viewer.download();
|
|
671
1192
|
/// })
|
|
672
1193
|
/// ```
|
|
673
|
-
pub fn download(&self,
|
|
1194
|
+
pub fn download(&self, options: Option<JsExportOptions>) -> ApiFuture<()> {
|
|
1195
|
+
let ExportOptions {
|
|
1196
|
+
method,
|
|
1197
|
+
panel: name,
|
|
1198
|
+
} = parse_options(options);
|
|
1199
|
+
let method = method.map(|m| JsString::from(m.as_str()));
|
|
674
1200
|
let this = self.clone();
|
|
675
1201
|
ApiFuture::new_throttled(async move {
|
|
676
1202
|
let method = if let Some(method) = method
|
|
@@ -682,10 +1208,11 @@ impl PerspectiveViewerElement {
|
|
|
682
1208
|
ExportMethod::Csv
|
|
683
1209
|
};
|
|
684
1210
|
|
|
1211
|
+
let panel = this.resolve_panel(name)?;
|
|
685
1212
|
let blob =
|
|
686
|
-
export_method_to_blob(&
|
|
1213
|
+
export_method_to_blob(&panel.session, &panel.renderer, &this.presentation, method)
|
|
687
1214
|
.await?;
|
|
688
|
-
let is_chart =
|
|
1215
|
+
let is_chart = panel.renderer.is_chart();
|
|
689
1216
|
download(
|
|
690
1217
|
format!("untitled{}", method.as_filename(is_chart)).as_ref(),
|
|
691
1218
|
&blob,
|
|
@@ -711,7 +1238,12 @@ impl PerspectiveViewerElement {
|
|
|
711
1238
|
/// ```javascript
|
|
712
1239
|
/// const data = await viewer.export("plugin");
|
|
713
1240
|
/// ```
|
|
714
|
-
pub fn export(&self,
|
|
1241
|
+
pub fn export(&self, options: Option<JsExportOptions>) -> ApiFuture<JsValue> {
|
|
1242
|
+
let ExportOptions {
|
|
1243
|
+
method,
|
|
1244
|
+
panel: name,
|
|
1245
|
+
} = parse_options(options);
|
|
1246
|
+
let method = method.map(|m| JsString::from(m.as_str()));
|
|
715
1247
|
let this = self.clone();
|
|
716
1248
|
ApiFuture::new(async move {
|
|
717
1249
|
let method = if let Some(method) = method
|
|
@@ -723,7 +1255,8 @@ impl PerspectiveViewerElement {
|
|
|
723
1255
|
ExportMethod::Csv
|
|
724
1256
|
};
|
|
725
1257
|
|
|
726
|
-
|
|
1258
|
+
let panel = this.resolve_panel(name)?;
|
|
1259
|
+
export_method_to_jsvalue(&panel.session, &panel.renderer, &this.presentation, method)
|
|
727
1260
|
.await
|
|
728
1261
|
})
|
|
729
1262
|
}
|
|
@@ -743,7 +1276,12 @@ impl PerspectiveViewerElement {
|
|
|
743
1276
|
/// await viewer.copy();
|
|
744
1277
|
/// })
|
|
745
1278
|
/// ```
|
|
746
|
-
pub fn copy(&self,
|
|
1279
|
+
pub fn copy(&self, options: Option<JsExportOptions>) -> ApiFuture<()> {
|
|
1280
|
+
let ExportOptions {
|
|
1281
|
+
method,
|
|
1282
|
+
panel: name,
|
|
1283
|
+
} = parse_options(options);
|
|
1284
|
+
let method = method.map(|m| JsString::from(m.as_str()));
|
|
747
1285
|
let this = self.clone();
|
|
748
1286
|
ApiFuture::new_throttled(async move {
|
|
749
1287
|
let method = if let Some(method) = method
|
|
@@ -755,36 +1293,64 @@ impl PerspectiveViewerElement {
|
|
|
755
1293
|
ExportMethod::Csv
|
|
756
1294
|
};
|
|
757
1295
|
|
|
1296
|
+
let panel = this.resolve_panel(name)?;
|
|
758
1297
|
let js_task =
|
|
759
|
-
export_method_to_blob(&
|
|
1298
|
+
export_method_to_blob(&panel.session, &panel.renderer, &this.presentation, method);
|
|
760
1299
|
copy_to_clipboard(js_task, MimeType::TextPlain).await
|
|
761
1300
|
})
|
|
762
1301
|
}
|
|
763
1302
|
|
|
764
|
-
/// Reset
|
|
1303
|
+
/// Reset a panel's `ViewerConfig` to its data-relative default.
|
|
1304
|
+
///
|
|
1305
|
+
/// Without a `panel`, this is ELEMENT-LEVEL: EVERY panel is reset and the
|
|
1306
|
+
/// cross-filter overlay cleared (symmetric with whole-element
|
|
1307
|
+
/// [`Self::save`] / [`Self::restore`]). With `{panel}`, only that panel is
|
|
1308
|
+
/// reset — the other panels and the overlay are left untouched.
|
|
765
1309
|
///
|
|
766
1310
|
/// # Arguments
|
|
767
1311
|
///
|
|
768
1312
|
/// - `reset_all` - If set, will clear expressions and column settings as
|
|
769
1313
|
/// well.
|
|
1314
|
+
/// - `options` - An optional `{panel?: string}`; the panel to reset, or
|
|
1315
|
+
/// every panel when omitted.
|
|
770
1316
|
///
|
|
771
1317
|
/// # JavaScript Examples
|
|
772
1318
|
///
|
|
773
1319
|
/// ```javascript
|
|
774
|
-
/// await viewer.reset();
|
|
1320
|
+
/// await viewer.reset(); // every panel
|
|
1321
|
+
/// await viewer.reset(true, {panel: "p1"}); // just "p1", + expressions
|
|
775
1322
|
/// ```
|
|
776
|
-
pub fn reset(&self, reset_all: Option<bool>) -> ApiFuture<()> {
|
|
777
|
-
|
|
778
|
-
let
|
|
1323
|
+
pub fn reset(&self, reset_all: Option<bool>, options: Option<JsPanelOptions>) -> ApiFuture<()> {
|
|
1324
|
+
let PanelOptions { panel: name } = parse_options(options);
|
|
1325
|
+
let effect = self.workspace.effects().guard();
|
|
1326
|
+
let this = self.clone();
|
|
779
1327
|
let all = reset_all.unwrap_or_default();
|
|
780
1328
|
ApiFuture::new_throttled(async move {
|
|
781
|
-
let
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
.
|
|
785
|
-
.
|
|
1329
|
+
let _effect = effect;
|
|
1330
|
+
let (completion, receiver) = Completion::new();
|
|
1331
|
+
{
|
|
1332
|
+
let root = this.root.borrow();
|
|
1333
|
+
let app = root.as_ref().ok_or("Already deleted")?;
|
|
1334
|
+
match name {
|
|
1335
|
+
// Element-level: reset every panel + the cross-filter overlay.
|
|
1336
|
+
None => {
|
|
1337
|
+
tracing::debug!("Resetting config");
|
|
1338
|
+
app.send_message(PerspectiveViewerMsg::Reset(all, Some(completion)));
|
|
1339
|
+
},
|
|
1340
|
+
// A single named panel; errors if the panel doesn't exist.
|
|
1341
|
+
Some(name) => {
|
|
1342
|
+
let panel = this.resolve_panel(Some(name))?;
|
|
1343
|
+
tracing::debug!("Resetting config ({})", panel.id);
|
|
1344
|
+
app.send_message(PerspectiveViewerMsg::ResetPanel(
|
|
1345
|
+
Some(panel.id.to_string()),
|
|
1346
|
+
all,
|
|
1347
|
+
Some(completion),
|
|
1348
|
+
));
|
|
1349
|
+
},
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
786
1352
|
|
|
787
|
-
|
|
1353
|
+
receiver.await.map_err(|_| ApiError::new("Cancelled"))?
|
|
788
1354
|
})
|
|
789
1355
|
}
|
|
790
1356
|
|
|
@@ -817,18 +1383,27 @@ impl PerspectiveViewerElement {
|
|
|
817
1383
|
.unwrap_or_default()
|
|
818
1384
|
.unwrap_or_default();
|
|
819
1385
|
|
|
820
|
-
let
|
|
1386
|
+
let effect = self.workspace.effects().guard();
|
|
1387
|
+
let workspace = self.workspace.clone();
|
|
821
1388
|
ApiFuture::new_throttled(async move {
|
|
822
|
-
|
|
823
|
-
|
|
1389
|
+
let _effect = effect;
|
|
1390
|
+
// With zero panels there is nothing to resize; fan out to whatever
|
|
1391
|
+
// panels exist otherwise.
|
|
1392
|
+
let Some(panel) = workspace.active_panel() else {
|
|
1393
|
+
resize_visible_panels(&workspace).await;
|
|
1394
|
+
return Ok(());
|
|
1395
|
+
};
|
|
1396
|
+
|
|
1397
|
+
if !panel.renderer.is_plugin_activated()? {
|
|
1398
|
+
apply_and_render(&panel.session, &panel.renderer, ViewConfigUpdate::default())?
|
|
824
1399
|
.await?;
|
|
825
1400
|
} else if let Some(dims) = opts.dimensions {
|
|
826
|
-
|
|
1401
|
+
panel
|
|
827
1402
|
.renderer
|
|
828
1403
|
.resize_with_dimensions(dims.width, dims.height)
|
|
829
1404
|
.await?;
|
|
830
1405
|
} else {
|
|
831
|
-
|
|
1406
|
+
resize_visible_panels(&workspace).await;
|
|
832
1407
|
}
|
|
833
1408
|
|
|
834
1409
|
Ok(())
|
|
@@ -860,8 +1435,7 @@ impl PerspectiveViewerElement {
|
|
|
860
1435
|
if autosize {
|
|
861
1436
|
let handle = Some(ResizeObserverHandle::new(
|
|
862
1437
|
&self.elem,
|
|
863
|
-
&self.
|
|
864
|
-
&self.session,
|
|
1438
|
+
&self.workspace,
|
|
865
1439
|
&self.presentation,
|
|
866
1440
|
&self.root,
|
|
867
1441
|
));
|
|
@@ -873,10 +1447,11 @@ impl PerspectiveViewerElement {
|
|
|
873
1447
|
|
|
874
1448
|
/// Sets the auto-pause behavior of this component.
|
|
875
1449
|
///
|
|
876
|
-
/// When `true`, this `<perspective-viewer>` will
|
|
877
|
-
///
|
|
878
|
-
///
|
|
879
|
-
///
|
|
1450
|
+
/// When `true`, this `<perspective-viewer>` will skip rendering
|
|
1451
|
+
/// whenever it cannot be seen — tracked via an `IntersectionObserver`
|
|
1452
|
+
/// on itself (scrolled out of the viewport, `display: none`) combined
|
|
1453
|
+
/// with the document's page visibility (backgrounded browser tab,
|
|
1454
|
+
/// minimized window). Auto-pause is enabled by default.
|
|
880
1455
|
///
|
|
881
1456
|
/// # Arguments
|
|
882
1457
|
///
|
|
@@ -892,53 +1467,75 @@ impl PerspectiveViewerElement {
|
|
|
892
1467
|
#[wasm_bindgen]
|
|
893
1468
|
pub fn setAutoPause(&self, autopause: bool) -> ApiFuture<()> {
|
|
894
1469
|
if autopause {
|
|
895
|
-
let handle = Some(
|
|
1470
|
+
let handle = Some(AutoPauseHandle::new(
|
|
896
1471
|
&self.elem,
|
|
897
1472
|
&self.presentation,
|
|
898
|
-
&self.
|
|
899
|
-
&self.renderer,
|
|
1473
|
+
&self.workspace,
|
|
900
1474
|
));
|
|
901
1475
|
|
|
902
1476
|
*self.intersection_handle.borrow_mut() = handle;
|
|
903
1477
|
} else {
|
|
904
1478
|
*self.intersection_handle.borrow_mut() = None;
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
1479
|
+
let effect = self.workspace.effects().guard();
|
|
1480
|
+
let workspace = self.workspace.clone();
|
|
1481
|
+
let presentation = self.presentation.clone();
|
|
1482
|
+
return ApiFuture::new(async move {
|
|
1483
|
+
let _effect = effect;
|
|
1484
|
+
for id in workspace.panel_ids() {
|
|
1485
|
+
if let Some(panel) = workspace.panel(&id) {
|
|
1486
|
+
// A failed resume is already surfaced as that
|
|
1487
|
+
// panel's error state — don't let it abort the
|
|
1488
|
+
// remaining panels' resumes.
|
|
1489
|
+
let _ =
|
|
1490
|
+
set_panel_paused(&panel.session, &panel.renderer, &presentation, true)
|
|
1491
|
+
.await;
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
Ok(())
|
|
1496
|
+
});
|
|
914
1497
|
}
|
|
915
1498
|
|
|
916
1499
|
ApiFuture::new(async move { Ok(()) })
|
|
917
1500
|
}
|
|
918
1501
|
|
|
919
1502
|
/// Return a [`perspective_js::JsViewWindow`] for the currently selected
|
|
920
|
-
/// region.
|
|
1503
|
+
/// region of the named panel, or the active panel when `panel` is omitted.
|
|
921
1504
|
#[wasm_bindgen]
|
|
922
|
-
pub fn getSelection(&self) -> Option<JsViewWindow
|
|
923
|
-
|
|
1505
|
+
pub fn getSelection(&self, options: Option<JsPanelOptions>) -> ApiResult<Option<JsViewWindow>> {
|
|
1506
|
+
let PanelOptions { panel: name } = parse_options(options);
|
|
1507
|
+
let panel = self.resolve_panel(name)?;
|
|
1508
|
+
Ok(panel.renderer.get_selection().map(|x| x.into()))
|
|
924
1509
|
}
|
|
925
1510
|
|
|
926
|
-
/// Set the selection [`perspective_js::JsViewWindow`] for
|
|
1511
|
+
/// Set the selection [`perspective_js::JsViewWindow`] for the named panel,
|
|
1512
|
+
/// or the active panel when `panel` is omitted.
|
|
927
1513
|
#[wasm_bindgen]
|
|
928
|
-
pub fn setSelection(
|
|
1514
|
+
pub fn setSelection(
|
|
1515
|
+
&self,
|
|
1516
|
+
window: Option<JsViewWindow>,
|
|
1517
|
+
options: Option<JsPanelOptions>,
|
|
1518
|
+
) -> ApiResult<()> {
|
|
1519
|
+
let PanelOptions { panel: name } = parse_options(options);
|
|
929
1520
|
let window = window.map(|x| x.into_serde_ext()).transpose()?;
|
|
930
|
-
self.renderer.set_selection(window);
|
|
1521
|
+
self.resolve_panel(name)?.renderer.set_selection(window);
|
|
931
1522
|
Ok(())
|
|
932
1523
|
}
|
|
933
1524
|
|
|
934
|
-
/// Get this viewer's edit port for the
|
|
935
|
-
/// [`Table::update`] for details on ports)
|
|
1525
|
+
/// Get this viewer's edit port for the named panel's [`Table`] (see
|
|
1526
|
+
/// [`Table::update`] for details on ports), or the active panel when
|
|
1527
|
+
/// `panel` is omitted.
|
|
936
1528
|
#[wasm_bindgen]
|
|
937
|
-
pub fn getEditPort(&self) ->
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
.
|
|
1529
|
+
pub fn getEditPort(&self, options: Option<JsPanelOptions>) -> ApiResult<f64> {
|
|
1530
|
+
let PanelOptions { panel: name } = parse_options(options);
|
|
1531
|
+
let panel = self.resolve_panel(name)?;
|
|
1532
|
+
let edit_port = if let Some(ctx) = panel.renderer.render_context() {
|
|
1533
|
+
ctx.edit_port
|
|
1534
|
+
} else {
|
|
1535
|
+
panel.session.metadata().get_edit_port()
|
|
1536
|
+
};
|
|
1537
|
+
|
|
1538
|
+
Ok(edit_port.ok_or("No `Table` loaded")?)
|
|
942
1539
|
}
|
|
943
1540
|
|
|
944
1541
|
/// Restyle all plugins from current document.
|
|
@@ -959,10 +1556,21 @@ impl PerspectiveViewerElement {
|
|
|
959
1556
|
/// ```
|
|
960
1557
|
#[wasm_bindgen]
|
|
961
1558
|
pub fn restyleElement(&self) -> ApiFuture<JsValue> {
|
|
962
|
-
clone!(self.
|
|
1559
|
+
clone!(self.workspace, self.presentation);
|
|
1560
|
+
let effect = workspace.effects().guard();
|
|
963
1561
|
ApiFuture::new(async move {
|
|
964
|
-
let
|
|
965
|
-
|
|
1562
|
+
let _effect = effect;
|
|
1563
|
+
let default = presentation.get_default_theme_name().await;
|
|
1564
|
+
for panel in workspace
|
|
1565
|
+
.panel_ids()
|
|
1566
|
+
.into_iter()
|
|
1567
|
+
.filter_map(|id| workspace.panel(&id))
|
|
1568
|
+
{
|
|
1569
|
+
panel.renderer.set_default_theme(default.clone());
|
|
1570
|
+
panel.renderer.restyle_all().await?;
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
Ok(JsValue::UNDEFINED)
|
|
966
1574
|
})
|
|
967
1575
|
}
|
|
968
1576
|
|
|
@@ -982,8 +1590,10 @@ impl PerspectiveViewerElement {
|
|
|
982
1590
|
/// ```
|
|
983
1591
|
#[wasm_bindgen]
|
|
984
1592
|
pub fn resetThemes(&self, themes: Option<Box<[JsValue]>>) -> ApiFuture<JsValue> {
|
|
985
|
-
clone!(self.
|
|
1593
|
+
clone!(self.workspace, self.presentation);
|
|
1594
|
+
let effect = workspace.effects().guard();
|
|
986
1595
|
ApiFuture::new(async move {
|
|
1596
|
+
let _effect = effect;
|
|
987
1597
|
let themes: Option<Vec<String>> = themes
|
|
988
1598
|
.unwrap_or_default()
|
|
989
1599
|
.iter()
|
|
@@ -991,17 +1601,37 @@ impl PerspectiveViewerElement {
|
|
|
991
1601
|
.collect();
|
|
992
1602
|
|
|
993
1603
|
let theme_name = presentation.get_selected_theme_name().await;
|
|
994
|
-
|
|
995
|
-
let
|
|
996
|
-
|
|
997
|
-
.await?
|
|
1604
|
+
presentation.reset_available_themes(themes).await;
|
|
1605
|
+
let available = presentation.get_available_themes().await?;
|
|
1606
|
+
let reset_theme = available
|
|
998
1607
|
.iter()
|
|
999
1608
|
.find(|y| theme_name.as_ref() == Some(y))
|
|
1000
1609
|
.cloned();
|
|
1001
1610
|
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1611
|
+
presentation.set_theme_name(reset_theme.as_deref()).await?;
|
|
1612
|
+
let new_default = presentation.get_default_theme_name().await;
|
|
1613
|
+
for panel in workspace
|
|
1614
|
+
.panel_ids()
|
|
1615
|
+
.into_iter()
|
|
1616
|
+
.filter_map(|id| workspace.panel(&id))
|
|
1617
|
+
{
|
|
1618
|
+
// Availability applies to PANELS too: a panel pinned to a
|
|
1619
|
+
// theme outside the new set follows the host — clear the pin
|
|
1620
|
+
// so it renders (and `save`s) the new registry default, the
|
|
1621
|
+
// same keep-if-available-else-default rule applied to the
|
|
1622
|
+
// host selection above.
|
|
1623
|
+
if panel
|
|
1624
|
+
.renderer
|
|
1625
|
+
.theme()
|
|
1626
|
+
.is_some_and(|t| !available.contains(&t))
|
|
1627
|
+
{
|
|
1628
|
+
panel.renderer.set_theme(None);
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
panel.renderer.set_default_theme(new_default.clone());
|
|
1632
|
+
if panel.renderer.needs_restyle() {
|
|
1633
|
+
panel.renderer.restyle_all().await?;
|
|
1634
|
+
}
|
|
1005
1635
|
}
|
|
1006
1636
|
|
|
1007
1637
|
Ok(JsValue::UNDEFINED)
|
|
@@ -1027,7 +1657,14 @@ impl PerspectiveViewerElement {
|
|
|
1027
1657
|
/// ```
|
|
1028
1658
|
#[wasm_bindgen]
|
|
1029
1659
|
pub fn setThrottle(&self, val: Option<f64>) {
|
|
1030
|
-
self
|
|
1660
|
+
for panel in self
|
|
1661
|
+
.workspace
|
|
1662
|
+
.panel_ids()
|
|
1663
|
+
.into_iter()
|
|
1664
|
+
.filter_map(|id| self.workspace.panel(&id))
|
|
1665
|
+
{
|
|
1666
|
+
panel.renderer.set_throttle(val);
|
|
1667
|
+
}
|
|
1031
1668
|
}
|
|
1032
1669
|
|
|
1033
1670
|
/// Toggle (or force) the config panel open/closed.
|
|
@@ -1044,8 +1681,10 @@ impl PerspectiveViewerElement {
|
|
|
1044
1681
|
/// ```
|
|
1045
1682
|
#[wasm_bindgen]
|
|
1046
1683
|
pub fn toggleConfig(&self, force: Option<bool>) -> ApiFuture<JsValue> {
|
|
1684
|
+
let effect = self.workspace.effects().guard();
|
|
1047
1685
|
let root = self.root.clone();
|
|
1048
1686
|
ApiFuture::new(async move {
|
|
1687
|
+
let _effect = effect;
|
|
1049
1688
|
let force = force.map(SettingsUpdate::Update);
|
|
1050
1689
|
let (sender, receiver) = channel::<ApiResult<wasm_bindgen::JsValue>>();
|
|
1051
1690
|
root.borrow().as_ref().into_apierror()?.send_message(
|
|
@@ -1061,7 +1700,10 @@ impl PerspectiveViewerElement {
|
|
|
1061
1700
|
/// [`registerPlugin`] after the host has rendered for the first time.
|
|
1062
1701
|
#[wasm_bindgen]
|
|
1063
1702
|
pub fn getAllPlugins(&self) -> Array {
|
|
1064
|
-
self.
|
|
1703
|
+
self.workspace
|
|
1704
|
+
.active_renderer()
|
|
1705
|
+
.map(|r| r.get_all_plugins().iter().collect::<Array>())
|
|
1706
|
+
.unwrap_or_default()
|
|
1065
1707
|
}
|
|
1066
1708
|
|
|
1067
1709
|
/// Gets a plugin Custom Element with the `name` field, or get the active
|
|
@@ -1073,12 +1715,108 @@ impl PerspectiveViewerElement {
|
|
|
1073
1715
|
/// or `None` for the active plugin's Custom Element.
|
|
1074
1716
|
#[wasm_bindgen]
|
|
1075
1717
|
pub fn getPlugin(&self, name: Option<String>) -> ApiResult<JsPerspectiveViewerPlugin> {
|
|
1718
|
+
let renderer = self
|
|
1719
|
+
.workspace
|
|
1720
|
+
.active_renderer()
|
|
1721
|
+
.ok_or_else(|| ApiError::new("No active panel"))?;
|
|
1076
1722
|
match name {
|
|
1077
|
-
None =>
|
|
1078
|
-
Some(name) =>
|
|
1723
|
+
None => renderer.ensure_plugin_selected(),
|
|
1724
|
+
Some(name) => renderer.get_plugin(&name),
|
|
1079
1725
|
}
|
|
1080
1726
|
}
|
|
1081
1727
|
|
|
1728
|
+
/// Add a new, independent panel to this viewer's layout, rendering the
|
|
1729
|
+
/// supplied [`ViewerConfigUpdate`] into it. The panel uses the default
|
|
1730
|
+
/// [`perspective_client::Client`] (the first passed to [`Self::load`]) to
|
|
1731
|
+
/// resolve its `table`. Returns the generated panel id.
|
|
1732
|
+
///
|
|
1733
|
+
/// Element-level config fields (`settings`, `theme`) in the argument are
|
|
1734
|
+
/// ignored — those are shared across the element, not per-panel.
|
|
1735
|
+
#[wasm_bindgen]
|
|
1736
|
+
pub fn addPanel(&self, update: JsViewerConfigUpdate) -> ApiFuture<JsValue> {
|
|
1737
|
+
clone!(self.elem, self.presentation, self.workspace);
|
|
1738
|
+
let effect = workspace.effects().guard();
|
|
1739
|
+
let notify = self.layout_changed_notify();
|
|
1740
|
+
ApiFuture::new(async move {
|
|
1741
|
+
let _effect = effect;
|
|
1742
|
+
let update = ViewerConfigUpdate::decode(&update)?;
|
|
1743
|
+
let id = create_panel(
|
|
1744
|
+
&elem,
|
|
1745
|
+
&presentation,
|
|
1746
|
+
&workspace,
|
|
1747
|
+
¬ify,
|
|
1748
|
+
None,
|
|
1749
|
+
update,
|
|
1750
|
+
None,
|
|
1751
|
+
)
|
|
1752
|
+
.await?;
|
|
1753
|
+
Ok(JsValue::from_str(id.as_str()))
|
|
1754
|
+
})
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
/// Get the ids of all panels in this viewer's layout, in insertion order.
|
|
1758
|
+
#[wasm_bindgen]
|
|
1759
|
+
pub fn getPanelNames(&self) -> Array {
|
|
1760
|
+
self.workspace
|
|
1761
|
+
.panel_ids()
|
|
1762
|
+
.iter()
|
|
1763
|
+
.map(|id| JsValue::from_str(id.as_str()))
|
|
1764
|
+
.collect()
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
/// The id of the active panel — the one the settings panel and status-bar
|
|
1768
|
+
/// toolbar target — or `null` when the element has zero panels.
|
|
1769
|
+
#[wasm_bindgen]
|
|
1770
|
+
pub fn getActivePanel(&self) -> JsValue {
|
|
1771
|
+
self.workspace
|
|
1772
|
+
.active_id()
|
|
1773
|
+
.map(|id| JsValue::from_str(id.as_str()))
|
|
1774
|
+
.unwrap_or(JsValue::NULL)
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
/// Make the panel with id `name` the active panel, re-targeting the
|
|
1778
|
+
/// settings panel and status-bar toolbar (and the root's
|
|
1779
|
+
/// session/renderer subscriptions) to its engines. Resolves after the
|
|
1780
|
+
/// activation-chrome redraws on both sides of the switch have completed
|
|
1781
|
+
/// (invariant I6).
|
|
1782
|
+
#[wasm_bindgen]
|
|
1783
|
+
pub fn setActivePanel(&self, name: String) -> ApiFuture<()> {
|
|
1784
|
+
let effect = self.workspace.effects().guard();
|
|
1785
|
+
let root = self.root.clone();
|
|
1786
|
+
ApiFuture::new(async move {
|
|
1787
|
+
let _effect = effect;
|
|
1788
|
+
let (completion, receiver) = Completion::new();
|
|
1789
|
+
root.borrow()
|
|
1790
|
+
.as_ref()
|
|
1791
|
+
.into_apierror()?
|
|
1792
|
+
.send_message(PerspectiveViewerMsg::SetActivePanel(name, Some(completion)));
|
|
1793
|
+
|
|
1794
|
+
receiver.await.map_err(|_| ApiError::new("Cancelled"))?
|
|
1795
|
+
})
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
/// Remove the panel with id `name` from the layout, disposing its engines
|
|
1799
|
+
/// (its `View` is deleted and its `Table` reference released). The last
|
|
1800
|
+
/// remaining panel cannot be removed (resolves as a no-op). Resolves
|
|
1801
|
+
/// after the panel's teardown run completes, carrying any teardown
|
|
1802
|
+
/// error — previously fire-and-forget and silently dropped (invariant
|
|
1803
|
+
/// I6). See also [`Self::addPanel`].
|
|
1804
|
+
#[wasm_bindgen]
|
|
1805
|
+
pub fn removePanel(&self, name: String) -> ApiFuture<()> {
|
|
1806
|
+
let effect = self.workspace.effects().guard();
|
|
1807
|
+
let root = self.root.clone();
|
|
1808
|
+
ApiFuture::new(async move {
|
|
1809
|
+
let _effect = effect;
|
|
1810
|
+
let (completion, receiver) = Completion::new();
|
|
1811
|
+
root.borrow()
|
|
1812
|
+
.as_ref()
|
|
1813
|
+
.into_apierror()?
|
|
1814
|
+
.send_message(PerspectiveViewerMsg::ClosePanel(name, Some(completion)));
|
|
1815
|
+
|
|
1816
|
+
receiver.await.map_err(|_| ApiError::new("Cancelled"))?
|
|
1817
|
+
})
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1082
1820
|
/// Create a new JavaScript Heap reference for this model instance.
|
|
1083
1821
|
#[doc(hidden)]
|
|
1084
1822
|
#[allow(clippy::use_self)]
|
|
@@ -1094,40 +1832,31 @@ impl PerspectiveViewerElement {
|
|
|
1094
1832
|
/// string}`. The CustomEvent is also fired whenever the user toggles the
|
|
1095
1833
|
/// sidebar manually.
|
|
1096
1834
|
#[wasm_bindgen]
|
|
1097
|
-
pub fn toggleColumnSettings(
|
|
1098
|
-
clone!(self.session, self.root);
|
|
1099
|
-
ApiFuture::new_throttled(async move {
|
|
1100
|
-
let locator = get_column_locator(&session.metadata(), Some(column_name));
|
|
1101
|
-
let (sender, receiver) = channel::<()>();
|
|
1102
|
-
root.borrow().as_ref().into_apierror()?.send_message(
|
|
1103
|
-
PerspectiveViewerMsg::OpenColumnSettings {
|
|
1104
|
-
locator,
|
|
1105
|
-
sender: Some(sender),
|
|
1106
|
-
toggle: true,
|
|
1107
|
-
},
|
|
1108
|
-
);
|
|
1109
|
-
|
|
1110
|
-
receiver.await.map_err(|_| ApiError::from("Cancelled"))
|
|
1111
|
-
})
|
|
1112
|
-
}
|
|
1113
|
-
|
|
1114
|
-
/// Force open the settings for a particular column. Pass `null` to close
|
|
1115
|
-
/// the column settings panel. See [`Self::toggleColumnSettings`] for more.
|
|
1116
|
-
#[wasm_bindgen]
|
|
1117
|
-
pub fn openColumnSettings(
|
|
1835
|
+
pub fn toggleColumnSettings(
|
|
1118
1836
|
&self,
|
|
1119
|
-
column_name:
|
|
1120
|
-
|
|
1837
|
+
column_name: String,
|
|
1838
|
+
options: Option<JsPanelOptions>,
|
|
1121
1839
|
) -> ApiFuture<()> {
|
|
1122
|
-
let
|
|
1123
|
-
|
|
1840
|
+
let PanelOptions { panel: name } = parse_options(options);
|
|
1841
|
+
let effect = self.workspace.effects().guard();
|
|
1842
|
+
let this = self.clone();
|
|
1124
1843
|
ApiFuture::new_throttled(async move {
|
|
1844
|
+
let _effect = effect;
|
|
1845
|
+
let panel = this.resolve_panel(name)?;
|
|
1846
|
+
let was_active = this.workspace.active_id().as_ref() == Some(&panel.id);
|
|
1847
|
+
let locator = get_column_locator(&panel.session.metadata(), Some(column_name));
|
|
1848
|
+
if !was_active {
|
|
1849
|
+
this.root.borrow().as_ref().into_apierror()?.send_message(
|
|
1850
|
+
PerspectiveViewerMsg::SetActivePanel(panel.id.as_str().to_owned(), None),
|
|
1851
|
+
);
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1125
1854
|
let (sender, receiver) = channel::<()>();
|
|
1126
|
-
root.borrow().as_ref().into_apierror()?.send_message(
|
|
1855
|
+
this.root.borrow().as_ref().into_apierror()?.send_message(
|
|
1127
1856
|
PerspectiveViewerMsg::OpenColumnSettings {
|
|
1128
1857
|
locator,
|
|
1129
1858
|
sender: Some(sender),
|
|
1130
|
-
toggle:
|
|
1859
|
+
toggle: was_active,
|
|
1131
1860
|
},
|
|
1132
1861
|
);
|
|
1133
1862
|
|