@perspective-dev/viewer 5.2.0 → 5.3.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 +8 -8
- package/dist/cdn/perspective-viewer.js.map +3 -3
- package/dist/css/blueprint.css +1 -0
- package/dist/css/botanical.css +1 -1
- package/dist/css/dracula.css +1 -1
- package/dist/css/eggplant.css +1 -0
- 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/ledger.css +1 -0
- package/dist/css/monokai.css +1 -1
- package/dist/css/nord.css +1 -0
- 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 -0
- package/dist/docs/perspective-docs.json +1 -1
- package/dist/esm/column-format.d.ts +16 -3
- package/dist/esm/perspective-viewer.d.ts +1 -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 +8 -8
- package/dist/esm/perspective-viewer.js.map +3 -3
- package/dist/esm/plugin.d.ts +10 -0
- package/dist/esm/ts-rs/PanelViewerConfig.d.ts +1 -1
- package/dist/esm/ts-rs/SaveWorkspaceOptions.d.ts +10 -0
- package/dist/esm/ts-rs/ViewerConfig.d.ts +1 -1
- package/dist/esm/ts-rs/WorkspaceConfig.d.ts +9 -1
- package/dist/esm/ts-rs/WorkspaceConfigUpdate.d.ts +13 -3
- package/dist/wasm/perspective-viewer.d.ts +43 -36
- package/dist/wasm/perspective-viewer.js +198 -136
- package/dist/wasm/perspective-viewer.wasm +0 -0
- package/dist/wasm/perspective-viewer.wasm.d.ts +19 -17
- package/package.json +1 -1
- package/src/css/column-settings-panel.css +28 -5
- package/src/css/column-style.css +291 -31
- package/src/css/containers/tabs.css +13 -3
- package/src/css/dom/select.css +21 -9
- package/src/css/form/code-editor.css +4 -4
- package/src/css/plugin-selector.css +1 -0
- package/src/css/plugin-settings-panel.css +72 -0
- package/src/css/viewer.css +0 -5
- package/src/rust/agent/tools.rs +53 -19
- package/src/rust/components/column_selector/active_column.rs +2 -2
- package/src/rust/components/column_selector/add_expression_button.rs +3 -3
- package/src/rust/components/column_selector/aggregate_selector.rs +2 -3
- package/src/rust/components/column_selector/config_selector.rs +8 -0
- package/src/rust/components/column_selector/expr_edit_button.rs +4 -10
- package/src/rust/components/column_selector/inactive_column.rs +2 -2
- package/src/rust/components/column_selector.rs +48 -4
- package/src/rust/components/column_settings_sidebar/style_tab/primitive_field.rs +293 -126
- package/src/rust/components/column_settings_sidebar/style_tab.rs +330 -234
- package/src/rust/components/column_settings_sidebar.rs +88 -69
- package/src/rust/components/containers/control_group.rs +91 -0
- package/src/rust/components/containers/mod.rs +1 -0
- package/src/rust/components/containers/select.rs +8 -2
- package/src/rust/components/datetime_column_style/simple.rs +15 -4
- package/src/rust/components/datetime_column_style.rs +50 -10
- package/src/rust/components/editable_header.rs +85 -144
- package/src/rust/components/expression_editor.rs +9 -16
- package/src/rust/components/form/code_editor.rs +16 -6
- package/src/rust/components/form/debug.rs +1 -0
- package/src/rust/components/form/mod.rs +3 -1
- package/src/rust/components/form/multi_stop_gradient_selector.rs +476 -0
- package/src/rust/components/form/named_value_picker.rs +86 -0
- package/src/rust/components/form/palette_selector.rs +440 -0
- package/src/rust/components/main_panel/msg.rs +2 -2
- package/src/rust/components/main_panel/reconcile.rs +4 -4
- package/src/rust/components/plugin_tab.rs +126 -112
- package/src/rust/components/settings_panel.rs +26 -20
- package/src/rust/components/status_bar.rs +0 -1
- package/src/rust/components/style_controls/number_string_format/digits_section.rs +22 -10
- package/src/rust/components/style_controls/number_string_format/misc_section.rs +6 -2
- package/src/rust/components/style_controls/number_string_format/style_section.rs +1 -0
- package/src/rust/components/style_controls/number_string_format/types.rs +24 -0
- package/src/rust/components/style_controls/number_string_format.rs +37 -14
- package/src/rust/components/viewer/msg.rs +46 -7
- package/src/rust/components/viewer/panels.rs +11 -9
- package/src/rust/components/viewer/render.rs +8 -3
- package/src/rust/components/viewer/settings.rs +163 -23
- package/src/rust/components/viewer/snapshots.rs +46 -21
- package/src/rust/components/viewer/wiring.rs +14 -25
- package/src/rust/components/viewer.rs +12 -8
- package/src/rust/config/column_config_schema.rs +435 -45
- package/src/rust/config/css_scale.rs +882 -0
- package/src/rust/config/datetime_column_style.rs +4 -21
- package/src/rust/config/mod.rs +2 -0
- package/src/rust/config/number_string_format.rs +211 -18
- package/src/rust/config/options.rs +10 -0
- package/src/rust/config/viewer_config.rs +49 -55
- package/src/rust/config/workspace_config.rs +132 -5
- package/src/rust/custom_elements/viewer.rs +214 -155
- package/src/rust/custom_events.rs +14 -1
- package/src/rust/js/regular_layout.rs +3 -3
- package/src/rust/lib.rs +2 -0
- package/src/rust/presentation/column_locator.rs +19 -5
- package/src/rust/presentation.rs +209 -36
- package/src/rust/queries/column_locator.rs +74 -44
- package/src/rust/queries/get_viewer_config.rs +0 -3
- package/src/rust/queries/mod.rs +2 -0
- package/src/rust/queries/palette_set.rs +82 -0
- package/src/rust/queries/plugin_column_styles.rs +6 -2
- package/src/rust/renderer/dispatch.rs +50 -12
- package/src/rust/renderer/plugin_config.rs +362 -53
- package/src/rust/renderer.rs +15 -42
- package/src/rust/session/column_defaults_update.rs +15 -8
- package/src/rust/session/metadata.rs +5 -1
- package/src/rust/session/props.rs +1 -0
- package/src/rust/session.rs +9 -6
- package/src/rust/tasks/create_panel.rs +44 -57
- package/src/rust/tasks/edit_expression.rs +19 -23
- package/src/rust/tasks/edit_window.rs +8 -10
- package/src/rust/tasks/eject.rs +4 -1
- package/src/rust/tasks/pipeline.rs +2 -2
- package/src/rust/tasks/presize_panels.rs +44 -5
- package/src/rust/tasks/reset_all.rs +4 -4
- package/src/rust/tasks/restore_and_render.rs +17 -44
- package/src/rust/tasks/restore_panel.rs +11 -13
- package/src/rust/tasks/set_edit_mode.rs +3 -1
- package/src/rust/tasks/sync_update_panels.rs +11 -1
- package/src/rust/tasks/update_theme.rs +23 -18
- package/src/rust/utils/css_vars.rs +26 -0
- package/src/rust/utils/mod.rs +2 -0
- package/src/rust/utils/spawn.rs +1 -1
- package/src/rust/workspace.rs +435 -272
- package/src/svg/lock.svg +7 -0
- package/src/themes/blueprint.css +190 -0
- package/src/themes/botanical.css +14 -17
- package/src/themes/defaults.css +4 -110
- package/src/themes/dracula.css +19 -14
- package/src/themes/eggplant.css +189 -0
- package/src/themes/gruvbox-dark.css +23 -15
- package/src/themes/gruvbox.css +11 -1
- package/src/themes/icons.css +2 -2
- package/src/themes/intl/de.css +23 -1
- package/src/themes/intl/es.css +23 -1
- package/src/themes/intl/fr.css +23 -1
- package/src/themes/intl/ja.css +23 -1
- package/src/themes/intl/pt.css +23 -1
- package/src/themes/intl/zh.css +23 -1
- package/src/themes/intl.css +26 -4
- package/src/themes/ledger.css +195 -0
- package/src/themes/monokai.css +24 -14
- package/src/themes/nord.css +186 -0
- package/src/themes/phosphor.css +7 -14
- package/src/themes/pro-dark.css +12 -16
- package/src/themes/pro.css +118 -0
- package/src/themes/solarized-dark.css +9 -16
- package/src/themes/solarized.css +7 -0
- package/src/themes/themes.css +10 -5
- package/src/themes/vaporwave.css +22 -14
- package/src/themes/velvet.css +192 -0
- package/src/ts/column-format.ts +52 -2
- package/src/ts/perspective-viewer.ts +1 -0
- package/src/ts/plugin.ts +14 -0
- package/src/ts/ts-rs/PanelViewerConfig.ts +1 -1
- package/src/ts/ts-rs/SaveWorkspaceOptions.ts +11 -0
- package/src/ts/ts-rs/ViewerConfig.ts +1 -1
- package/src/ts/ts-rs/WorkspaceConfig.ts +8 -2
- package/src/ts/ts-rs/WorkspaceConfigUpdate.ts +11 -3
- package/src/rust/components/form/color_range_selector.rs +0 -137
- package/src/svg/dropdown-selector-light.svg +0 -3
- /package/dist/wasm/snippets/{perspective-viewer-02e461f3d7062b26 → perspective-viewer-87bd21b215d0631a}/inline0.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-02e461f3d7062b26 → perspective-viewer-87bd21b215d0631a}/inline1.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-02e461f3d7062b26 → perspective-viewer-87bd21b215d0631a}/inline2.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-02e461f3d7062b26 → perspective-viewer-87bd21b215d0631a}/inline3.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-02e461f3d7062b26 → perspective-viewer-87bd21b215d0631a}/inline4.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-02e461f3d7062b26 → perspective-viewer-87bd21b215d0631a}/inline5.js +0 -0
|
@@ -41,7 +41,9 @@ pub fn get_plugin_config_schema(
|
|
|
41
41
|
let view_config_js =
|
|
42
42
|
wasm_bindgen::JsValue::from_serde_ext(view_config).unwrap_or(wasm_bindgen::JsValue::NULL);
|
|
43
43
|
let raw = plugin._plugin_config_schema(&view_config_js)?;
|
|
44
|
-
serde_wasm_bindgen::from_value(raw)
|
|
44
|
+
serde_wasm_bindgen::from_value::<ColumnConfigSchema>(raw)
|
|
45
|
+
.map(|schema| schema.canonicalize())
|
|
46
|
+
.map_err(|e| e.into())
|
|
45
47
|
}
|
|
46
48
|
|
|
47
49
|
/// Queries the active plugin for the per-column [`ColumnConfigSchema`].
|
|
@@ -97,5 +99,7 @@ pub fn get_column_config_schema(
|
|
|
97
99
|
&stats_js,
|
|
98
100
|
)?;
|
|
99
101
|
|
|
100
|
-
serde_wasm_bindgen::from_value(raw)
|
|
102
|
+
serde_wasm_bindgen::from_value::<ColumnConfigSchema>(raw)
|
|
103
|
+
.map(|schema| schema.canonicalize())
|
|
104
|
+
.map_err(|e| e.into())
|
|
101
105
|
}
|
|
@@ -49,7 +49,7 @@ impl Renderer {
|
|
|
49
49
|
let active_plugin = self.active_plugin();
|
|
50
50
|
if let Some(plugin) = plugin.or(active_plugin.as_ref()) {
|
|
51
51
|
let theme_elem = plugin.unchecked_ref::<HtmlElement>();
|
|
52
|
-
match self.
|
|
52
|
+
match self.theme() {
|
|
53
53
|
Some(theme)
|
|
54
54
|
if theme_elem.get_attribute("theme").as_deref() != Some(theme.as_str()) =>
|
|
55
55
|
{
|
|
@@ -73,7 +73,7 @@ impl Renderer {
|
|
|
73
73
|
let _pin = self.pin_context(&guard, ctx.clone());
|
|
74
74
|
let plugin = self.ensure_plugin_selected()?;
|
|
75
75
|
let meta = self.metadata();
|
|
76
|
-
let stamped_theme = self.
|
|
76
|
+
let stamped_theme = self.theme();
|
|
77
77
|
self.stamp_theme(Some(&plugin));
|
|
78
78
|
self.stamp_active(&plugin);
|
|
79
79
|
plugin.restyle();
|
|
@@ -134,7 +134,19 @@ impl Renderer {
|
|
|
134
134
|
|
|
135
135
|
pub async fn resize(&self) -> ApiResult<()> {
|
|
136
136
|
self.0.geometry_cmd.set(Some(GeometryCmd::Measure));
|
|
137
|
-
|
|
137
|
+
|
|
138
|
+
// This caller queued a `Measure`, but the parked run it resolves
|
|
139
|
+
// through executes the LATEST `geometry_cmd` — which a concurrent
|
|
140
|
+
// presize sweep may have swapped to a `Presize` whose sweep
|
|
141
|
+
// coalesced (and so received `None`). Any present closure returned
|
|
142
|
+
// here is therefore held by no one else: invoke it, or the
|
|
143
|
+
// transport's present-hold leaks and the plot freezes on its
|
|
144
|
+
// last-blitted frame.
|
|
145
|
+
if let Some(present) = self.geometry_task().await? {
|
|
146
|
+
present.call0(&JsValue::NULL)?;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
Ok(())
|
|
138
150
|
}
|
|
139
151
|
|
|
140
152
|
/// Public one-shot resize to explicit dimensions (`viewer.resize({
|
|
@@ -153,10 +165,8 @@ impl Renderer {
|
|
|
153
165
|
width: f64,
|
|
154
166
|
height: f64,
|
|
155
167
|
) -> ApiResult<Option<js_sys::Function>> {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
self.0.presize_pending.set(self.0.presize_pending.get() - 1);
|
|
159
|
-
result
|
|
168
|
+
let _pending = PresizePendingGuard::increment(self);
|
|
169
|
+
self.presize_inner(None, width, height).await
|
|
160
170
|
}
|
|
161
171
|
|
|
162
172
|
/// Pre-size AND pre-position the plugin to its target layout bo.
|
|
@@ -167,10 +177,8 @@ impl Renderer {
|
|
|
167
177
|
width: f64,
|
|
168
178
|
height: f64,
|
|
169
179
|
) -> ApiResult<Option<js_sys::Function>> {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
self.0.presize_pending.set(self.0.presize_pending.get() - 1);
|
|
173
|
-
result
|
|
180
|
+
let _pending = PresizePendingGuard::increment(self);
|
|
181
|
+
self.presize_inner(Some((dx, dy)), width, height).await
|
|
174
182
|
}
|
|
175
183
|
|
|
176
184
|
async fn presize_inner(
|
|
@@ -362,7 +370,14 @@ impl Renderer {
|
|
|
362
370
|
.debounce_with(|guard| async move {
|
|
363
371
|
set_timeout(timer.get_throttle()).await?;
|
|
364
372
|
if self.0.presize_pending.get() > 0 {
|
|
373
|
+
// Record the drop like the hidden-panel path
|
|
374
|
+
// (`wire_panel_render_sub`) does — a silently
|
|
375
|
+
// discarded update otherwise leaves the plugin's
|
|
376
|
+
// retained frame stale until the NEXT data tick,
|
|
377
|
+
// or forever if none comes. `draw_view` /
|
|
378
|
+
// `activation_render` consume the marker.
|
|
365
379
|
tracing::debug!("Update skipped, presize pending");
|
|
380
|
+
self.set_data_stale();
|
|
366
381
|
return Ok(());
|
|
367
382
|
}
|
|
368
383
|
|
|
@@ -416,7 +431,7 @@ impl Renderer {
|
|
|
416
431
|
let viewer_elem = self.0.borrow().viewer_elem.clone();
|
|
417
432
|
let slot = self.slot_name();
|
|
418
433
|
let first_paint = !self.0.has_drawn.get();
|
|
419
|
-
let stamped_theme = self.
|
|
434
|
+
let stamped_theme = self.theme();
|
|
420
435
|
self.stamp_active(&plugin);
|
|
421
436
|
self.stamp_theme(Some(&plugin));
|
|
422
437
|
if self.needs_restyle() {
|
|
@@ -461,3 +476,26 @@ impl Renderer {
|
|
|
461
476
|
self.draw_lock.clone()
|
|
462
477
|
}
|
|
463
478
|
}
|
|
479
|
+
|
|
480
|
+
/// RAII increment of [`RendererData::presize_pending`], the counter that
|
|
481
|
+
/// gates [`Renderer::update_lazy`] dispatches. Decrementing on `Drop`
|
|
482
|
+
/// (not on the happy path) makes the release unconditional — a presize
|
|
483
|
+
/// future cancelled mid-await can never strand the counter above zero,
|
|
484
|
+
/// which would silently discard every subsequent debounced update for
|
|
485
|
+
/// the panel's lifetime.
|
|
486
|
+
struct PresizePendingGuard(Renderer);
|
|
487
|
+
|
|
488
|
+
impl PresizePendingGuard {
|
|
489
|
+
fn increment(renderer: &Renderer) -> Self {
|
|
490
|
+
let cell = &renderer.0.presize_pending;
|
|
491
|
+
cell.set(cell.get() + 1);
|
|
492
|
+
Self(renderer.clone())
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
impl Drop for PresizePendingGuard {
|
|
497
|
+
fn drop(&mut self) {
|
|
498
|
+
let cell = &self.0.0.presize_pending;
|
|
499
|
+
cell.set(cell.get() - 1);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
@@ -18,7 +18,7 @@ use std::collections::HashMap;
|
|
|
18
18
|
|
|
19
19
|
use futures::future::join_all;
|
|
20
20
|
use perspective_client::config::ViewConfig;
|
|
21
|
-
use perspective_js::utils::{ApiResult, JsValueSerdeExt};
|
|
21
|
+
use perspective_js::utils::{ApiError, ApiResult, JsValueSerdeExt};
|
|
22
22
|
use serde_json::Value;
|
|
23
23
|
use wasm_bindgen::prelude::*;
|
|
24
24
|
|
|
@@ -63,6 +63,50 @@ impl Renderer {
|
|
|
63
63
|
.unwrap_or_default()
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
fn resolve_css_refs_in(&self, column: &str, entry: &mut serde_json::Map<String, Value>) {
|
|
67
|
+
let host = self.borrow().viewer_elem.clone();
|
|
68
|
+
let lookup = |name: &str| crate::utils::read_custom_property(&host, name);
|
|
69
|
+
for (key, name) in resolve_css_refs(entry, &lookup) {
|
|
70
|
+
tracing::warn!(
|
|
71
|
+
"Dropping `columns_config[\"{column}\"].{key}`: `var({name})` is undefined or not \
|
|
72
|
+
a valid value of its kind"
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/// Every CSS literal stored in the active plugin's per-column bucket
|
|
78
|
+
/// with its schema kind, sorted by `(column, key)`.
|
|
79
|
+
pub fn css_literals_in_use(
|
|
80
|
+
&self,
|
|
81
|
+
view_config: &ViewConfig,
|
|
82
|
+
session: &Session,
|
|
83
|
+
) -> Vec<CssLiteralUse> {
|
|
84
|
+
let mut out = vec![];
|
|
85
|
+
for (column, entry) in self.all_columns_configs() {
|
|
86
|
+
let Ok(schema) =
|
|
87
|
+
self.query_column_config_schema(view_config, session, &column, Some(&entry))
|
|
88
|
+
else {
|
|
89
|
+
continue;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
for (key, value) in &entry {
|
|
93
|
+
if let (Some(kind), Some(literal)) = (schema.css_kind_of(key), value.as_str())
|
|
94
|
+
&& parse_var_ref(literal).is_none()
|
|
95
|
+
{
|
|
96
|
+
out.push(CssLiteralUse {
|
|
97
|
+
column: column.clone(),
|
|
98
|
+
key: key.clone(),
|
|
99
|
+
kind,
|
|
100
|
+
literal: literal.to_owned(),
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
out.sort_by(|a, b| (&a.column, &a.key).cmp(&(&b.column, &b.key)));
|
|
107
|
+
out
|
|
108
|
+
}
|
|
109
|
+
|
|
66
110
|
/// Restore-prep snapshot: like [`Self::all_columns_configs`], but
|
|
67
111
|
/// for each column also materializes any `ControlSpec::Number`
|
|
68
112
|
/// fields the schema declares with `include: true` that aren't
|
|
@@ -89,7 +133,7 @@ impl Renderer {
|
|
|
89
133
|
else {
|
|
90
134
|
continue;
|
|
91
135
|
};
|
|
92
|
-
let needs_warm = schema.
|
|
136
|
+
let needs_warm = schema.leaf_fields().into_iter().any(|f| {
|
|
93
137
|
matches!(
|
|
94
138
|
f,
|
|
95
139
|
ControlSpec::Number {
|
|
@@ -120,7 +164,7 @@ impl Renderer {
|
|
|
120
164
|
continue;
|
|
121
165
|
};
|
|
122
166
|
|
|
123
|
-
for field in
|
|
167
|
+
for field in schema.leaf_fields() {
|
|
124
168
|
let ControlSpec::Number {
|
|
125
169
|
key,
|
|
126
170
|
default,
|
|
@@ -173,15 +217,15 @@ impl Renderer {
|
|
|
173
217
|
.cloned()
|
|
174
218
|
}
|
|
175
219
|
|
|
176
|
-
/// Wholesale update the active plugin's per-column config map
|
|
220
|
+
/// Wholesale update the active plugin's per-column config map.
|
|
177
221
|
pub fn update_columns_configs(
|
|
178
222
|
&self,
|
|
179
223
|
view_config: &ViewConfig,
|
|
180
224
|
session: &Session,
|
|
181
225
|
update: ColumnConfigUpdate,
|
|
182
|
-
) -> bool {
|
|
226
|
+
) -> ApiResult<bool> {
|
|
183
227
|
let Some(n) = self.active_plugin_name() else {
|
|
184
|
-
return false;
|
|
228
|
+
return Ok(false);
|
|
185
229
|
};
|
|
186
230
|
|
|
187
231
|
match update {
|
|
@@ -190,24 +234,31 @@ impl Renderer {
|
|
|
190
234
|
let bucket = st.plugin_states.entry(n).or_default();
|
|
191
235
|
let was_nonempty = !bucket.columns.is_empty();
|
|
192
236
|
bucket.columns.clear();
|
|
193
|
-
was_nonempty
|
|
237
|
+
Ok(was_nonempty)
|
|
194
238
|
},
|
|
195
|
-
OptionalUpdate::Missing => false,
|
|
239
|
+
OptionalUpdate::Missing => Ok(false),
|
|
196
240
|
OptionalUpdate::Update(map) => {
|
|
197
|
-
let stripped: Vec<(String, serde_json::Map<String, serde_json::Value>)> =
|
|
198
|
-
.
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
241
|
+
let mut stripped: Vec<(String, serde_json::Map<String, serde_json::Value>)> =
|
|
242
|
+
Vec::with_capacity(map.len());
|
|
243
|
+
for (col, mut cfg) in map {
|
|
244
|
+
if let Ok(schema) =
|
|
245
|
+
self.query_column_config_schema(view_config, session, &col, Some(&cfg))
|
|
246
|
+
{
|
|
247
|
+
let active = schema.active_keys();
|
|
248
|
+
cfg.retain(|k, _| active.contains(k));
|
|
249
|
+
let errors = normalize_css_values(&schema, &mut cfg);
|
|
250
|
+
if let Some((key, error)) = errors.first() {
|
|
251
|
+
return Err(ApiError::from(JsValue::from_str(&format!(
|
|
252
|
+
"Invalid `columns_config[\"{col}\"].{key}`: {error}"
|
|
253
|
+
))));
|
|
206
254
|
}
|
|
207
255
|
|
|
208
|
-
(col, cfg)
|
|
209
|
-
|
|
210
|
-
|
|
256
|
+
self.resolve_css_refs_in(&col, &mut cfg);
|
|
257
|
+
strip_default_values(&schema, &mut cfg);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
stripped.push((col, cfg));
|
|
261
|
+
}
|
|
211
262
|
|
|
212
263
|
let mut st = self.borrow_mut();
|
|
213
264
|
let bucket = st.plugin_states.entry(n).or_default();
|
|
@@ -226,7 +277,7 @@ impl Renderer {
|
|
|
226
277
|
}
|
|
227
278
|
}
|
|
228
279
|
|
|
229
|
-
changed
|
|
280
|
+
Ok(changed)
|
|
230
281
|
},
|
|
231
282
|
}
|
|
232
283
|
}
|
|
@@ -253,22 +304,45 @@ impl Renderer {
|
|
|
253
304
|
&column_name,
|
|
254
305
|
current_value.as_ref(),
|
|
255
306
|
) {
|
|
307
|
+
for (key, error) in normalize_css_values(&schema, &mut update.value) {
|
|
308
|
+
tracing::error!("Dropping `{column_name}`.`{key}`: {error}");
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
self.resolve_css_refs_in(&column_name, &mut update.value);
|
|
256
312
|
strip_default_values(&schema, &mut update.value);
|
|
257
313
|
}
|
|
258
314
|
|
|
315
|
+
let next = {
|
|
316
|
+
let mut st = self.borrow_mut();
|
|
317
|
+
let bucket = st.plugin_states.entry(n.clone()).or_default();
|
|
318
|
+
let entry = bucket.columns.entry(column_name.clone()).or_default();
|
|
319
|
+
for k in &update.keys {
|
|
320
|
+
entry.remove(k);
|
|
321
|
+
}
|
|
322
|
+
for (k, v) in update.value {
|
|
323
|
+
if update.keys.contains(&k) {
|
|
324
|
+
entry.insert(k, v);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
entry.clone()
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
let active = self
|
|
332
|
+
.query_column_config_schema(view_config, session, &column_name, Some(&next))
|
|
333
|
+
.ok()
|
|
334
|
+
.map(|schema| schema.active_keys());
|
|
335
|
+
|
|
259
336
|
let mut st = self.borrow_mut();
|
|
260
337
|
let bucket = st.plugin_states.entry(n).or_default();
|
|
261
|
-
let entry = bucket.columns.
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
if
|
|
267
|
-
|
|
338
|
+
if let Some(entry) = bucket.columns.get_mut(&column_name) {
|
|
339
|
+
if let Some(active) = &active {
|
|
340
|
+
entry.retain(|k, _| active.contains(k));
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if entry.is_empty() {
|
|
344
|
+
bucket.columns.remove(&column_name);
|
|
268
345
|
}
|
|
269
|
-
}
|
|
270
|
-
if entry.is_empty() {
|
|
271
|
-
bucket.columns.remove(&column_name);
|
|
272
346
|
}
|
|
273
347
|
}
|
|
274
348
|
|
|
@@ -305,7 +379,9 @@ impl Renderer {
|
|
|
305
379
|
let plugin = self.ensure_plugin_selected()?;
|
|
306
380
|
let view_config_js = JsValue::from_serde_ext(view_config).unwrap_or(JsValue::NULL);
|
|
307
381
|
let raw = plugin._plugin_config_schema(&view_config_js)?;
|
|
308
|
-
serde_wasm_bindgen::from_value(raw)
|
|
382
|
+
serde_wasm_bindgen::from_value::<ColumnConfigSchema>(raw)
|
|
383
|
+
.map(|schema| schema.canonicalize())
|
|
384
|
+
.map_err(|e| e.into())
|
|
309
385
|
}
|
|
310
386
|
|
|
311
387
|
/// Per-column counterpart of [`query_plugin_config_schema`]. Used by
|
|
@@ -352,7 +428,9 @@ impl Renderer {
|
|
|
352
428
|
&stats_js,
|
|
353
429
|
)?;
|
|
354
430
|
|
|
355
|
-
serde_wasm_bindgen::from_value(raw)
|
|
431
|
+
serde_wasm_bindgen::from_value::<ColumnConfigSchema>(raw)
|
|
432
|
+
.map(|schema| schema.canonicalize())
|
|
433
|
+
.map_err(|e| e.into())
|
|
356
434
|
}
|
|
357
435
|
|
|
358
436
|
/// Wholesale update the active plugin's plugin-level config map.
|
|
@@ -365,9 +443,9 @@ impl Renderer {
|
|
|
365
443
|
&self,
|
|
366
444
|
view_config: &ViewConfig,
|
|
367
445
|
update: PluginConfigUpdate,
|
|
368
|
-
) -> bool {
|
|
446
|
+
) -> ApiResult<bool> {
|
|
369
447
|
let Some(n) = self.active_plugin_name() else {
|
|
370
|
-
return false;
|
|
448
|
+
return Ok(false);
|
|
371
449
|
};
|
|
372
450
|
|
|
373
451
|
let schema = self.query_plugin_config_schema(view_config).ok();
|
|
@@ -377,17 +455,24 @@ impl Renderer {
|
|
|
377
455
|
OptionalUpdate::SetDefault => {
|
|
378
456
|
let changed = !bucket.plugin.is_empty();
|
|
379
457
|
bucket.plugin.clear();
|
|
380
|
-
changed
|
|
458
|
+
Ok(changed)
|
|
381
459
|
},
|
|
382
|
-
OptionalUpdate::Missing => false,
|
|
460
|
+
OptionalUpdate::Missing => Ok(false),
|
|
383
461
|
OptionalUpdate::Update(mut map) => {
|
|
384
462
|
let mut changed = false;
|
|
385
463
|
if let Some(s) = &schema {
|
|
386
464
|
let active = s.active_keys();
|
|
387
465
|
map.retain(|k, _| active.contains(k));
|
|
466
|
+
let errors = normalize_css_values(s, &mut map);
|
|
467
|
+
if let Some((key, error)) = errors.first() {
|
|
468
|
+
return Err(ApiError::from(JsValue::from_str(&format!(
|
|
469
|
+
"Invalid `plugin_config.{key}`: {error}"
|
|
470
|
+
))));
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
let leaves = s.leaf_fields();
|
|
388
474
|
map.retain(|key, value| {
|
|
389
|
-
let is_default =
|
|
390
|
-
.fields
|
|
475
|
+
let is_default = leaves
|
|
391
476
|
.iter()
|
|
392
477
|
.any(|spec| matches_declared_default(spec, key, value));
|
|
393
478
|
if is_default {
|
|
@@ -408,7 +493,7 @@ impl Renderer {
|
|
|
408
493
|
}
|
|
409
494
|
}
|
|
410
495
|
|
|
411
|
-
changed
|
|
496
|
+
Ok(changed)
|
|
412
497
|
},
|
|
413
498
|
}
|
|
414
499
|
}
|
|
@@ -424,6 +509,10 @@ impl Renderer {
|
|
|
424
509
|
};
|
|
425
510
|
|
|
426
511
|
if let Ok(schema) = self.query_plugin_config_schema(view_config) {
|
|
512
|
+
for (key, error) in normalize_css_values(&schema, &mut update.value) {
|
|
513
|
+
tracing::error!("Dropping `plugin_config`.`{key}`: {error}");
|
|
514
|
+
}
|
|
515
|
+
|
|
427
516
|
strip_default_values(&schema, &mut update.value);
|
|
428
517
|
}
|
|
429
518
|
|
|
@@ -446,13 +535,41 @@ impl Renderer {
|
|
|
446
535
|
}
|
|
447
536
|
}
|
|
448
537
|
|
|
538
|
+
fn normalize_css_values(
|
|
539
|
+
schema: &ColumnConfigSchema,
|
|
540
|
+
map: &mut serde_json::Map<String, serde_json::Value>,
|
|
541
|
+
) -> Vec<(String, String)> {
|
|
542
|
+
let mut errors = vec![];
|
|
543
|
+
for (key, value) in map.iter_mut() {
|
|
544
|
+
let Some(kind) = schema.css_kind_of(key) else {
|
|
545
|
+
continue;
|
|
546
|
+
};
|
|
547
|
+
|
|
548
|
+
let canonical = match value.as_str() {
|
|
549
|
+
Some(src) => kind.canonicalize(src),
|
|
550
|
+
None => Err(format!("expected a CSS string, got `{value}`")),
|
|
551
|
+
};
|
|
552
|
+
|
|
553
|
+
match canonical {
|
|
554
|
+
Ok(canonical) => *value = Value::String(canonical),
|
|
555
|
+
Err(error) => errors.push((key.clone(), error)),
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
for (key, _) in &errors {
|
|
560
|
+
map.remove(key);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
errors
|
|
564
|
+
}
|
|
565
|
+
|
|
449
566
|
fn strip_default_values(
|
|
450
567
|
schema: &ColumnConfigSchema,
|
|
451
568
|
map: &mut serde_json::Map<String, serde_json::Value>,
|
|
452
569
|
) {
|
|
570
|
+
let leaves = schema.leaf_fields();
|
|
453
571
|
map.retain(|key, value| {
|
|
454
|
-
!
|
|
455
|
-
.fields
|
|
572
|
+
!leaves
|
|
456
573
|
.iter()
|
|
457
574
|
.any(|spec| matches_declared_default(spec, key, value))
|
|
458
575
|
});
|
|
@@ -479,17 +596,209 @@ fn matches_declared_default(spec: &ControlSpec, key: &str, value: &Value) -> boo
|
|
|
479
596
|
} if k == key => value.as_str() == Some(default.as_str()),
|
|
480
597
|
ControlSpec::Color {
|
|
481
598
|
key: k, default, ..
|
|
482
|
-
} if k == key =>
|
|
483
|
-
ControlSpec::
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
key_neg,
|
|
490
|
-
default_neg,
|
|
491
|
-
..
|
|
492
|
-
} if key_neg == key => value.as_str() == Some(default_neg.as_str()),
|
|
599
|
+
} if k == key => css_matches(CssKind::Color, value, default),
|
|
600
|
+
ControlSpec::Palette {
|
|
601
|
+
key: k, default, ..
|
|
602
|
+
} if k == key => css_matches(CssKind::Palette, value, default),
|
|
603
|
+
ControlSpec::GradientStops {
|
|
604
|
+
key: k, default, ..
|
|
605
|
+
} if k == key => css_matches(CssKind::Gradient, value, default),
|
|
493
606
|
_ => false,
|
|
494
607
|
}
|
|
495
608
|
}
|
|
609
|
+
|
|
610
|
+
fn css_matches(kind: CssKind, value: &Value, default: &str) -> bool {
|
|
611
|
+
value
|
|
612
|
+
.as_str()
|
|
613
|
+
.and_then(|src| kind.canonicalize(src).ok())
|
|
614
|
+
.is_some_and(|canonical| canonical == default)
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
#[cfg(test)]
|
|
618
|
+
mod tests {
|
|
619
|
+
use serde_json::json;
|
|
620
|
+
|
|
621
|
+
use super::*;
|
|
622
|
+
|
|
623
|
+
#[test]
|
|
624
|
+
fn palette_default_matches_canonically() {
|
|
625
|
+
let spec = ControlSpec::Palette {
|
|
626
|
+
key: "palette".to_owned(),
|
|
627
|
+
default: "linear-gradient(to right, #0366d6, #ff7f0e)".to_owned(),
|
|
628
|
+
max: None,
|
|
629
|
+
};
|
|
630
|
+
|
|
631
|
+
assert!(matches_declared_default(
|
|
632
|
+
&spec,
|
|
633
|
+
"palette",
|
|
634
|
+
&json!("linear-gradient(to right, #0366d6, #ff7f0e)")
|
|
635
|
+
));
|
|
636
|
+
|
|
637
|
+
assert!(matches_declared_default(
|
|
638
|
+
&spec,
|
|
639
|
+
"palette",
|
|
640
|
+
&json!("linear-gradient(90deg, RGB(3,102,214), #FF7F0E)")
|
|
641
|
+
));
|
|
642
|
+
|
|
643
|
+
assert!(!matches_declared_default(
|
|
644
|
+
&spec,
|
|
645
|
+
"palette",
|
|
646
|
+
&json!("linear-gradient(to right, #ff7f0e, #0366d6)")
|
|
647
|
+
));
|
|
648
|
+
|
|
649
|
+
assert!(!matches_declared_default(
|
|
650
|
+
&spec,
|
|
651
|
+
"palette",
|
|
652
|
+
&json!("var(--psp-user--palette-1)")
|
|
653
|
+
));
|
|
654
|
+
|
|
655
|
+
assert!(!matches_declared_default(
|
|
656
|
+
&spec,
|
|
657
|
+
"palette",
|
|
658
|
+
&json!(["#0366d6"])
|
|
659
|
+
));
|
|
660
|
+
assert!(!matches_declared_default(
|
|
661
|
+
&spec,
|
|
662
|
+
"other",
|
|
663
|
+
&json!("linear-gradient(to right, #0366d6, #ff7f0e)")
|
|
664
|
+
));
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
#[test]
|
|
668
|
+
fn gradient_default_matches_canonical_values() {
|
|
669
|
+
let spec = ControlSpec::GradientStops {
|
|
670
|
+
key: "gradient".to_owned(),
|
|
671
|
+
default: "linear-gradient(to right, #0366d6 0%, #ff7f0e 33.3%)".to_owned(),
|
|
672
|
+
discrete: false,
|
|
673
|
+
};
|
|
674
|
+
|
|
675
|
+
assert!(matches_declared_default(
|
|
676
|
+
&spec,
|
|
677
|
+
"gradient",
|
|
678
|
+
&json!("linear-gradient(#0366d6, #ff7f0e 33.3%)")
|
|
679
|
+
));
|
|
680
|
+
|
|
681
|
+
assert!(!matches_declared_default(
|
|
682
|
+
&spec,
|
|
683
|
+
"gradient",
|
|
684
|
+
&json!("linear-gradient(to right, #0366d6 0%, #ff7f0e 33.4%)")
|
|
685
|
+
));
|
|
686
|
+
|
|
687
|
+
let color = ControlSpec::Color {
|
|
688
|
+
key: "color".to_owned(),
|
|
689
|
+
default: "#ff7f0e".to_owned(),
|
|
690
|
+
};
|
|
691
|
+
|
|
692
|
+
assert!(matches_declared_default(&color, "color", &json!("#FF7F0E")));
|
|
693
|
+
assert!(matches_declared_default(
|
|
694
|
+
&color,
|
|
695
|
+
"color",
|
|
696
|
+
&json!("rgb(255, 127, 14)")
|
|
697
|
+
));
|
|
698
|
+
assert!(!matches_declared_default(
|
|
699
|
+
&color,
|
|
700
|
+
"color",
|
|
701
|
+
&json!("var(--psp-user--color-1)")
|
|
702
|
+
));
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
#[test]
|
|
706
|
+
fn strip_default_values_sees_through_groups() {
|
|
707
|
+
let leaves = vec![
|
|
708
|
+
ControlSpec::Bool {
|
|
709
|
+
key: "flag".to_owned(),
|
|
710
|
+
default: false,
|
|
711
|
+
},
|
|
712
|
+
ControlSpec::Number {
|
|
713
|
+
key: "size".to_owned(),
|
|
714
|
+
default: 3.0,
|
|
715
|
+
include: None,
|
|
716
|
+
min: None,
|
|
717
|
+
max: None,
|
|
718
|
+
step: None,
|
|
719
|
+
},
|
|
720
|
+
];
|
|
721
|
+
|
|
722
|
+
let flat = ColumnConfigSchema {
|
|
723
|
+
fields: leaves.clone(),
|
|
724
|
+
};
|
|
725
|
+
|
|
726
|
+
let grouped = ColumnConfigSchema {
|
|
727
|
+
fields: vec![ControlSpec::Group {
|
|
728
|
+
key: "section".to_owned(),
|
|
729
|
+
fields: leaves,
|
|
730
|
+
}],
|
|
731
|
+
};
|
|
732
|
+
|
|
733
|
+
let src = json!({ "flag": false, "size": 4.0, "foreign": 1 })
|
|
734
|
+
.as_object()
|
|
735
|
+
.unwrap()
|
|
736
|
+
.clone();
|
|
737
|
+
|
|
738
|
+
let mut a = src.clone();
|
|
739
|
+
let mut b = src;
|
|
740
|
+
strip_default_values(&flat, &mut a);
|
|
741
|
+
strip_default_values(&grouped, &mut b);
|
|
742
|
+
assert_eq!(a, b);
|
|
743
|
+
assert_eq!(
|
|
744
|
+
a,
|
|
745
|
+
json!({ "size": 4.0, "foreign": 1 })
|
|
746
|
+
.as_object()
|
|
747
|
+
.unwrap()
|
|
748
|
+
.clone()
|
|
749
|
+
);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
#[test]
|
|
753
|
+
fn normalize_css_values_canonicalizes_and_reports() {
|
|
754
|
+
let schema = ColumnConfigSchema {
|
|
755
|
+
fields: vec![
|
|
756
|
+
ControlSpec::Color {
|
|
757
|
+
key: "color".to_owned(),
|
|
758
|
+
default: "#000000".to_owned(),
|
|
759
|
+
},
|
|
760
|
+
ControlSpec::Palette {
|
|
761
|
+
key: "palette".to_owned(),
|
|
762
|
+
default: "linear-gradient(to right, #000000)".to_owned(),
|
|
763
|
+
max: None,
|
|
764
|
+
},
|
|
765
|
+
ControlSpec::GradientStops {
|
|
766
|
+
key: "gradient".to_owned(),
|
|
767
|
+
default: "linear-gradient(to right, #000000 0%, #ffffff 100%)".to_owned(),
|
|
768
|
+
discrete: false,
|
|
769
|
+
},
|
|
770
|
+
ControlSpec::Bool {
|
|
771
|
+
key: "flag".to_owned(),
|
|
772
|
+
default: false,
|
|
773
|
+
},
|
|
774
|
+
],
|
|
775
|
+
};
|
|
776
|
+
|
|
777
|
+
let mut map = json!({
|
|
778
|
+
"color": "RGB(255,0,0)",
|
|
779
|
+
"palette": "var(--psp-user--palette-warm)",
|
|
780
|
+
"gradient": [{ "color": "#000000", "offset": 0 }],
|
|
781
|
+
"flag": true,
|
|
782
|
+
"foreign": "linear-gradient(red, blue)",
|
|
783
|
+
})
|
|
784
|
+
.as_object()
|
|
785
|
+
.unwrap()
|
|
786
|
+
.clone();
|
|
787
|
+
|
|
788
|
+
let errors = normalize_css_values(&schema, &mut map);
|
|
789
|
+
assert_eq!(errors.len(), 1);
|
|
790
|
+
assert_eq!(errors[0].0, "gradient");
|
|
791
|
+
assert_eq!(
|
|
792
|
+
map,
|
|
793
|
+
json!({
|
|
794
|
+
"color": "#ff0000",
|
|
795
|
+
"palette": "var(--psp-user--palette-warm)",
|
|
796
|
+
"flag": true,
|
|
797
|
+
"foreign": "linear-gradient(red, blue)",
|
|
798
|
+
})
|
|
799
|
+
.as_object()
|
|
800
|
+
.unwrap()
|
|
801
|
+
.clone()
|
|
802
|
+
);
|
|
803
|
+
}
|
|
804
|
+
}
|