@perspective-dev/viewer 5.0.0 → 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 +6 -6
- package/dist/cdn/perspective-viewer.js.map +2 -2
- 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/perspective-viewer.inline.js +9 -9
- package/dist/esm/perspective-viewer.inline.js.map +2 -2
- package/dist/esm/perspective-viewer.js +1 -1
- package/dist/esm/perspective-viewer.js.map +2 -2
- package/dist/esm/ts-rs/PanelViewerConfig.d.ts +2 -0
- package/dist/esm/ts-rs/ViewerConfig.d.ts +2 -0
- 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/wasm/perspective-viewer.d.ts +103 -16
- package/dist/wasm/perspective-viewer.js +118 -108
- package/dist/wasm/perspective-viewer.wasm +0 -0
- package/dist/wasm/perspective-viewer.wasm.d.ts +15 -15
- package/package.json +1 -1
- package/src/css/column-selector.css +1 -0
- package/src/css/column-settings-panel.css +67 -2
- package/src/css/column-style.css +4 -3
- package/src/css/config-selector.css +22 -1
- package/src/css/containers/context-menu.css +12 -6
- package/src/css/containers/tabs.css +2 -2
- package/src/css/viewer.css +8 -0
- package/src/rust/components/column_selector/active_column.rs +21 -27
- package/src/rust/components/column_selector/column_selector_column_row.rs +73 -0
- package/src/rust/components/column_selector/config_selector.rs +7 -1
- package/src/rust/components/column_selector/expr_edit_button.rs +7 -1
- package/src/rust/components/column_selector/inactive_column.rs +8 -1
- package/src/rust/components/column_selector.rs +20 -3
- 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 +6 -0
- package/src/rust/components/editable_header.rs +2 -0
- package/src/rust/components/main_panel.rs +5 -0
- package/src/rust/components/mod.rs +1 -0
- package/src/rust/components/panel_menu.rs +7 -0
- package/src/rust/components/panel_tab.rs +5 -0
- package/src/rust/components/portal.rs +7 -0
- package/src/rust/components/viewer/msg.rs +2 -0
- package/src/rust/components/viewer/render.rs +12 -0
- package/src/rust/components/viewer/settings.rs +33 -1
- package/src/rust/components/viewer.rs +2 -0
- package/src/rust/components/window_editor.rs +1013 -0
- package/src/rust/lib.rs +4 -1
- package/src/rust/presentation/column_locator.rs +16 -1
- package/src/rust/queries/columns_iter_set.rs +7 -1
- package/src/rust/renderer/dispatch.rs +85 -58
- package/src/rust/renderer.rs +34 -1
- 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.rs +7 -5
- package/src/rust/tasks/edit_window.rs +155 -0
- package/src/rust/tasks/mod.rs +2 -0
- package/src/rust/utils/browser/dragdrop.rs +12 -0
- package/src/rust/utils/browser/tests/debounce.rs +49 -0
- package/src/rust/utils/debounce.rs +76 -27
- package/src/svg/pin-icon.svg +5 -0
- package/src/themes/icons.css +1 -0
- 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/ts/ts-rs/PanelViewerConfig.ts +2 -1
- package/src/ts/ts-rs/ViewerConfig.ts +2 -1
- 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/dist/wasm/snippets/{perspective-viewer-daeef4ceb066e6c7 → perspective-viewer-8a8c7d5d696b43ae}/inline0.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-daeef4ceb066e6c7 → perspective-viewer-8a8c7d5d696b43ae}/inline1.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-daeef4ceb066e6c7 → perspective-viewer-8a8c7d5d696b43ae}/inline2.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-daeef4ceb066e6c7 → perspective-viewer-8a8c7d5d696b43ae}/inline3.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-daeef4ceb066e6c7 → perspective-viewer-8a8c7d5d696b43ae}/inline4.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-daeef4ceb066e6c7 → perspective-viewer-8a8c7d5d696b43ae}/inline5.js +0 -0
|
@@ -13,12 +13,13 @@ mod attributes_tab;
|
|
|
13
13
|
|
|
14
14
|
mod save_settings;
|
|
15
15
|
pub(crate) mod style_tab;
|
|
16
|
+
mod window_tab;
|
|
16
17
|
|
|
17
18
|
use std::rc::Rc;
|
|
18
19
|
|
|
19
20
|
use derivative::Derivative;
|
|
20
21
|
use itertools::Itertools;
|
|
21
|
-
use perspective_client::config::{ColumnType, Expression, ViewConfig};
|
|
22
|
+
use perspective_client::config::{ColumnType, Expression, ViewConfig, WindowSpec};
|
|
22
23
|
use perspective_client::utils::PerspectiveResultExt;
|
|
23
24
|
use yew::{Callback, Component, Html, Properties, html, props};
|
|
24
25
|
|
|
@@ -27,15 +28,19 @@ use self::style_tab::StyleTabProps;
|
|
|
27
28
|
use crate::components::column_settings_sidebar::attributes_tab::AttributesTab;
|
|
28
29
|
use crate::components::column_settings_sidebar::save_settings::SaveSettingsProps;
|
|
29
30
|
use crate::components::column_settings_sidebar::style_tab::StyleTab;
|
|
31
|
+
use crate::components::column_settings_sidebar::window_tab::{WindowTab, WindowTabProps};
|
|
30
32
|
use crate::components::containers::sidebar::Sidebar;
|
|
31
33
|
use crate::components::containers::tab_list::TabList;
|
|
32
34
|
use crate::components::editable_header::EditableHeaderProps;
|
|
33
35
|
use crate::components::expression_editor::ExpressionEditorProps;
|
|
34
36
|
use crate::components::type_icon::TypeIconType;
|
|
37
|
+
use crate::components::window_editor::WindowEditorProps;
|
|
35
38
|
use crate::presentation::{ColumnLocator, ColumnSettingsTab, Presentation};
|
|
36
39
|
use crate::renderer::Renderer;
|
|
37
40
|
use crate::session::{Session, SessionMetadataRc};
|
|
38
|
-
use crate::tasks::{
|
|
41
|
+
use crate::tasks::{
|
|
42
|
+
delete_expr, delete_window, save_expr, save_window, update_expr, update_window,
|
|
43
|
+
};
|
|
39
44
|
use crate::utils::PtrEqRc;
|
|
40
45
|
|
|
41
46
|
#[derive(Clone, Derivative, Properties)]
|
|
@@ -47,9 +52,22 @@ pub struct ColumnSettingsPanelProps {
|
|
|
47
52
|
pub width_override: Option<i32>,
|
|
48
53
|
pub on_select_tab: Callback<ColumnSettingsTab>,
|
|
49
54
|
|
|
50
|
-
///
|
|
51
|
-
///
|
|
52
|
-
|
|
55
|
+
/// Shared trap-door width across the drawer's Style/Attributes/Window
|
|
56
|
+
/// tabs.
|
|
57
|
+
#[prop_or_default]
|
|
58
|
+
pub auto_width: f64,
|
|
59
|
+
|
|
60
|
+
#[prop_or_default]
|
|
61
|
+
pub on_auto_width: Callback<f64>,
|
|
62
|
+
|
|
63
|
+
/// Whether the drawer is pinned into the layout.
|
|
64
|
+
#[prop_or_default]
|
|
65
|
+
pub is_pinned: bool,
|
|
66
|
+
|
|
67
|
+
#[prop_or_default]
|
|
68
|
+
pub on_toggle_pin: Callback<()>,
|
|
69
|
+
|
|
70
|
+
/// Active plugin name.
|
|
53
71
|
pub plugin_name: Option<String>,
|
|
54
72
|
|
|
55
73
|
/// Session metadata snapshot — threaded from `SessionProps`.
|
|
@@ -75,8 +93,10 @@ pub struct ColumnSettingsPanelProps {
|
|
|
75
93
|
pub session: Session,
|
|
76
94
|
}
|
|
77
95
|
|
|
78
|
-
impl
|
|
79
|
-
|
|
96
|
+
impl ColumnSettingsPanelProps {
|
|
97
|
+
/// Everything EXCEPT the trap-door `auto_width`: the props whose change
|
|
98
|
+
/// invalidates the drafts that `initialize` rebuilds.
|
|
99
|
+
fn identity_eq(&self, other: &Self) -> bool {
|
|
80
100
|
self.selected_column == other.selected_column
|
|
81
101
|
&& self.selected_tab == other.selected_tab
|
|
82
102
|
&& self.plugin_name == other.plugin_name
|
|
@@ -87,12 +107,21 @@ impl PartialEq for ColumnSettingsPanelProps {
|
|
|
87
107
|
}
|
|
88
108
|
}
|
|
89
109
|
|
|
110
|
+
impl PartialEq for ColumnSettingsPanelProps {
|
|
111
|
+
fn eq(&self, other: &Self) -> bool {
|
|
112
|
+
self.identity_eq(other)
|
|
113
|
+
&& self.auto_width == other.auto_width
|
|
114
|
+
&& self.is_pinned == other.is_pinned
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
90
118
|
#[derive(Debug)]
|
|
91
119
|
pub enum ColumnSettingsPanelMsg {
|
|
92
120
|
SetExprValue(Rc<String>),
|
|
93
121
|
SetExprValid(bool),
|
|
94
122
|
SetHeaderValue(Option<String>),
|
|
95
123
|
SetHeaderValid(bool),
|
|
124
|
+
SetWindowValue(Option<WindowSpec>),
|
|
96
125
|
SetSelectedTab((usize, ColumnSettingsTab)),
|
|
97
126
|
OnSaveAttributes(()),
|
|
98
127
|
OnResetAttributes(()),
|
|
@@ -117,6 +146,8 @@ pub struct ColumnSettingsPanel {
|
|
|
117
146
|
reset_enabled: bool,
|
|
118
147
|
save_count: u8,
|
|
119
148
|
save_enabled: bool,
|
|
149
|
+
initial_window_value: Option<WindowSpec>,
|
|
150
|
+
window_value: Option<WindowSpec>,
|
|
120
151
|
tabs: Vec<ColumnSettingsTab>,
|
|
121
152
|
}
|
|
122
153
|
|
|
@@ -138,6 +169,8 @@ impl Component for ColumnSettingsPanel {
|
|
|
138
169
|
reset_count: 0,
|
|
139
170
|
column_name: "".to_owned(),
|
|
140
171
|
maybe_ty: None,
|
|
172
|
+
initial_window_value: None,
|
|
173
|
+
window_value: None,
|
|
141
174
|
tabs: vec![],
|
|
142
175
|
on_input: Callback::default(),
|
|
143
176
|
on_save: Callback::default(),
|
|
@@ -149,12 +182,15 @@ impl Component for ColumnSettingsPanel {
|
|
|
149
182
|
}
|
|
150
183
|
|
|
151
184
|
fn changed(&mut self, ctx: &yew::prelude::Context<Self>, old_props: &Self::Properties) -> bool {
|
|
152
|
-
|
|
185
|
+
// Only reached when props are unequal. Re-`initialize` (which wipes
|
|
186
|
+
// in-progress expression/window drafts) only on IDENTITY changes -
|
|
187
|
+
// a trap-door `auto_width` change re-renders the `Sidebar` sizer
|
|
188
|
+
// alone.
|
|
189
|
+
if !ctx.props().identity_eq(old_props) {
|
|
153
190
|
self.initialize(ctx);
|
|
154
|
-
true
|
|
155
|
-
} else {
|
|
156
|
-
false
|
|
157
191
|
}
|
|
192
|
+
|
|
193
|
+
true
|
|
158
194
|
}
|
|
159
195
|
|
|
160
196
|
fn update(&mut self, ctx: &yew::prelude::Context<Self>, msg: Self::Message) -> bool {
|
|
@@ -187,6 +223,15 @@ impl Component for ColumnSettingsPanel {
|
|
|
187
223
|
self.save_enabled_effect();
|
|
188
224
|
true
|
|
189
225
|
},
|
|
226
|
+
ColumnSettingsPanelMsg::SetWindowValue(val) => {
|
|
227
|
+
if self.window_value != val {
|
|
228
|
+
self.window_value = val;
|
|
229
|
+
self.reset_enabled = true;
|
|
230
|
+
true
|
|
231
|
+
} else {
|
|
232
|
+
false
|
|
233
|
+
}
|
|
234
|
+
},
|
|
190
235
|
ColumnSettingsPanelMsg::SetSelectedTab((_, val)) => {
|
|
191
236
|
let rerender = ctx.props().selected_tab != Some(val);
|
|
192
237
|
ctx.props().on_select_tab.emit(val);
|
|
@@ -195,19 +240,58 @@ impl Component for ColumnSettingsPanel {
|
|
|
195
240
|
ColumnSettingsPanelMsg::OnResetAttributes(()) => {
|
|
196
241
|
self.header_value.clone_from(&self.initial_header_value);
|
|
197
242
|
self.expr_value.clone_from(&self.initial_expr_value);
|
|
243
|
+
self.window_value.clone_from(&self.initial_window_value);
|
|
198
244
|
self.save_enabled = false;
|
|
199
245
|
self.reset_enabled = false;
|
|
200
246
|
self.reset_count += 1;
|
|
201
247
|
true
|
|
202
248
|
},
|
|
203
249
|
ColumnSettingsPanelMsg::OnSaveAttributes(()) => {
|
|
250
|
+
if matches!(ctx.props().selected_tab, Some(ColumnSettingsTab::Window)) {
|
|
251
|
+
if let Some(spec) = self.window_value.clone() {
|
|
252
|
+
let name = self
|
|
253
|
+
.header_value
|
|
254
|
+
.clone()
|
|
255
|
+
.unwrap_or_else(|| self.column_name.clone());
|
|
256
|
+
match &ctx.props().selected_column {
|
|
257
|
+
ColumnLocator::Window(old_name) => update_window(
|
|
258
|
+
&ctx.props().session,
|
|
259
|
+
&ctx.props().renderer,
|
|
260
|
+
&ctx.props().presentation,
|
|
261
|
+
old_name.clone(),
|
|
262
|
+
name,
|
|
263
|
+
spec.clone(),
|
|
264
|
+
),
|
|
265
|
+
_ => {
|
|
266
|
+
if let Err(err) = save_window(
|
|
267
|
+
&ctx.props().session,
|
|
268
|
+
&ctx.props().renderer,
|
|
269
|
+
&ctx.props().presentation,
|
|
270
|
+
name,
|
|
271
|
+
spec.clone(),
|
|
272
|
+
) {
|
|
273
|
+
tracing::warn!("{}", err);
|
|
274
|
+
}
|
|
275
|
+
},
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
self.initial_window_value = Some(spec);
|
|
279
|
+
self.initial_header_value.clone_from(&self.header_value);
|
|
280
|
+
self.save_enabled = false;
|
|
281
|
+
self.reset_enabled = false;
|
|
282
|
+
self.save_count += 1;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return true;
|
|
286
|
+
}
|
|
287
|
+
|
|
204
288
|
let new_expr = Expression::new(
|
|
205
289
|
self.header_value.clone().map(|s| s.into()),
|
|
206
290
|
(*(self.expr_value)).clone().into(),
|
|
207
291
|
);
|
|
208
292
|
|
|
209
293
|
match &ctx.props().selected_column {
|
|
210
|
-
ColumnLocator::Table(_) => {
|
|
294
|
+
ColumnLocator::Table(_) | ColumnLocator::Window(_) => {
|
|
211
295
|
tracing::error!("Tried to save non-expression column!")
|
|
212
296
|
},
|
|
213
297
|
ColumnLocator::Expression(name) => update_expr(
|
|
@@ -244,6 +328,13 @@ impl Component for ColumnSettingsPanel {
|
|
|
244
328
|
&self.column_name,
|
|
245
329
|
)
|
|
246
330
|
.unwrap_or_log();
|
|
331
|
+
} else if ctx.props().selected_column.is_saved_window() {
|
|
332
|
+
delete_window(
|
|
333
|
+
&ctx.props().session,
|
|
334
|
+
&ctx.props().renderer,
|
|
335
|
+
&self.column_name,
|
|
336
|
+
)
|
|
337
|
+
.unwrap_or_log();
|
|
247
338
|
}
|
|
248
339
|
|
|
249
340
|
ctx.props().on_close.emit(());
|
|
@@ -253,15 +344,24 @@ impl Component for ColumnSettingsPanel {
|
|
|
253
344
|
}
|
|
254
345
|
|
|
255
346
|
fn view(&self, ctx: &yew::prelude::Context<Self>) -> Html {
|
|
347
|
+
let is_window_tab = matches!(ctx.props().selected_tab, Some(ColumnSettingsTab::Window));
|
|
348
|
+
|
|
349
|
+
let header_placeholder = if is_window_tab {
|
|
350
|
+
Rc::new(self.column_name.clone())
|
|
351
|
+
} else {
|
|
352
|
+
self.expr_value.clone()
|
|
353
|
+
};
|
|
354
|
+
|
|
256
355
|
let header_props = props!(EditableHeaderProps {
|
|
257
356
|
initial_value: self.initial_header_value.clone(),
|
|
258
|
-
placeholder:
|
|
357
|
+
placeholder: header_placeholder,
|
|
259
358
|
reset_count: self.reset_count,
|
|
260
|
-
editable: ctx.props().selected_column.is_expr()
|
|
359
|
+
editable: (ctx.props().selected_column.is_expr()
|
|
261
360
|
&& matches!(
|
|
262
361
|
ctx.props().selected_tab,
|
|
263
362
|
Some(ColumnSettingsTab::Attributes)
|
|
264
|
-
)
|
|
363
|
+
))
|
|
364
|
+
|| (ctx.props().selected_column.is_window_editable() && is_window_tab),
|
|
265
365
|
update_on_input: true,
|
|
266
366
|
icon_type: self
|
|
267
367
|
.maybe_ty
|
|
@@ -324,7 +424,26 @@ impl Component for ColumnSettingsPanel {
|
|
|
324
424
|
|
|
325
425
|
let attrs_tab = AttributesTabProps {
|
|
326
426
|
expr_editor,
|
|
327
|
-
save_section,
|
|
427
|
+
save_section: save_section.clone(),
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
let window_changed = self.window_value != self.initial_window_value
|
|
431
|
+
|| self.header_value != self.initial_header_value;
|
|
432
|
+
let window_tab = WindowTabProps {
|
|
433
|
+
editor: WindowEditorProps {
|
|
434
|
+
metadata: ctx.props().metadata.clone(),
|
|
435
|
+
initial: self.initial_window_value.clone(),
|
|
436
|
+
on_change: ctx.link().callback(ColumnSettingsPanelMsg::SetWindowValue),
|
|
437
|
+
reset_count: self.reset_count,
|
|
438
|
+
presentation: ctx.props().presentation.clone(),
|
|
439
|
+
session: ctx.props().session.clone(),
|
|
440
|
+
selected_theme: ctx.props().selected_theme.clone(),
|
|
441
|
+
},
|
|
442
|
+
save_section: SaveSettingsProps {
|
|
443
|
+
save_enabled: self.window_value.is_some() && window_changed && self.header_valid,
|
|
444
|
+
show_danger_zone: ctx.props().selected_column.is_saved_window(),
|
|
445
|
+
..save_section
|
|
446
|
+
},
|
|
328
447
|
};
|
|
329
448
|
|
|
330
449
|
let style_tab = StyleTabProps {
|
|
@@ -342,6 +461,7 @@ impl Component for ColumnSettingsPanel {
|
|
|
342
461
|
|
|
343
462
|
let tab_children = self.tabs.iter().map(|tab| match tab {
|
|
344
463
|
ColumnSettingsTab::Attributes => html! { <AttributesTab ..attrs_tab.clone() /> },
|
|
464
|
+
ColumnSettingsTab::Window => html! { <WindowTab ..window_tab.clone() /> },
|
|
345
465
|
ColumnSettingsTab::Style => html! { <StyleTab ..style_tab.clone() /> },
|
|
346
466
|
});
|
|
347
467
|
|
|
@@ -358,6 +478,10 @@ impl Component for ColumnSettingsPanel {
|
|
|
358
478
|
on_close={ctx.props().on_close.clone()}
|
|
359
479
|
id_prefix="column_settings"
|
|
360
480
|
width_override={ctx.props().width_override}
|
|
481
|
+
auto_width={ctx.props().auto_width}
|
|
482
|
+
on_auto_width={ctx.props().on_auto_width.clone()}
|
|
483
|
+
is_pinned={ctx.props().is_pinned}
|
|
484
|
+
on_toggle_pin={Some(ctx.props().on_toggle_pin.clone())}
|
|
361
485
|
selected_tab={selected_tab_idx}
|
|
362
486
|
{header_props}
|
|
363
487
|
>
|
|
@@ -403,6 +527,16 @@ impl ColumnSettingsPanel {
|
|
|
403
527
|
.metadata
|
|
404
528
|
.locator_view_type(&ctx.props().selected_column);
|
|
405
529
|
|
|
530
|
+
// Specs are unnamed (the `windows` map key names them, and the
|
|
531
|
+
// editable header owns naming in the UI), so drafts and saved specs
|
|
532
|
+
// compare directly.
|
|
533
|
+
let initial_window_value = ctx
|
|
534
|
+
.props()
|
|
535
|
+
.selected_column
|
|
536
|
+
.name()
|
|
537
|
+
.and_then(|name| ctx.props().view_config.windows.get(name))
|
|
538
|
+
.cloned();
|
|
539
|
+
|
|
406
540
|
let tabs = {
|
|
407
541
|
let mut tabs = vec![];
|
|
408
542
|
let is_new_expr = ctx.props().selected_column.is_new_expr();
|
|
@@ -424,6 +558,17 @@ impl ColumnSettingsPanel {
|
|
|
424
558
|
tabs.push(ColumnSettingsTab::Attributes);
|
|
425
559
|
}
|
|
426
560
|
|
|
561
|
+
let supports_windows = ctx
|
|
562
|
+
.props()
|
|
563
|
+
.metadata
|
|
564
|
+
.get_features()
|
|
565
|
+
.map(|x| x.has_window_aggregates())
|
|
566
|
+
.unwrap_or_default();
|
|
567
|
+
|
|
568
|
+
if ctx.props().selected_column.is_window_editable() && supports_windows {
|
|
569
|
+
tabs.push(ColumnSettingsTab::Window);
|
|
570
|
+
}
|
|
571
|
+
|
|
427
572
|
tabs
|
|
428
573
|
};
|
|
429
574
|
|
|
@@ -440,6 +585,8 @@ impl ColumnSettingsPanel {
|
|
|
440
585
|
header_value: initial_header_value.clone(),
|
|
441
586
|
initial_header_value,
|
|
442
587
|
maybe_ty,
|
|
588
|
+
window_value: initial_window_value.clone(),
|
|
589
|
+
initial_window_value,
|
|
443
590
|
tabs,
|
|
444
591
|
header_valid: true,
|
|
445
592
|
on_input,
|
|
@@ -52,6 +52,21 @@ where
|
|
|
52
52
|
|
|
53
53
|
#[prop_or_default]
|
|
54
54
|
pub allow_duplicates: bool,
|
|
55
|
+
|
|
56
|
+
/// Single-slot mode: the list holds at most one item, a dragover
|
|
57
|
+
/// preview REPLACES the current item rather than inserting beside it,
|
|
58
|
+
/// and the trailing `EmptyColumn` autocomplete renders only while the
|
|
59
|
+
/// slot is empty.
|
|
60
|
+
#[prop_or_default]
|
|
61
|
+
pub single_slot: bool,
|
|
62
|
+
|
|
63
|
+
/// The in-flight drag is INVALID for this list (a parent-defined rule,
|
|
64
|
+
/// e.g. the window editor's Table-columns-only slots): the dragover
|
|
65
|
+
/// preview is suppressed and the invalid-X overlay renders instead,
|
|
66
|
+
/// like a duplicate drag over `group_by`/`split_by`. The parent's drop
|
|
67
|
+
/// handler is still responsible for ignoring the drop itself.
|
|
68
|
+
#[prop_or_default]
|
|
69
|
+
pub is_invalid: bool,
|
|
55
70
|
}
|
|
56
71
|
|
|
57
72
|
impl<T, U> PartialEq for DragDropListProps<T, U>
|
|
@@ -66,6 +81,8 @@ where
|
|
|
66
81
|
&& self.allow_duplicates == other.allow_duplicates
|
|
67
82
|
&& self.is_dragover == other.is_dragover
|
|
68
83
|
&& self.disabled == other.disabled
|
|
84
|
+
&& self.single_slot == other.single_slot
|
|
85
|
+
&& self.is_invalid == other.is_invalid
|
|
69
86
|
}
|
|
70
87
|
}
|
|
71
88
|
|
|
@@ -202,7 +219,51 @@ where
|
|
|
202
219
|
let invalid_drag: bool;
|
|
203
220
|
let mut valid_duplicate_drag = false;
|
|
204
221
|
|
|
205
|
-
let columns_html = {
|
|
222
|
+
let columns_html = if ctx.props().single_slot {
|
|
223
|
+
invalid_drag = ctx.props().is_invalid && ctx.props().is_dragover.is_some();
|
|
224
|
+
|
|
225
|
+
// Dragging the slot's own pill over its own slot is a no-op
|
|
226
|
+
// move - keep showing the pill instead of the drop preview
|
|
227
|
+
// (mirrors the multi-column branch's `is_self_move` handling).
|
|
228
|
+
let is_self_move = ctx
|
|
229
|
+
.props()
|
|
230
|
+
.presentation
|
|
231
|
+
.get_drag_target()
|
|
232
|
+
.map(|x| V::is_self_move(x))
|
|
233
|
+
.unwrap_or_default();
|
|
234
|
+
|
|
235
|
+
let close = ctx.props().parent.callback(|_| V::close(0));
|
|
236
|
+
let dragenter = ctx.props().parent.callback({
|
|
237
|
+
let container_noderef = container_noderef.clone();
|
|
238
|
+
move |event: DragEvent| {
|
|
239
|
+
event.stop_propagation();
|
|
240
|
+
event.prevent_default();
|
|
241
|
+
if event.related_target().is_none()
|
|
242
|
+
&& let Some(elem) = container_noderef.cast::<HtmlElement>()
|
|
243
|
+
{
|
|
244
|
+
let _ = elem.dataset().set("safaridragleave", "true");
|
|
245
|
+
}
|
|
246
|
+
V::dragenter(0)
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
if ctx.props().is_dragover.is_some() && !is_self_move && !invalid_drag {
|
|
251
|
+
html! {
|
|
252
|
+
<div class="pivot-column" ondragenter={dragenter}>
|
|
253
|
+
<div class="config-drop" />
|
|
254
|
+
</div>
|
|
255
|
+
}
|
|
256
|
+
} else if let Some(column) = ctx.props().children.iter().next() {
|
|
257
|
+
html! {
|
|
258
|
+
<div class="pivot-column" ondragenter={dragenter}>
|
|
259
|
+
{ Html::from(column) }
|
|
260
|
+
<span class="row_close" onmousedown={close} />
|
|
261
|
+
</div>
|
|
262
|
+
}
|
|
263
|
+
} else {
|
|
264
|
+
html! {}
|
|
265
|
+
}
|
|
266
|
+
} else {
|
|
206
267
|
let mut columns = ctx
|
|
207
268
|
.props()
|
|
208
269
|
.children
|
|
@@ -211,7 +272,10 @@ where
|
|
|
211
272
|
.enumerate()
|
|
212
273
|
.collect::<Vec<_>>();
|
|
213
274
|
|
|
214
|
-
invalid_drag = if
|
|
275
|
+
invalid_drag = if ctx.props().is_invalid && ctx.props().is_dragover.is_some() {
|
|
276
|
+
// Parent-defined invalidity: no preview, X overlay only.
|
|
277
|
+
true
|
|
278
|
+
} else if let Some((x, column)) = &ctx.props().is_dragover {
|
|
215
279
|
let index = *x;
|
|
216
280
|
let is_append = index == columns.len();
|
|
217
281
|
let is_self_move = ctx
|
|
@@ -314,6 +378,12 @@ where
|
|
|
314
378
|
.collect::<Html>()
|
|
315
379
|
};
|
|
316
380
|
|
|
381
|
+
let show_empty = if ctx.props().single_slot {
|
|
382
|
+
ctx.props().children.is_empty() && ctx.props().is_dragover.is_none()
|
|
383
|
+
} else {
|
|
384
|
+
ctx.props().is_dragover.is_none() | (!invalid_drag && valid_duplicate_drag)
|
|
385
|
+
};
|
|
386
|
+
|
|
317
387
|
let column_dropdown = ctx.props().column_dropdown.clone();
|
|
318
388
|
let exclude = ctx.props().exclude.clone();
|
|
319
389
|
let on_select = ctx.props().parent.callback(V::create);
|
|
@@ -345,7 +415,7 @@ where
|
|
|
345
415
|
onmousedown={ctx.props().parent.callback(move |_| V::close(0))}
|
|
346
416
|
/>
|
|
347
417
|
</div>
|
|
348
|
-
} else if
|
|
418
|
+
} else if show_empty {
|
|
349
419
|
<EmptyColumn {column_dropdown} {exclude} {on_select} />
|
|
350
420
|
} else if invalid_drag {
|
|
351
421
|
<InvalidColumn />
|
|
@@ -11,14 +11,17 @@
|
|
|
11
11
|
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
|
12
12
|
|
|
13
13
|
use perspective_client::clone;
|
|
14
|
-
use
|
|
14
|
+
use wasm_bindgen::JsCast;
|
|
15
|
+
use wasm_bindgen::prelude::Closure;
|
|
16
|
+
use web_sys::HtmlElement;
|
|
15
17
|
use yew::{
|
|
16
|
-
Callback, Children, Html, Properties, function_component, html, use_effect_with,
|
|
17
|
-
|
|
18
|
+
Callback, Children, Html, Properties, function_component, html, use_effect_with, use_mut_ref,
|
|
19
|
+
use_node_ref,
|
|
18
20
|
};
|
|
19
21
|
|
|
20
22
|
use crate::components::containers::sidebar_close_button::SidebarCloseButton;
|
|
21
23
|
use crate::components::editable_header::{EditableHeader, EditableHeaderProps};
|
|
24
|
+
use crate::js::{ResizeObserver, ResizeObserverEntry};
|
|
22
25
|
|
|
23
26
|
#[derive(PartialEq, Clone, Properties)]
|
|
24
27
|
pub struct SidebarProps {
|
|
@@ -31,6 +34,28 @@ pub struct SidebarProps {
|
|
|
31
34
|
pub width_override: Option<i32>,
|
|
32
35
|
pub selected_tab: Option<usize>,
|
|
33
36
|
pub header_props: EditableHeaderProps,
|
|
37
|
+
|
|
38
|
+
/// Trap-door width shared across this sidebar's tabs: the lifted
|
|
39
|
+
/// running max of the widths this component reports through
|
|
40
|
+
/// `on_auto_width`. Held by the parent (ultimately
|
|
41
|
+
/// `PerspectiveViewer`'s geometry state, like the settings panel's
|
|
42
|
+
/// Query/Plugin/Debug trap-door) so it survives tab switches AND
|
|
43
|
+
/// sidebar re-mounts, and clears on divider reset.
|
|
44
|
+
#[prop_or_default]
|
|
45
|
+
pub auto_width: f64,
|
|
46
|
+
|
|
47
|
+
/// Fires with the sidebar's rendered width after each render; the
|
|
48
|
+
/// owner keeps the running max threaded back as `auto_width`.
|
|
49
|
+
#[prop_or_default]
|
|
50
|
+
pub on_auto_width: Callback<f64>,
|
|
51
|
+
|
|
52
|
+
/// Pinned state for the header's pin toggle; the button renders only
|
|
53
|
+
/// when `on_toggle_pin` is provided.
|
|
54
|
+
#[prop_or_default]
|
|
55
|
+
pub is_pinned: bool,
|
|
56
|
+
|
|
57
|
+
#[prop_or_default]
|
|
58
|
+
pub on_toggle_pin: Option<Callback<()>>,
|
|
34
59
|
}
|
|
35
60
|
|
|
36
61
|
/// Sidebars are designed to live in a
|
|
@@ -39,33 +64,85 @@ pub struct SidebarProps {
|
|
|
39
64
|
pub fn Sidebar(p: &SidebarProps) -> Html {
|
|
40
65
|
let id = &p.id_prefix;
|
|
41
66
|
let noderef = use_node_ref();
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
67
|
+
|
|
68
|
+
// The trap-door reports the sidebar's rendered width to its owner via
|
|
69
|
+
// a `ResizeObserver` on the sidebar element, NOT a render effect:
|
|
70
|
+
// width changes are driven by DOM mutations anywhere in the tab
|
|
71
|
+
// subtree (e.g. the window editor staging a long column name into a
|
|
72
|
+
// slot), which need not re-render this component at all. The observer
|
|
73
|
+
// sees every one. `contentRect` (not the border box) is load-bearing:
|
|
74
|
+
// the sizer below is a content child, so ratcheting the border box
|
|
75
|
+
// would feed any sidebar padding back into unbounded growth. A manual
|
|
76
|
+
// divider drag (`width_override`) disables the ratchet until the
|
|
77
|
+
// divider resets.
|
|
78
|
+
let live_props = use_mut_ref(|| (Callback::<f64>::default(), None::<i32>));
|
|
79
|
+
*live_props.borrow_mut() = (p.on_auto_width.clone(), p.width_override);
|
|
80
|
+
use_effect_with((), {
|
|
81
|
+
clone!(noderef, live_props);
|
|
82
|
+
move |_| {
|
|
83
|
+
let closure =
|
|
84
|
+
Closure::<dyn FnMut(js_sys::Array)>::new(move |entries: js_sys::Array| {
|
|
85
|
+
let (on_auto_width, width_override) = live_props.borrow().clone();
|
|
86
|
+
if width_override.is_none() {
|
|
87
|
+
for entry in entries.iter() {
|
|
88
|
+
let entry: ResizeObserverEntry = entry.unchecked_into();
|
|
89
|
+
on_auto_width.emit(entry.content_rect().width());
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
let observer = ResizeObserver::new(closure.as_ref().unchecked_ref());
|
|
95
|
+
let elem = noderef.cast::<HtmlElement>();
|
|
96
|
+
if let Some(elem) = &elem {
|
|
97
|
+
observer.observe(elem);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
move || {
|
|
101
|
+
if let Some(elem) = &elem {
|
|
102
|
+
observer.unobserve(elem);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
drop(closure);
|
|
59
106
|
}
|
|
60
107
|
}
|
|
61
108
|
});
|
|
62
109
|
|
|
63
|
-
let
|
|
110
|
+
let auto_width = if p.width_override.is_none() {
|
|
111
|
+
p.auto_width
|
|
112
|
+
} else {
|
|
113
|
+
0.0
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
let width_style = format!("min-width: 200px; width: {}px", auto_width);
|
|
117
|
+
let pin_button = p.on_toggle_pin.as_ref().map(|cb| {
|
|
118
|
+
let onclick = {
|
|
119
|
+
let cb = cb.clone();
|
|
120
|
+
Callback::from(move |_: web_sys::MouseEvent| cb.emit(()))
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
let mut class = yew::classes!("sidebar_pin_button");
|
|
124
|
+
if p.is_pinned {
|
|
125
|
+
class.push("is-pinned");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
html! {
|
|
129
|
+
<span
|
|
130
|
+
id={format!("{id}_pin_button")}
|
|
131
|
+
{class}
|
|
132
|
+
title={if p.is_pinned { "Unpin" } else { "Pin" }}
|
|
133
|
+
{onclick}
|
|
134
|
+
/>
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
64
138
|
html! {
|
|
65
139
|
<>
|
|
66
140
|
<SidebarCloseButton id={format!("{id}_close_button")} on_close_sidebar={&p.on_close} />
|
|
67
141
|
<div class="sidebar_column" id={format!("{id}_sidebar")} ref={noderef}>
|
|
68
|
-
<div class="sidebar_header"
|
|
142
|
+
<div class="sidebar_header">
|
|
143
|
+
<EditableHeader ..p.header_props.clone() />
|
|
144
|
+
{ pin_button }
|
|
145
|
+
</div>
|
|
69
146
|
<div class="sidebar_border" id={format!("{id}_border")} />
|
|
70
147
|
{ p.children.iter().collect::<Html>() }
|
|
71
148
|
<div class="sidebar-auto-width" style={width_style} />
|
|
@@ -26,6 +26,10 @@ pub struct SplitPanelProps {
|
|
|
26
26
|
#[prop_or_default]
|
|
27
27
|
pub id: Option<String>,
|
|
28
28
|
|
|
29
|
+
/// Extra classes on the root element (ignored under `no_wrap`).
|
|
30
|
+
#[prop_or_default]
|
|
31
|
+
pub class: Classes,
|
|
32
|
+
|
|
29
33
|
#[prop_or_default]
|
|
30
34
|
pub orientation: Orientation,
|
|
31
35
|
|
|
@@ -110,6 +114,7 @@ impl SplitPanelProps {
|
|
|
110
114
|
impl PartialEq for SplitPanelProps {
|
|
111
115
|
fn eq(&self, other: &Self) -> bool {
|
|
112
116
|
self.id == other.id
|
|
117
|
+
&& self.class == other.class
|
|
113
118
|
&& self.children == other.children
|
|
114
119
|
&& self.orientation == other.orientation
|
|
115
120
|
&& self.reverse == other.reverse
|
|
@@ -305,6 +310,7 @@ impl Component for SplitPanel {
|
|
|
305
310
|
}
|
|
306
311
|
|
|
307
312
|
let mut classes = classes!("split-panel");
|
|
313
|
+
classes.extend(ctx.props().class.clone());
|
|
308
314
|
if orientation == Orientation::Vertical {
|
|
309
315
|
classes.push("orient-vertical");
|
|
310
316
|
}
|
|
@@ -119,10 +119,12 @@ impl Component for EditableHeader {
|
|
|
119
119
|
}
|
|
120
120
|
let metadata = &ctx.props().metadata;
|
|
121
121
|
let expressions = metadata.get_expression_columns();
|
|
122
|
+
let windows = metadata.get_window_columns();
|
|
122
123
|
let found = metadata
|
|
123
124
|
.get_table_columns()?
|
|
124
125
|
.iter()
|
|
125
126
|
.chain(expressions)
|
|
127
|
+
.chain(windows)
|
|
126
128
|
.contains(&new_value);
|
|
127
129
|
Some(!found)
|
|
128
130
|
})()
|
|
@@ -291,6 +291,11 @@ impl Component for MainPanel {
|
|
|
291
291
|
.link()
|
|
292
292
|
.callback(|(id, x, y)| MainPanelMsg::ContextMenu(id, x, y));
|
|
293
293
|
let contextmenu_listener = Closure::wrap(Box::new(move |event: web_sys::Event| {
|
|
294
|
+
// Shift+right-click passes through to the native browser menu.
|
|
295
|
+
if event.unchecked_ref::<web_sys::MouseEvent>().shift_key() {
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
|
|
294
299
|
let path = event.composed_path();
|
|
295
300
|
let mut panel_id = None;
|
|
296
301
|
for i in 0..path.length() {
|