@perspective-dev/viewer 5.4.0 → 5.5.1
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 +4 -4
- package/dist/cdn/perspective-viewer.js.map +2 -2
- package/dist/css/blueprint.css +1 -1
- package/dist/css/botanical.css +1 -1
- package/dist/css/dracula.css +1 -1
- package/dist/css/eggplant.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/ar.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/ledger.css +1 -1
- package/dist/css/monokai.css +1 -1
- package/dist/css/nord.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/css/velvet.css +1 -1
- package/dist/docs/perspective-docs.json +1 -1
- package/dist/esm/perspective-viewer.d.ts +2 -0
- package/dist/esm/perspective-viewer.inline.js +9 -9
- package/dist/esm/perspective-viewer.inline.js.map +3 -3
- package/dist/esm/perspective-viewer.js +4 -4
- package/dist/esm/perspective-viewer.js.map +2 -2
- package/dist/esm/plugin.d.ts +9 -0
- package/dist/esm/ts-rs/AddPanelOptions.d.ts +9 -0
- package/dist/esm/ts-rs/ExportMethod.d.ts +1 -1
- package/dist/esm/ts-rs/GetViewMode.d.ts +4 -0
- package/dist/esm/ts-rs/GetViewOptions.d.ts +8 -0
- package/dist/esm/ts-rs/RestoreOptions.d.ts +6 -0
- package/dist/esm/ts-rs/RestoreWorkspaceOptions.d.ts +9 -0
- package/dist/esm/ts-rs/WorkspaceConfigUpdate.d.ts +30 -22
- package/dist/wasm/perspective-viewer.d.ts +102 -47
- package/dist/wasm/perspective-viewer.js +233 -142
- package/dist/wasm/perspective-viewer.wasm +0 -0
- package/dist/wasm/perspective-viewer.wasm.d.ts +18 -18
- package/package.json +7 -7
- package/src/css/column-selector.css +11 -3
- package/src/css/column-style.css +162 -0
- package/src/css/config-selector.css +1 -1
- package/src/css/form/debug.css +9 -0
- package/src/css/status-bar.css +11 -0
- package/src/css/ui/containers/tabs.css +2 -1
- package/src/rust/agent/tools.rs +80 -7
- package/src/rust/components/column_selector/active_column.rs +9 -0
- package/src/rust/components/column_selector.rs +11 -0
- package/src/rust/components/column_settings_sidebar/style_tab/alignment_field.rs +90 -0
- package/src/rust/components/column_settings_sidebar/style_tab/font_field.rs +267 -0
- package/src/rust/components/column_settings_sidebar/style_tab/primitive_field.rs +1 -1
- package/src/rust/components/column_settings_sidebar/style_tab.rs +68 -9
- package/src/rust/components/debug_panel.rs +107 -50
- package/src/rust/components/export_dropdown.rs +6 -0
- package/src/rust/components/main_panel/msg.rs +4 -0
- package/src/rust/components/main_panel.rs +14 -0
- package/src/rust/components/mod.rs +0 -1
- package/src/rust/components/plugin_tab.rs +57 -3
- package/src/rust/components/settings_panel.rs +6 -0
- package/src/rust/components/status_bar.rs +12 -3
- package/src/rust/components/status_indicator.rs +4 -0
- package/src/rust/components/viewer/filters.rs +2 -2
- package/src/rust/components/viewer/msg.rs +4 -0
- package/src/rust/components/viewer/panels.rs +6 -3
- package/src/rust/components/viewer/render.rs +1 -0
- package/src/rust/components/viewer/wiring.rs +14 -2
- package/src/rust/components/viewer.rs +2 -2
- package/src/rust/config/column_config_schema.rs +312 -8
- package/src/rust/config/datetime_column_style.rs +0 -11
- package/src/rust/config/export_method.rs +24 -0
- package/src/rust/config/mod.rs +0 -2
- package/src/rust/config/options.rs +50 -0
- package/src/rust/config/workspace_config.rs +69 -24
- package/src/rust/custom_elements/viewer.rs +214 -113
- package/src/rust/js/plugin.rs +3 -1
- package/src/rust/lib.rs +7 -3
- package/src/rust/queries/plugin_column_styles.rs +6 -5
- package/src/rust/renderer/plugin_config.rs +200 -41
- package/src/rust/renderer/plugin_store.rs +2 -1
- package/src/rust/renderer/props.rs +5 -0
- package/src/rust/renderer.rs +9 -0
- package/src/rust/session/metadata.rs +5 -10
- package/src/rust/session/props.rs +4 -0
- package/src/rust/session.rs +273 -195
- package/src/rust/tasks/copy_export.rs +67 -0
- package/src/rust/tasks/create_panel.rs +16 -13
- package/src/rust/tasks/pipeline.rs +26 -8
- package/src/rust/tasks/restore_panel.rs +129 -30
- package/src/rust/tasks/send_column_config.rs +3 -0
- package/src/rust/tasks/sync_update_panels.rs +148 -68
- package/src/rust/tasks/table_lifecycle.rs +7 -1
- package/src/rust/ui/containers/tab_list.rs +1 -1
- package/src/rust/ui/form/intl_label.rs +4 -23
- package/src/rust/ui/portal.rs +1 -0
- package/src/rust/utils/css_vars.rs +13 -0
- package/src/rust/{config/datetime_column_style/color_mode.rs → utils/in_flight.rs} +47 -35
- package/src/rust/utils/local_fonts.rs +395 -0
- package/src/rust/utils/mod.rs +4 -0
- package/src/rust/utils/pending_effects.rs +6 -30
- package/src/rust/workspace.rs +12 -0
- package/src/svg/status_pending.svg +3 -0
- package/src/themes/icons.css +1 -0
- package/src/themes/intl/ar.css +20 -7
- package/src/themes/intl/de.css +20 -7
- package/src/themes/intl/es.css +20 -7
- package/src/themes/intl/fr.css +20 -7
- package/src/themes/intl/ja.css +20 -7
- package/src/themes/intl/pt.css +20 -7
- package/src/themes/intl/zh.css +20 -7
- package/src/themes/intl.css +20 -7
- package/src/themes/monokai.css +1 -1
- package/src/themes/vaporwave.css +1 -1
- package/src/ts/perspective-viewer.ts +2 -0
- package/src/ts/plugin.ts +9 -0
- package/src/ts/ts-rs/AddPanelOptions.ts +10 -0
- package/src/ts/ts-rs/ExportMethod.ts +1 -1
- package/src/ts/ts-rs/{DatetimeColorMode.ts → GetViewMode.ts} +4 -1
- package/src/ts/ts-rs/GetViewOptions.ts +7 -0
- package/src/ts/ts-rs/RestoreOptions.ts +7 -1
- package/src/ts/ts-rs/RestoreWorkspaceOptions.ts +10 -0
- package/src/ts/ts-rs/WorkspaceConfigUpdate.ts +31 -20
- package/dist/esm/ts-rs/DatetimeColorMode.d.ts +0 -1
- package/dist/esm/ts-rs/FormatMode.d.ts +0 -1
- package/dist/esm/ts-rs/StringColorMode.d.ts +0 -1
- package/src/rust/components/string_column_style.rs +0 -120
- package/src/rust/config/string_column_style.rs +0 -135
- package/src/ts/ts-rs/FormatMode.ts +0 -3
- package/src/ts/ts-rs/StringColorMode.ts +0 -3
- /package/dist/wasm/snippets/{perspective-viewer-adc6f3f79141112b → perspective-viewer-6813b568219c6763}/inline0.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-adc6f3f79141112b → perspective-viewer-6813b568219c6763}/inline1.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-adc6f3f79141112b → perspective-viewer-6813b568219c6763}/inline2.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-adc6f3f79141112b → perspective-viewer-6813b568219c6763}/inline3.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-adc6f3f79141112b → perspective-viewer-6813b568219c6763}/inline4.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-adc6f3f79141112b → perspective-viewer-6813b568219c6763}/inline5.js +0 -0
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
|
2
|
+
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
|
3
|
+
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
|
4
|
+
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
|
5
|
+
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
|
6
|
+
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
|
7
|
+
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
|
8
|
+
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
|
9
|
+
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
|
10
|
+
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
|
11
|
+
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
|
12
|
+
|
|
13
|
+
use std::rc::Rc;
|
|
14
|
+
|
|
15
|
+
use serde_json::Value;
|
|
16
|
+
use web_sys::{HtmlInputElement, InputEvent, MouseEvent};
|
|
17
|
+
use yew::prelude::*;
|
|
18
|
+
|
|
19
|
+
use super::primitive_field::emit;
|
|
20
|
+
use crate::config::{ColumnConfigFieldUpdate, FontSize, FontToggle};
|
|
21
|
+
use crate::ui::{OptionalField, Select, SelectItem};
|
|
22
|
+
use crate::utils::{
|
|
23
|
+
LocalFontsAccess, font_family_options, local_fonts_access, on_local_fonts_changed,
|
|
24
|
+
probe_local_fonts_access, read_intl_string, request_local_fonts_access,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
#[derive(Properties, PartialEq)]
|
|
28
|
+
pub struct FontFieldProps {
|
|
29
|
+
pub field_key: String,
|
|
30
|
+
pub default: String,
|
|
31
|
+
pub current: Option<String>,
|
|
32
|
+
|
|
33
|
+
/// The size input's key, default and bounds, when the spec declares
|
|
34
|
+
/// one, and the stored value for that key.
|
|
35
|
+
#[prop_or_default]
|
|
36
|
+
pub size: Option<FontSize>,
|
|
37
|
+
|
|
38
|
+
#[prop_or_default]
|
|
39
|
+
pub size_current: Option<f64>,
|
|
40
|
+
|
|
41
|
+
/// The bold toggle's key and default, when the spec declares one, and
|
|
42
|
+
/// the stored value for that key.
|
|
43
|
+
#[prop_or_default]
|
|
44
|
+
pub bold: Option<FontToggle>,
|
|
45
|
+
|
|
46
|
+
#[prop_or_default]
|
|
47
|
+
pub bold_current: Option<bool>,
|
|
48
|
+
|
|
49
|
+
#[prop_or_default]
|
|
50
|
+
pub italic: Option<FontToggle>,
|
|
51
|
+
|
|
52
|
+
#[prop_or_default]
|
|
53
|
+
pub italic_current: Option<bool>,
|
|
54
|
+
pub on_change: Callback<ColumnConfigFieldUpdate>,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/// The `ControlSpec::Font` widget: a family `<select>`, the optional size
|
|
58
|
+
/// input and bold / italic toggles, and a "Local Fonts" action while the
|
|
59
|
+
/// `local-fonts` permission is promptable.
|
|
60
|
+
#[function_component]
|
|
61
|
+
pub fn FontField(props: &FontFieldProps) -> Html {
|
|
62
|
+
let force_update = use_force_update();
|
|
63
|
+
use_effect_with((), move |_| {
|
|
64
|
+
let sub = on_local_fonts_changed(Callback::from(move |()| force_update.force_update()));
|
|
65
|
+
probe_local_fonts_access();
|
|
66
|
+
move || drop(sub)
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
let selected = props
|
|
70
|
+
.current
|
|
71
|
+
.clone()
|
|
72
|
+
.unwrap_or_else(|| props.default.clone());
|
|
73
|
+
|
|
74
|
+
let toggle_state = |toggle: &Option<FontToggle>, current: Option<bool>| {
|
|
75
|
+
toggle
|
|
76
|
+
.as_ref()
|
|
77
|
+
.map(|t| (t.clone(), current.unwrap_or(t.default)))
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
let bold = toggle_state(&props.bold, props.bold_current);
|
|
81
|
+
let italic = toggle_state(&props.italic, props.italic_current);
|
|
82
|
+
let family_checked = selected != props.default;
|
|
83
|
+
let style_checked = props
|
|
84
|
+
.size
|
|
85
|
+
.as_ref()
|
|
86
|
+
.is_some_and(|s| props.size_current.is_some_and(|v| v != s.default))
|
|
87
|
+
|| bold.as_ref().is_some_and(|(t, on)| *on != t.default)
|
|
88
|
+
|| italic.as_ref().is_some_and(|(t, on)| *on != t.default);
|
|
89
|
+
|
|
90
|
+
let values: Rc<Vec<SelectItem<String>>> = Rc::new(
|
|
91
|
+
font_family_options(&selected)
|
|
92
|
+
.into_iter()
|
|
93
|
+
.map(SelectItem::Option)
|
|
94
|
+
.collect(),
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
let on_select = use_callback(
|
|
98
|
+
(
|
|
99
|
+
props.field_key.clone(),
|
|
100
|
+
props.default.clone(),
|
|
101
|
+
props.on_change.clone(),
|
|
102
|
+
),
|
|
103
|
+
|value: String, (key, default, on_change)| {
|
|
104
|
+
if value == *default {
|
|
105
|
+
emit(on_change, key, None);
|
|
106
|
+
} else {
|
|
107
|
+
emit(on_change, key, Some(Value::String(value)));
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
let on_reset_style = use_callback(
|
|
113
|
+
(
|
|
114
|
+
props.size.clone(),
|
|
115
|
+
props.bold.clone(),
|
|
116
|
+
props.italic.clone(),
|
|
117
|
+
props.field_key.clone(),
|
|
118
|
+
props.on_change.clone(),
|
|
119
|
+
),
|
|
120
|
+
|_: MouseEvent, (size, bold, italic, key, on_change)| {
|
|
121
|
+
let keys = [
|
|
122
|
+
size.as_ref().map(|s| s.key.clone()),
|
|
123
|
+
bold.as_ref().map(|t| t.key.clone()),
|
|
124
|
+
italic.as_ref().map(|t| t.key.clone()),
|
|
125
|
+
Some(key.clone()),
|
|
126
|
+
]
|
|
127
|
+
.into_iter()
|
|
128
|
+
.flatten()
|
|
129
|
+
.collect();
|
|
130
|
+
|
|
131
|
+
on_change.emit(ColumnConfigFieldUpdate {
|
|
132
|
+
keys,
|
|
133
|
+
value: serde_json::Map::new(),
|
|
134
|
+
});
|
|
135
|
+
},
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
let size_input = props.size.as_ref().map(|size| {
|
|
139
|
+
let on_input = {
|
|
140
|
+
let on_change = props.on_change.clone();
|
|
141
|
+
let size = size.clone();
|
|
142
|
+
Callback::from(move |event: InputEvent| {
|
|
143
|
+
let value = event
|
|
144
|
+
.target_unchecked_into::<HtmlInputElement>()
|
|
145
|
+
.value_as_number();
|
|
146
|
+
let value = (value.is_finite() && value != size.default)
|
|
147
|
+
.then(|| serde_json::Number::from_f64(value).map(Value::Number))
|
|
148
|
+
.flatten();
|
|
149
|
+
|
|
150
|
+
emit(&on_change, &size.key, value);
|
|
151
|
+
})
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
html! {
|
|
155
|
+
<input
|
|
156
|
+
type="number"
|
|
157
|
+
class="parameter font-size"
|
|
158
|
+
id={format!("{}-input", size.key)}
|
|
159
|
+
min={size.min.map(|v| v.to_string())}
|
|
160
|
+
max={size.max.map(|v| v.to_string())}
|
|
161
|
+
step={size.step.map(|v| v.to_string())}
|
|
162
|
+
value={props.size_current.unwrap_or(size.default).to_string()}
|
|
163
|
+
oninput={on_input}
|
|
164
|
+
/>
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
let toggle_button = |class: &'static str, state: &Option<(FontToggle, bool)>| {
|
|
169
|
+
let Some((toggle, on)) = state else {
|
|
170
|
+
return html! {};
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
let next = !*on;
|
|
174
|
+
let on_toggle = {
|
|
175
|
+
let on_change = props.on_change.clone();
|
|
176
|
+
let toggle = toggle.clone();
|
|
177
|
+
Callback::from(move |_: MouseEvent| {
|
|
178
|
+
let value = (next != toggle.default).then_some(Value::Bool(next));
|
|
179
|
+
emit(&on_change, &toggle.key, value);
|
|
180
|
+
})
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
html! {
|
|
184
|
+
<span
|
|
185
|
+
class={classes!("font-style-toggle", class)}
|
|
186
|
+
id={format!("{}-toggle", toggle.key)}
|
|
187
|
+
role="button"
|
|
188
|
+
aria-pressed={on.to_string()}
|
|
189
|
+
onclick={on_toggle}
|
|
190
|
+
/>
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
let access = local_fonts_access();
|
|
195
|
+
let action_ref = use_node_ref();
|
|
196
|
+
use_effect_with((action_ref.clone(), access), |(action_ref, access)| {
|
|
197
|
+
let Some(elem) = action_ref.cast::<web_sys::Element>() else {
|
|
198
|
+
return;
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
let (slug, fallback) = match access {
|
|
202
|
+
Some(LocalFontsAccess::Denied) => (
|
|
203
|
+
"local-fonts-blocked-title",
|
|
204
|
+
"Access to local fonts is blocked for this site. Allow it in the browser's site \
|
|
205
|
+
settings to list them here.",
|
|
206
|
+
),
|
|
207
|
+
_ => (
|
|
208
|
+
"request-local-fonts-title",
|
|
209
|
+
"Allow access to the fonts installed on this device",
|
|
210
|
+
),
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
let title = read_intl_string(&elem, slug).unwrap_or_else(|| fallback.to_owned());
|
|
214
|
+
let _ = elem.set_attribute("title", &title);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
let local_fonts = match access {
|
|
218
|
+
Some(LocalFontsAccess::Prompt) => {
|
|
219
|
+
let on_request = Callback::from(|_: MouseEvent| request_local_fonts_access());
|
|
220
|
+
html! {
|
|
221
|
+
<div class="local-fonts-controls">
|
|
222
|
+
<span
|
|
223
|
+
ref={action_ref}
|
|
224
|
+
class="local-fonts-request"
|
|
225
|
+
id="request-local-fonts"
|
|
226
|
+
onclick={on_request}
|
|
227
|
+
/>
|
|
228
|
+
</div>
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
Some(LocalFontsAccess::Denied) => html! {
|
|
232
|
+
<div class="local-fonts-controls">
|
|
233
|
+
<span
|
|
234
|
+
ref={action_ref}
|
|
235
|
+
class="local-fonts-request blocked"
|
|
236
|
+
id="request-local-fonts"
|
|
237
|
+
/>
|
|
238
|
+
</div>
|
|
239
|
+
},
|
|
240
|
+
_ => html! {},
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
html! {
|
|
244
|
+
<>
|
|
245
|
+
<div class="row">
|
|
246
|
+
{ local_fonts }
|
|
247
|
+
<OptionalField
|
|
248
|
+
label={props.field_key.clone()}
|
|
249
|
+
on_check={on_reset_style}
|
|
250
|
+
checked={family_checked || style_checked}
|
|
251
|
+
>
|
|
252
|
+
<div class="font-group">
|
|
253
|
+
<Select<String>
|
|
254
|
+
wrapper_class="font-group-family"
|
|
255
|
+
{values}
|
|
256
|
+
{selected}
|
|
257
|
+
{on_select}
|
|
258
|
+
/>
|
|
259
|
+
{ size_input }
|
|
260
|
+
{ toggle_button("bold", &bold) }
|
|
261
|
+
{ toggle_button("italic", &italic) }
|
|
262
|
+
</div>
|
|
263
|
+
</OptionalField>
|
|
264
|
+
</div>
|
|
265
|
+
</>
|
|
266
|
+
}
|
|
267
|
+
}
|
|
@@ -36,7 +36,7 @@ use crate::utils::{
|
|
|
36
36
|
canonicalize_css_color, gradient_to_css, palette_name_for,
|
|
37
37
|
};
|
|
38
38
|
|
|
39
|
-
fn emit(on_change: &Callback<ColumnConfigFieldUpdate>, key: &str, value: Option<Value>) {
|
|
39
|
+
pub(super) fn emit(on_change: &Callback<ColumnConfigFieldUpdate>, key: &str, value: Option<Value>) {
|
|
40
40
|
let mut map = serde_json::Map::new();
|
|
41
41
|
if let Some(v) = value {
|
|
42
42
|
map.insert(key.to_owned(), v);
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
|
12
12
|
|
|
13
13
|
mod agg_depth_selector;
|
|
14
|
+
pub(crate) mod alignment_field;
|
|
15
|
+
pub(crate) mod font_field;
|
|
14
16
|
pub(crate) mod primitive_field;
|
|
15
17
|
mod symbol;
|
|
16
18
|
|
|
@@ -22,17 +24,18 @@ use perspective_client::config::ColumnType;
|
|
|
22
24
|
use yew::{Callback, Html, Properties, function_component, html};
|
|
23
25
|
|
|
24
26
|
use self::agg_depth_selector::*;
|
|
27
|
+
use self::alignment_field::AlignmentField;
|
|
28
|
+
use self::font_field::FontField;
|
|
25
29
|
use self::primitive_field::{
|
|
26
30
|
BoolField, ColorField, EnumField, GradientStopsField, NumberFieldPrimitive, PaletteField,
|
|
27
31
|
};
|
|
28
32
|
use crate::components::column_settings_sidebar::style_tab::symbol::SymbolStyle;
|
|
29
33
|
use crate::components::datetime_column_style::DatetimeColumnStyle;
|
|
30
34
|
use crate::components::number_series_style::NumberSeriesStyle;
|
|
31
|
-
use crate::components::string_column_style::StringColumnStyle;
|
|
32
35
|
use crate::components::style_controls::CustomNumberFormat;
|
|
33
36
|
use crate::config::{
|
|
34
|
-
ColumnConfigFieldUpdate, ControlSpec, CustomNumberFormatConfig,
|
|
35
|
-
NumberFormatDefaults, NumberSeriesStyleConfig,
|
|
37
|
+
Alignment, ColumnConfigFieldUpdate, ControlSpec, CustomNumberFormatConfig,
|
|
38
|
+
DatetimeColumnStyleConfig, FontToggle, NumberFormatDefaults, NumberSeriesStyleConfig,
|
|
36
39
|
};
|
|
37
40
|
use crate::presentation::Presentation;
|
|
38
41
|
use crate::queries::{fetch_column_abs_max, get_column_config_schema, named_values};
|
|
@@ -270,12 +273,6 @@ fn render_leaf(spec: ControlSpec, keys: &[String], ctx: &FieldRenderCtx) -> Opti
|
|
|
270
273
|
/>
|
|
271
274
|
}
|
|
272
275
|
},
|
|
273
|
-
ControlSpec::StringFormat => {
|
|
274
|
-
let config: Option<StringColumnStyleConfig> = deser_sub(raw_config);
|
|
275
|
-
html! {
|
|
276
|
-
<StringColumnStyle {config} on_change={on_change.clone()} keys={keys.to_vec()} />
|
|
277
|
-
}
|
|
278
|
-
},
|
|
279
276
|
ControlSpec::Symbols {
|
|
280
277
|
default: default_config,
|
|
281
278
|
} => {
|
|
@@ -341,6 +338,68 @@ fn render_leaf(spec: ControlSpec, keys: &[String], ctx: &FieldRenderCtx) -> Opti
|
|
|
341
338
|
/>
|
|
342
339
|
}
|
|
343
340
|
},
|
|
341
|
+
ControlSpec::Font {
|
|
342
|
+
key,
|
|
343
|
+
default,
|
|
344
|
+
size,
|
|
345
|
+
bold,
|
|
346
|
+
italic,
|
|
347
|
+
} => {
|
|
348
|
+
let current = raw_config
|
|
349
|
+
.as_ref()
|
|
350
|
+
.and_then(|m| m.get(&key))
|
|
351
|
+
.and_then(|v| v.as_str().map(|s| s.to_string()));
|
|
352
|
+
|
|
353
|
+
let toggle = |t: &FontToggle| {
|
|
354
|
+
raw_config
|
|
355
|
+
.as_ref()
|
|
356
|
+
.and_then(|m| m.get(&t.key))
|
|
357
|
+
.and_then(|v| v.as_bool())
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
let size_current = size
|
|
361
|
+
.as_ref()
|
|
362
|
+
.and_then(|s| raw_config.as_ref().and_then(|m| m.get(&s.key)))
|
|
363
|
+
.and_then(|v| v.as_f64());
|
|
364
|
+
|
|
365
|
+
let bold_current = bold.as_ref().and_then(toggle);
|
|
366
|
+
let italic_current = italic.as_ref().and_then(toggle);
|
|
367
|
+
html! {
|
|
368
|
+
<FontField
|
|
369
|
+
field_key={key}
|
|
370
|
+
{default}
|
|
371
|
+
{current}
|
|
372
|
+
{size}
|
|
373
|
+
{size_current}
|
|
374
|
+
{bold}
|
|
375
|
+
{bold_current}
|
|
376
|
+
{italic}
|
|
377
|
+
{italic_current}
|
|
378
|
+
on_change={on_change.clone()}
|
|
379
|
+
/>
|
|
380
|
+
}
|
|
381
|
+
},
|
|
382
|
+
ControlSpec::Alignment {
|
|
383
|
+
key,
|
|
384
|
+
default,
|
|
385
|
+
corners,
|
|
386
|
+
} => {
|
|
387
|
+
let current = raw_config
|
|
388
|
+
.as_ref()
|
|
389
|
+
.and_then(|m| m.get(&key))
|
|
390
|
+
.and_then(|v| v.as_str())
|
|
391
|
+
.and_then(Alignment::parse);
|
|
392
|
+
|
|
393
|
+
html! {
|
|
394
|
+
<AlignmentField
|
|
395
|
+
field_key={key}
|
|
396
|
+
{default}
|
|
397
|
+
{corners}
|
|
398
|
+
{current}
|
|
399
|
+
on_change={on_change.clone()}
|
|
400
|
+
/>
|
|
401
|
+
}
|
|
402
|
+
},
|
|
344
403
|
ControlSpec::Bool { key, default } => {
|
|
345
404
|
let current = raw_config
|
|
346
405
|
.as_ref()
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
|
11
11
|
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
|
12
12
|
|
|
13
|
+
use std::cell::Cell;
|
|
13
14
|
use std::rc::Rc;
|
|
14
15
|
|
|
15
16
|
use perspective_client::ExprValidationError;
|
|
@@ -18,8 +19,10 @@ use wasm_bindgen::prelude::*;
|
|
|
18
19
|
use yew::prelude::*;
|
|
19
20
|
|
|
20
21
|
use crate::components::code_editor::CodeEditor;
|
|
22
|
+
use crate::config::*;
|
|
21
23
|
use crate::js::{MimeType, copy_to_clipboard, paste_from_clipboard};
|
|
22
24
|
use crate::presentation::*;
|
|
25
|
+
use crate::queries::fetch_hosted_tables;
|
|
23
26
|
use crate::renderer::*;
|
|
24
27
|
use crate::session::*;
|
|
25
28
|
use crate::utils::*;
|
|
@@ -44,12 +47,18 @@ pub struct DebugPanelProps {
|
|
|
44
47
|
pub on_auto_width: Callback<f64>,
|
|
45
48
|
}
|
|
46
49
|
|
|
50
|
+
/// Whether the editor holds text the panel has not successfully applied, read
|
|
51
|
+
/// synchronously by the config-change listeners so a failed apply's own
|
|
52
|
+
/// `view_config_changed` never discards it.
|
|
53
|
+
type Dirty = Rc<Cell<bool>>;
|
|
54
|
+
|
|
47
55
|
#[function_component(DebugPanel)]
|
|
48
56
|
pub fn debug_panel(props: &DebugPanelProps) -> Html {
|
|
49
57
|
let expr = use_state_eq(|| Rc::new("".to_string()));
|
|
50
58
|
let error = use_state_eq(|| Option::<ExprValidationError>::None);
|
|
51
59
|
let select_all = use_memo((), |()| PubSub::default());
|
|
52
60
|
let modified = use_state_eq(|| false);
|
|
61
|
+
let dirty: Dirty = use_memo((), |()| Cell::new(false));
|
|
53
62
|
|
|
54
63
|
// Measure natural width on mount and route up to `SettingsPanel`.
|
|
55
64
|
let sizer = use_node_ref();
|
|
@@ -64,7 +73,7 @@ pub fn debug_panel(props: &DebugPanelProps) -> Html {
|
|
|
64
73
|
});
|
|
65
74
|
|
|
66
75
|
use_effect_with((expr.setter(), props.clone()), {
|
|
67
|
-
clone!(error, modified);
|
|
76
|
+
clone!(error, modified, dirty);
|
|
68
77
|
move |(text, state)| {
|
|
69
78
|
state.set_text(text.clone());
|
|
70
79
|
error.set(None);
|
|
@@ -75,6 +84,7 @@ pub fn debug_panel(props: &DebugPanelProps) -> Html {
|
|
|
75
84
|
text.clone(),
|
|
76
85
|
error.setter(),
|
|
77
86
|
modified.setter(),
|
|
87
|
+
dirty.clone(),
|
|
78
88
|
));
|
|
79
89
|
|
|
80
90
|
let sub2 = state
|
|
@@ -84,6 +94,7 @@ pub fn debug_panel(props: &DebugPanelProps) -> Html {
|
|
|
84
94
|
text.clone(),
|
|
85
95
|
error.setter(),
|
|
86
96
|
modified.setter(),
|
|
97
|
+
dirty.clone(),
|
|
87
98
|
));
|
|
88
99
|
|
|
89
100
|
let sub3 = state
|
|
@@ -93,6 +104,7 @@ pub fn debug_panel(props: &DebugPanelProps) -> Html {
|
|
|
93
104
|
text.clone(),
|
|
94
105
|
error.setter(),
|
|
95
106
|
modified.setter(),
|
|
107
|
+
dirty.clone(),
|
|
96
108
|
));
|
|
97
109
|
|
|
98
110
|
|| {
|
|
@@ -104,16 +116,17 @@ pub fn debug_panel(props: &DebugPanelProps) -> Html {
|
|
|
104
116
|
});
|
|
105
117
|
|
|
106
118
|
let oninput = use_callback(expr.setter(), {
|
|
107
|
-
clone!(modified);
|
|
119
|
+
clone!(modified, dirty);
|
|
108
120
|
move |x, expr| {
|
|
121
|
+
dirty.set(true);
|
|
109
122
|
modified.set(true);
|
|
110
123
|
expr.set(x)
|
|
111
124
|
}
|
|
112
125
|
});
|
|
113
126
|
|
|
114
127
|
let onsave = use_callback((expr.clone(), error.clone(), props.clone()), {
|
|
115
|
-
clone!(modified);
|
|
116
|
-
move |_, (text, error, props)| props.on_save(text, error, &modified)
|
|
128
|
+
clone!(modified, dirty);
|
|
129
|
+
move |_, (text, error, props)| props.on_save(text, text.setter(), error, &modified, &dirty)
|
|
117
130
|
});
|
|
118
131
|
|
|
119
132
|
let oncopy = use_callback(
|
|
@@ -133,13 +146,14 @@ pub fn debug_panel(props: &DebugPanelProps) -> Html {
|
|
|
133
146
|
);
|
|
134
147
|
|
|
135
148
|
let onapply = use_callback((expr.clone(), error.clone(), props.clone()), {
|
|
136
|
-
clone!(modified);
|
|
137
|
-
move |_, (text, error, props)| props.on_save(text, error, &modified)
|
|
149
|
+
clone!(modified, dirty);
|
|
150
|
+
move |_, (text, error, props)| props.on_save(text, text.setter(), error, &modified, &dirty)
|
|
138
151
|
});
|
|
139
152
|
|
|
140
153
|
let onreset = use_callback((expr.setter(), error.clone(), props.clone()), {
|
|
141
|
-
clone!(modified);
|
|
154
|
+
clone!(modified, dirty);
|
|
142
155
|
move |_, (text, error, props)| {
|
|
156
|
+
dirty.set(false);
|
|
143
157
|
props.set_text(text.clone());
|
|
144
158
|
error.set(None);
|
|
145
159
|
modified.set(false);
|
|
@@ -147,16 +161,17 @@ pub fn debug_panel(props: &DebugPanelProps) -> Html {
|
|
|
147
161
|
});
|
|
148
162
|
|
|
149
163
|
let onpaste = use_callback((expr.clone(), error.clone(), props.clone()), {
|
|
150
|
-
clone!(modified);
|
|
164
|
+
clone!(modified, dirty);
|
|
151
165
|
move |_, (text, error, props)| {
|
|
152
|
-
clone!(text, error, props, modified);
|
|
166
|
+
clone!(text, error, props, modified, dirty);
|
|
153
167
|
ApiFuture::spawn(async move {
|
|
154
168
|
if let Some(x) = paste_from_clipboard().await {
|
|
155
169
|
let x = Rc::new(x);
|
|
170
|
+
dirty.set(true);
|
|
156
171
|
modified.set(true);
|
|
157
172
|
error.set(None);
|
|
158
173
|
text.set(x.clone());
|
|
159
|
-
props.on_save(&x, &error, &modified);
|
|
174
|
+
props.on_save(&x, text.setter(), &error, &modified, &dirty);
|
|
160
175
|
}
|
|
161
176
|
|
|
162
177
|
Ok(())
|
|
@@ -184,6 +199,9 @@ pub fn debug_panel(props: &DebugPanelProps) -> Html {
|
|
|
184
199
|
select_all={select_all.subscriber()}
|
|
185
200
|
error={(*error).clone()}
|
|
186
201
|
/>
|
|
202
|
+
if let Some(err) = &*error {
|
|
203
|
+
<div id="debug-panel-error" class="error">{ &err.error_message }</div>
|
|
204
|
+
}
|
|
187
205
|
</div>
|
|
188
206
|
<div
|
|
189
207
|
class="scroll-panel-auto-width"
|
|
@@ -219,66 +237,105 @@ impl DebugPanelProps {
|
|
|
219
237
|
text: UseStateSetter<Rc<String>>,
|
|
220
238
|
error: UseStateSetter<Option<ExprValidationError>>,
|
|
221
239
|
modified: UseStateSetter<bool>,
|
|
240
|
+
dirty: Dirty,
|
|
222
241
|
) -> impl Fn(()) + use<> {
|
|
223
242
|
let props = self.clone();
|
|
224
243
|
move |_| {
|
|
244
|
+
if dirty.get() {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
|
|
225
248
|
error.set(None);
|
|
226
249
|
props.set_text(text.clone());
|
|
227
250
|
modified.set(false);
|
|
228
251
|
}
|
|
229
252
|
}
|
|
230
253
|
|
|
254
|
+
/// Validate and apply the editor's text as this panel's config, treating
|
|
255
|
+
/// an un-hosted `table` as a validation error rather than a pending bind.
|
|
231
256
|
fn on_save(
|
|
232
257
|
&self,
|
|
233
|
-
|
|
258
|
+
source: &Rc<String>,
|
|
259
|
+
text: UseStateSetter<Rc<String>>,
|
|
234
260
|
error: &UseStateHandle<Option<ExprValidationError>>,
|
|
235
261
|
modified: &UseStateHandle<bool>,
|
|
262
|
+
dirty: &Dirty,
|
|
236
263
|
) {
|
|
237
264
|
let props = self.clone();
|
|
238
|
-
clone!(
|
|
265
|
+
clone!(source, error, modified, dirty);
|
|
239
266
|
ApiFuture::spawn(async move {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
)
|
|
253
|
-
.await
|
|
254
|
-
{
|
|
255
|
-
Ok(_) => {
|
|
256
|
-
modified.set(false);
|
|
257
|
-
},
|
|
258
|
-
Err(e) => {
|
|
259
|
-
modified.set(true);
|
|
260
|
-
error.set(Some(ExprValidationError {
|
|
261
|
-
error_message: JsValue::from(e).as_string().unwrap_or_else(|| {
|
|
262
|
-
"Failed to validate viewer config".to_owned()
|
|
263
|
-
}),
|
|
264
|
-
line: 0_u32,
|
|
265
|
-
column: 0,
|
|
266
|
-
}));
|
|
267
|
-
},
|
|
268
|
-
}
|
|
269
|
-
Ok(())
|
|
270
|
-
},
|
|
267
|
+
let fail = |message: String, (line, column): (u32, u32)| {
|
|
268
|
+
dirty.set(true);
|
|
269
|
+
modified.set(true);
|
|
270
|
+
error.set(Some(ExprValidationError {
|
|
271
|
+
error_message: message,
|
|
272
|
+
line,
|
|
273
|
+
column,
|
|
274
|
+
}));
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
let config: ViewerConfigUpdate = match serde_json::from_str(&source) {
|
|
278
|
+
Ok(config) => config,
|
|
271
279
|
Err(err) => {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
280
|
+
let position = (err.line() as u32 - 1, err.column() as u32 - 1);
|
|
281
|
+
fail(err.to_string(), position);
|
|
282
|
+
return Ok(());
|
|
283
|
+
},
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
if let OptionalUpdate::Update(name) = &config.table {
|
|
287
|
+
let hosted = fetch_hosted_tables(&props.workspace).await;
|
|
288
|
+
if !hosted
|
|
289
|
+
.iter()
|
|
290
|
+
.any(|(_, names)| names.iter().any(|n| n == name))
|
|
291
|
+
{
|
|
292
|
+
fail(
|
|
293
|
+
format!("Unknown table \"{name}\""),
|
|
294
|
+
locate_key(&source, "table"),
|
|
295
|
+
);
|
|
296
|
+
|
|
297
|
+
return Ok(());
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
let active = props.workspace.active_renderer().as_ref() == Some(&props.renderer);
|
|
302
|
+
let result = crate::tasks::restore_panel(
|
|
303
|
+
&props.session,
|
|
304
|
+
&props.renderer,
|
|
305
|
+
&props.presentation,
|
|
306
|
+
&props.workspace,
|
|
307
|
+
crate::tasks::RestoreMode::Existing { active },
|
|
308
|
+
config,
|
|
309
|
+
crate::tasks::RestoreErrors::Suppress,
|
|
310
|
+
MissingTable::Error,
|
|
311
|
+
)
|
|
312
|
+
.await;
|
|
278
313
|
|
|
279
|
-
|
|
314
|
+
match result {
|
|
315
|
+
Ok(_) => {
|
|
316
|
+
dirty.set(false);
|
|
317
|
+
error.set(None);
|
|
318
|
+
modified.set(false);
|
|
319
|
+
props.set_text(text);
|
|
280
320
|
},
|
|
321
|
+
Err(e) => fail(format!("{e}"), (0, 0)),
|
|
281
322
|
}
|
|
323
|
+
|
|
324
|
+
Ok(())
|
|
282
325
|
});
|
|
283
326
|
}
|
|
284
327
|
}
|
|
328
|
+
|
|
329
|
+
/// The 0-based `(line, column)` of the first `"key"` in `text`, or `(0, 0)`
|
|
330
|
+
/// when absent.
|
|
331
|
+
fn locate_key(text: &str, key: &str) -> (u32, u32) {
|
|
332
|
+
let needle = format!("\"{key}\"");
|
|
333
|
+
text.lines()
|
|
334
|
+
.enumerate()
|
|
335
|
+
.find_map(|(line, content)| {
|
|
336
|
+
content
|
|
337
|
+
.find(&needle)
|
|
338
|
+
.map(|column| (line as u32, column as u32))
|
|
339
|
+
})
|
|
340
|
+
.unwrap_or((0, 0))
|
|
341
|
+
}
|