@perspective-dev/viewer 5.5.1 → 5.6.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/README.md +82 -3
- package/dist/cdn/perspective-viewer.js +9 -9
- package/dist/cdn/perspective-viewer.js.map +3 -3
- 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/column-format.js +2 -0
- package/dist/esm/column-format.js.map +7 -0
- package/dist/esm/extensions.d.ts +3 -13
- package/dist/esm/perspective-viewer.d.ts +1 -1
- package/dist/esm/perspective-viewer.js +9 -9
- package/dist/esm/perspective-viewer.js.map +3 -3
- package/dist/esm/plugin.d.ts +4 -1
- package/dist/esm/select-detail.d.ts +15 -0
- package/dist/esm/select-detail.js +2 -0
- package/dist/esm/select-detail.js.map +7 -0
- package/dist/esm/ts-rs/DerivedTableConfig.d.ts +35 -0
- package/dist/esm/ts-rs/PanelViewerConfig.d.ts +7 -6
- package/dist/esm/ts-rs/RestoreOptions.d.ts +3 -7
- package/dist/esm/ts-rs/TableRef.d.ts +9 -0
- package/dist/esm/ts-rs/ViewerConfig.d.ts +7 -6
- package/dist/esm/ts-rs/ViewerConfigUpdate.d.ts +7 -5
- package/dist/esm/ts-rs/WorkspaceConfig.d.ts +9 -1
- package/dist/esm/ts-rs/WorkspaceConfigUpdate.d.ts +9 -0
- package/dist/wasm/perspective-viewer.d.ts +115 -35
- package/dist/wasm/perspective-viewer.js +314 -190
- package/dist/wasm/perspective-viewer.wasm +0 -0
- package/dist/wasm/perspective-viewer.wasm.d.ts +21 -17
- package/package.json +31 -18
- package/src/css/column-selector.css +205 -50
- package/src/css/ui/containers/context-menu.css +12 -1
- package/src/css/ui/containers/dropdown-menu-items.css +29 -0
- package/src/css/ui/containers/scroll-panel.css +4 -1
- package/src/rust/agent/client/protocol.rs +0 -89
- package/src/rust/agent/markdown/mod.rs +0 -238
- package/src/rust/agent/preamble.md +1 -0
- package/src/rust/agent/tools.rs +2 -16
- package/src/rust/components/column_selector/active_column.rs +15 -5
- package/src/rust/components/column_selector/add_expression_button.rs +9 -26
- package/src/rust/components/column_selector/config_selector.rs +34 -6
- package/src/rust/components/column_selector/derived_control.rs +229 -0
- package/src/rust/components/column_selector/expr_edit_button.rs +3 -2
- package/src/rust/components/column_selector/filter_column.rs +0 -2
- package/src/rust/components/column_selector/inactive_column.rs +4 -5
- package/src/rust/components/column_selector/pivot_column.rs +21 -1
- package/src/rust/components/column_selector/sort_column.rs +4 -3
- package/src/rust/components/column_selector.rs +21 -1
- package/src/rust/components/column_settings_sidebar/save_settings.rs +5 -3
- package/src/rust/components/column_settings_sidebar/style_tab.rs +1 -0
- package/src/rust/components/column_settings_sidebar.rs +25 -19
- package/src/rust/components/debug_panel.rs +38 -29
- package/src/rust/components/dragdrop_list.rs +8 -9
- package/src/rust/components/main_panel/msg.rs +6 -4
- package/src/rust/components/main_panel/reconcile.rs +6 -1
- package/src/rust/components/main_panel/render.rs +70 -51
- package/src/rust/components/main_panel/update.rs +39 -1
- package/src/rust/components/main_panel.rs +97 -59
- package/src/rust/components/new_panel_menu.rs +69 -1
- package/src/rust/components/panel_menu.rs +63 -12
- package/src/rust/components/plugin_tab.rs +3 -2
- package/src/rust/components/render_warning.rs +2 -1
- package/src/rust/components/settings_panel.rs +4 -4
- package/src/rust/components/status_bar.rs +74 -24
- package/src/rust/components/status_indicator.rs +8 -1
- package/src/rust/components/viewer/filters.rs +13 -1
- package/src/rust/components/viewer/msg.rs +4 -0
- package/src/rust/components/viewer/panels.rs +41 -1
- package/src/rust/components/viewer/render.rs +1 -0
- package/src/rust/components/viewer/settings.rs +0 -22
- package/src/rust/components/viewer/snapshots.rs +2 -2
- package/src/rust/components/viewer/wiring.rs +14 -0
- package/src/rust/components/viewer.rs +45 -25
- package/src/rust/components/window_editor.rs +5 -4
- package/src/rust/config/column_config_schema.rs +38 -377
- package/src/rust/config/number_string_format.rs +0 -94
- package/src/rust/config/options.rs +3 -7
- package/src/rust/config/plugin_static_config.rs +0 -42
- package/src/rust/config/viewer_config.rs +47 -17
- package/src/rust/config/workspace_config.rs +158 -128
- package/src/rust/custom_elements/viewer.rs +187 -131
- package/src/rust/custom_events.rs +33 -18
- package/src/rust/derived.rs +266 -0
- package/src/rust/exprtk/tokenize.rs +0 -54
- package/src/rust/js/plugin.rs +1 -1
- package/src/rust/lib.rs +3 -0
- package/src/rust/presentation.rs +23 -4
- package/src/rust/queries/column_locator.rs +55 -48
- package/src/rust/queries/get_viewer_config.rs +1 -1
- package/src/rust/queries/plugin_column_styles.rs +22 -23
- package/src/rust/renderer/dispatch.rs +2 -0
- package/src/rust/renderer/plugin_config.rs +652 -487
- package/src/rust/renderer/props.rs +4 -0
- package/src/rust/renderer/registry.rs +2 -6
- package/src/rust/renderer/state.rs +205 -0
- package/src/rust/renderer.rs +201 -59
- package/src/rust/session/metadata.rs +30 -9
- package/src/rust/session/op_queue.rs +430 -0
- package/src/rust/session/panel_state.rs +327 -0
- package/src/rust/session/view_subscription.rs +42 -3
- package/src/rust/session.rs +900 -395
- package/src/rust/tasks/apply_global_filters.rs +49 -35
- package/src/rust/tasks/auto_pause.rs +22 -12
- package/src/rust/tasks/copy_export.rs +5 -0
- package/src/rust/tasks/create_panel.rs +34 -9
- package/src/rust/tasks/derive.rs +312 -0
- package/src/rust/tasks/mod.rs +6 -2
- package/src/rust/tasks/pipeline.rs +145 -89
- package/src/rust/tasks/reset_all.rs +31 -20
- package/src/rust/tasks/restore_derived.rs +200 -0
- package/src/rust/tasks/restore_panel.rs +153 -141
- package/src/rust/tasks/send_column_config.rs +56 -29
- package/src/rust/tasks/send_plugin_config.rs +52 -23
- package/src/rust/tasks/set_edit_mode.rs +33 -25
- package/src/rust/tasks/sync_update_panels.rs +39 -12
- package/src/rust/tasks/table_lifecycle.rs +70 -34
- package/src/rust/tasks/transactional_restore.rs +387 -0
- package/src/rust/tasks/update_theme.rs +45 -6
- package/src/rust/ui/containers/scroll_panel.rs +17 -10
- package/src/rust/ui/containers/split_panel.rs +0 -41
- package/src/rust/ui/context_menu.rs +157 -18
- package/src/rust/ui/font_loader.rs +23 -35
- package/src/rust/ui/form/mirrored_textarea.rs +0 -12
- package/src/rust/ui/form/multi_stop_gradient_selector.rs +0 -89
- package/src/rust/ui/form/palette_selector.rs +0 -51
- package/src/rust/ui/portal.rs +4 -10
- package/src/rust/utils/browser/mod.rs +2 -3
- package/src/{ts/perspective-viewer.inline.ts → rust/utils/browser/mouse.rs} +11 -10
- package/src/rust/utils/browser/request_animation_frame.rs +27 -8
- package/src/rust/utils/css.rs +0 -360
- package/src/rust/utils/local_fonts.rs +0 -36
- package/src/rust/utils/mod.rs +2 -3
- package/src/rust/utils/modal_position.rs +15 -21
- package/src/rust/utils/{browser/tests/mod.rs → plugin_host_state.rs} +31 -1
- package/src/rust/utils/pubsub.rs +0 -1
- package/src/rust/workspace.rs +187 -140
- package/src/svg/derived.svg +6 -0
- package/src/themes/icons.css +1 -0
- package/src/themes/intl/ar.css +10 -0
- package/src/themes/intl/de.css +10 -0
- package/src/themes/intl/es.css +10 -0
- package/src/themes/intl/fr.css +10 -0
- package/src/themes/intl/ja.css +10 -0
- package/src/themes/intl/pt.css +10 -0
- package/src/themes/intl/zh.css +10 -0
- package/src/themes/intl.css +10 -0
- package/src/ts/extensions.ts +3 -36
- package/src/ts/perspective-viewer.ts +1 -1
- package/src/ts/plugin.ts +4 -1
- package/src/{rust/utils/tests/request_animation_frame.rs → ts/select-detail.ts} +32 -26
- package/src/ts/ts-rs/DerivedTableConfig.ts +22 -0
- package/src/ts/ts-rs/PanelViewerConfig.ts +7 -6
- package/src/ts/ts-rs/RestoreOptions.ts +3 -7
- package/src/ts/ts-rs/TableRef.ts +9 -0
- package/src/ts/ts-rs/ViewerConfig.ts +7 -6
- package/src/ts/ts-rs/ViewerConfigUpdate.ts +7 -5
- package/src/ts/ts-rs/WorkspaceConfig.ts +7 -1
- package/src/ts/ts-rs/WorkspaceConfigUpdate.ts +7 -0
- package/dist/esm/perspective-viewer.inline.d.ts +0 -3
- package/dist/esm/perspective-viewer.inline.js +0 -21
- package/dist/esm/perspective-viewer.inline.js.map +0 -7
- package/src/rust/tasks/restore_and_render.rs +0 -107
- package/src/rust/utils/browser/tests/debounce.rs +0 -163
- package/src/rust/utils/tests/mod.rs +0 -14
- package/src/rust/utils/tests/pubsub.rs +0 -95
- /package/dist/wasm/snippets/{perspective-viewer-6813b568219c6763 → perspective-viewer-d0d525977a1acba2}/inline0.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-6813b568219c6763 → perspective-viewer-d0d525977a1acba2}/inline1.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-6813b568219c6763 → perspective-viewer-d0d525977a1acba2}/inline2.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-6813b568219c6763 → perspective-viewer-d0d525977a1acba2}/inline3.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-6813b568219c6763 → perspective-viewer-d0d525977a1acba2}/inline4.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-6813b568219c6763 → perspective-viewer-d0d525977a1acba2}/inline5.js +0 -0
|
@@ -11,23 +11,68 @@
|
|
|
11
11
|
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
|
12
12
|
|
|
13
13
|
//! The [`Renderer`]'s per-plugin config state: the plugin-level and
|
|
14
|
-
//! per-column buckets (`
|
|
14
|
+
//! per-column buckets (`PanelState::buckets`), their schema-aware strip/merge
|
|
15
15
|
//! write paths, and the restore-prep materialized snapshot.
|
|
16
16
|
|
|
17
|
-
use std::collections::HashMap;
|
|
17
|
+
use std::collections::{HashMap, HashSet};
|
|
18
|
+
use std::rc::Rc;
|
|
18
19
|
|
|
19
20
|
use futures::future::join_all;
|
|
20
21
|
use perspective_client::config::ViewConfig;
|
|
22
|
+
use perspective_client::proto::ColumnType;
|
|
21
23
|
use perspective_js::utils::{ApiError, ApiResult, JsValueSerdeExt};
|
|
22
24
|
use serde_json::Value;
|
|
23
25
|
use wasm_bindgen::prelude::*;
|
|
24
26
|
|
|
25
27
|
use super::Renderer;
|
|
28
|
+
use super::state::PluginRef;
|
|
26
29
|
use crate::config::*;
|
|
27
|
-
use crate::
|
|
30
|
+
use crate::js::plugin::JsPerspectiveViewerPlugin;
|
|
31
|
+
use crate::queries::{is_pivot_only_column, resolve_abs_max};
|
|
28
32
|
use crate::session::Session;
|
|
29
33
|
use crate::utils::{CssKind, CssLiteralUse, parse_var_ref, resolve_css_refs};
|
|
30
34
|
|
|
35
|
+
type ConfigMap = serde_json::Map<String, serde_json::Value>;
|
|
36
|
+
|
|
37
|
+
/// A schema query's answer: `Ok(None)` when the plugin declares no schema.
|
|
38
|
+
pub type SchemaResult = Result<Option<ColumnConfigSchema>, ValidationError>;
|
|
39
|
+
|
|
40
|
+
/// Where in the `ViewConfig` a column being configured appears, which is a
|
|
41
|
+
/// different axis from its plugin column slot.
|
|
42
|
+
#[derive(Clone, Copy, Debug, PartialEq)]
|
|
43
|
+
pub enum ColumnRole {
|
|
44
|
+
Column,
|
|
45
|
+
GroupBy,
|
|
46
|
+
SplitBy,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
impl ColumnRole {
|
|
50
|
+
pub fn as_str(&self) -> &'static str {
|
|
51
|
+
match self {
|
|
52
|
+
Self::Column => "column",
|
|
53
|
+
Self::GroupBy => "group_by",
|
|
54
|
+
Self::SplitBy => "split_by",
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/// Everything a column's schema is a function of, besides the column itself.
|
|
60
|
+
struct ColumnSchemaEnv<'a> {
|
|
61
|
+
view_config: &'a ViewConfig,
|
|
62
|
+
session: &'a Session,
|
|
63
|
+
|
|
64
|
+
/// The view types of a config not yet bound to a `View`; `None` reads the
|
|
65
|
+
/// bound `View`'s schema.
|
|
66
|
+
view_schema: Option<&'a HashMap<String, ColumnType>>,
|
|
67
|
+
|
|
68
|
+
/// Whether the session's column stats describe this config's table (`false`
|
|
69
|
+
/// for a restore that replaces it).
|
|
70
|
+
column_stats: bool,
|
|
71
|
+
|
|
72
|
+
/// The plugin-level config the column will live under.
|
|
73
|
+
plugin_config: &'a ConfigMap,
|
|
74
|
+
}
|
|
75
|
+
|
|
31
76
|
/// A per-column config map. Each inner [`serde_json::Map`] is a flat collection
|
|
32
77
|
/// of plugin-defined JSON keys whose shape is dictated by the active plugin's
|
|
33
78
|
/// [`crate::config::ColumnConfigSchema`].
|
|
@@ -35,7 +80,7 @@ pub type ColumnConfigMap = HashMap<String, serde_json::Map<String, serde_json::V
|
|
|
35
80
|
|
|
36
81
|
/// Per-plugin config bucket. Holds the per-column style map and the
|
|
37
82
|
/// plugin-level config map for one plugin. Buckets are keyed by plugin
|
|
38
|
-
/// name in [`
|
|
83
|
+
/// name in [`crate::session::PanelState::buckets`], so foreign keys from a
|
|
39
84
|
/// different plugin physically cannot appear in the active plugin's
|
|
40
85
|
/// bucket.
|
|
41
86
|
#[derive(Clone, Debug, Default)]
|
|
@@ -44,23 +89,438 @@ pub struct PluginScopedConfig {
|
|
|
44
89
|
pub plugin: serde_json::Map<String, serde_json::Value>,
|
|
45
90
|
}
|
|
46
91
|
|
|
92
|
+
/// The renderer's part of a restore that has passed every check.
|
|
93
|
+
pub struct PreparedRenderer {
|
|
94
|
+
/// The plugin selection to commit, when the restore ends on another plugin.
|
|
95
|
+
pub plugin: Option<PluginRef>,
|
|
96
|
+
|
|
97
|
+
/// The plugin whose bucket the validated updates belong to.
|
|
98
|
+
pub target_name: String,
|
|
99
|
+
pub plugin_config: ValidatedPluginConfig,
|
|
100
|
+
pub columns_config: ValidatedColumnsConfig,
|
|
101
|
+
pub theme: Option<Option<String>>,
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/// Which of the target plugin's buckets a [`Renderer::commit`] changed.
|
|
105
|
+
pub struct RendererChanges {
|
|
106
|
+
pub plugin_config: bool,
|
|
107
|
+
pub columns_config: bool,
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/// The plugin a bucket update is validated against and written to — named
|
|
111
|
+
/// EXPLICITLY, so a validation that precedes a plugin swap can name the swap
|
|
112
|
+
/// target rather than whichever plugin happens to be active.
|
|
113
|
+
pub struct PluginTarget {
|
|
114
|
+
pub element: JsPerspectiveViewerPlugin,
|
|
115
|
+
pub static_config: Rc<PluginStaticConfig>,
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/// What [`Renderer::apply_plugin_config`] writes for a validated
|
|
119
|
+
/// [`PluginConfigUpdate`] — every fallible check already done.
|
|
120
|
+
#[derive(Clone, Debug, PartialEq)]
|
|
121
|
+
pub enum ValidatedPluginConfig {
|
|
122
|
+
Missing,
|
|
123
|
+
Clear,
|
|
124
|
+
Set {
|
|
125
|
+
/// Keys to insert, already normalized and filtered to the schema's
|
|
126
|
+
/// active keys.
|
|
127
|
+
map: ConfigMap,
|
|
128
|
+
|
|
129
|
+
/// Keys the update set to their declared default: removed from the
|
|
130
|
+
/// bucket rather than stored.
|
|
131
|
+
remove: Vec<String>,
|
|
132
|
+
|
|
133
|
+
/// The schema's active keys, when a schema was available; the bucket is
|
|
134
|
+
/// pruned to them after the write.
|
|
135
|
+
active: Option<HashSet<String>>,
|
|
136
|
+
},
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/// What [`Renderer::apply_columns_config`] writes for a validated
|
|
140
|
+
/// [`ColumnConfigUpdate`]: per column, the normalized entry (an EMPTY entry
|
|
141
|
+
/// removes the column).
|
|
142
|
+
#[derive(Debug, PartialEq)]
|
|
143
|
+
pub enum ValidatedColumnsConfig {
|
|
144
|
+
Missing,
|
|
145
|
+
Clear,
|
|
146
|
+
Set(Vec<(String, ConfigMap)>),
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/// A bucket value the schema rejects.
|
|
150
|
+
#[derive(Debug, PartialEq)]
|
|
151
|
+
pub struct ValidationError(pub String);
|
|
152
|
+
|
|
153
|
+
impl From<ValidationError> for ApiError {
|
|
154
|
+
fn from(err: ValidationError) -> Self {
|
|
155
|
+
ApiError::from(JsValue::from_str(&err.0))
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/// Validate a plugin-level config update against the schema the merged result
|
|
160
|
+
/// would have.
|
|
161
|
+
pub fn validate_plugin_config(
|
|
162
|
+
current: &ConfigMap,
|
|
163
|
+
update: PluginConfigUpdate,
|
|
164
|
+
schema_of: &dyn Fn(&ConfigMap) -> SchemaResult,
|
|
165
|
+
) -> Result<ValidatedPluginConfig, ValidationError> {
|
|
166
|
+
match update {
|
|
167
|
+
OptionalUpdate::SetDefault => Ok(ValidatedPluginConfig::Clear),
|
|
168
|
+
OptionalUpdate::Missing => Ok(ValidatedPluginConfig::Missing),
|
|
169
|
+
OptionalUpdate::Update(mut map) => {
|
|
170
|
+
let mut merged = current.clone();
|
|
171
|
+
for (k, v) in &map {
|
|
172
|
+
merged.insert(k.clone(), v.clone());
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
let schema = schema_of(&merged)?;
|
|
176
|
+
let mut active = None;
|
|
177
|
+
let mut remove = vec![];
|
|
178
|
+
if let Some(s) = &schema {
|
|
179
|
+
let keys = s.active_keys();
|
|
180
|
+
map.retain(|k, _| keys.contains(k));
|
|
181
|
+
active = Some(keys);
|
|
182
|
+
let errors = normalize_values(s, &mut map);
|
|
183
|
+
if let Some((key, error)) = errors.first() {
|
|
184
|
+
return Err(ValidationError(format!(
|
|
185
|
+
"Invalid `plugin_config.{key}`: {error}"
|
|
186
|
+
)));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
let leaves = s.leaf_fields();
|
|
190
|
+
map.retain(|key, value| {
|
|
191
|
+
let is_default = leaves
|
|
192
|
+
.iter()
|
|
193
|
+
.any(|spec| matches_declared_default(spec, key, value));
|
|
194
|
+
if is_default {
|
|
195
|
+
remove.push(key.clone());
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
!is_default
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
Ok(ValidatedPluginConfig::Set {
|
|
203
|
+
map,
|
|
204
|
+
remove,
|
|
205
|
+
active,
|
|
206
|
+
})
|
|
207
|
+
},
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/// Validate a per-column config update, column by column, against the schema
|
|
212
|
+
/// each column's entry would have.
|
|
213
|
+
pub fn validate_columns_config(
|
|
214
|
+
update: ColumnConfigUpdate,
|
|
215
|
+
schema_of: &dyn Fn(&str, &ConfigMap) -> SchemaResult,
|
|
216
|
+
view_type_of: &dyn Fn(&str) -> Option<ColumnType>,
|
|
217
|
+
resolve_css: &dyn Fn(&str, &mut ConfigMap),
|
|
218
|
+
) -> Result<ValidatedColumnsConfig, ValidationError> {
|
|
219
|
+
match update {
|
|
220
|
+
OptionalUpdate::SetDefault => Ok(ValidatedColumnsConfig::Clear),
|
|
221
|
+
OptionalUpdate::Missing => Ok(ValidatedColumnsConfig::Missing),
|
|
222
|
+
OptionalUpdate::Update(map) => {
|
|
223
|
+
let mut stripped = Vec::with_capacity(map.len());
|
|
224
|
+
for (col, mut cfg) in map {
|
|
225
|
+
if let Some(schema) = schema_of(&col, &cfg)? {
|
|
226
|
+
let active = schema.active_keys();
|
|
227
|
+
cfg.retain(|k, _| active.contains(k));
|
|
228
|
+
let errors = normalize_values(&schema, &mut cfg);
|
|
229
|
+
if let Some((key, error)) = errors.first() {
|
|
230
|
+
let ty = view_type_of(&col)
|
|
231
|
+
.map(|t| format!(" for a {t} column"))
|
|
232
|
+
.unwrap_or_default();
|
|
233
|
+
|
|
234
|
+
return Err(ValidationError(format!(
|
|
235
|
+
"Invalid `columns_config[\"{col}\"].{key}`{ty}: {error}"
|
|
236
|
+
)));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
resolve_css(&col, &mut cfg);
|
|
240
|
+
strip_default_values(&schema, &mut cfg);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
stripped.push((col, cfg));
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
Ok(ValidatedColumnsConfig::Set(stripped))
|
|
247
|
+
},
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/// Whether `target`'s plugin element declares the schema method `name`.
|
|
252
|
+
fn declares(target: &PluginTarget, name: &str) -> bool {
|
|
253
|
+
js_sys::Reflect::get(&target.element, &JsValue::from_str(name))
|
|
254
|
+
.map(|f| f.is_function())
|
|
255
|
+
.unwrap_or(false)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/// Write a validated plugin-level update into `bucket`.
|
|
259
|
+
pub fn apply_plugin_config_to(
|
|
260
|
+
bucket: &mut PluginScopedConfig,
|
|
261
|
+
validated: ValidatedPluginConfig,
|
|
262
|
+
) -> bool {
|
|
263
|
+
match validated {
|
|
264
|
+
ValidatedPluginConfig::Missing => false,
|
|
265
|
+
ValidatedPluginConfig::Clear => {
|
|
266
|
+
let changed = !bucket.plugin.is_empty();
|
|
267
|
+
bucket.plugin.clear();
|
|
268
|
+
changed
|
|
269
|
+
},
|
|
270
|
+
ValidatedPluginConfig::Set {
|
|
271
|
+
map,
|
|
272
|
+
remove,
|
|
273
|
+
active,
|
|
274
|
+
} => {
|
|
275
|
+
let mut changed = false;
|
|
276
|
+
for key in remove {
|
|
277
|
+
if bucket.plugin.remove(&key).is_some() {
|
|
278
|
+
changed = true;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
for (k, v) in map {
|
|
283
|
+
let prev = bucket.plugin.insert(k, v.clone());
|
|
284
|
+
if prev.as_ref() != Some(&v) {
|
|
285
|
+
changed = true;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if let Some(active) = active {
|
|
290
|
+
let before = bucket.plugin.len();
|
|
291
|
+
bucket.plugin.retain(|k, _| active.contains(k));
|
|
292
|
+
changed |= bucket.plugin.len() != before;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
changed
|
|
296
|
+
},
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/// Write a validated per-column update into `bucket`.
|
|
301
|
+
pub fn apply_columns_config_to(
|
|
302
|
+
bucket: &mut PluginScopedConfig,
|
|
303
|
+
validated: ValidatedColumnsConfig,
|
|
304
|
+
) -> bool {
|
|
305
|
+
match validated {
|
|
306
|
+
ValidatedColumnsConfig::Missing => false,
|
|
307
|
+
ValidatedColumnsConfig::Clear => {
|
|
308
|
+
let was_nonempty = !bucket.columns.is_empty();
|
|
309
|
+
bucket.columns.clear();
|
|
310
|
+
was_nonempty
|
|
311
|
+
},
|
|
312
|
+
ValidatedColumnsConfig::Set(entries) => {
|
|
313
|
+
let mut changed = false;
|
|
314
|
+
for (col, cfg) in entries {
|
|
315
|
+
if cfg.is_empty() {
|
|
316
|
+
if bucket.columns.remove(&col).is_some() {
|
|
317
|
+
changed = true;
|
|
318
|
+
}
|
|
319
|
+
} else {
|
|
320
|
+
match bucket.columns.insert(col, cfg.clone()) {
|
|
321
|
+
None => changed = true,
|
|
322
|
+
Some(old) if old != cfg => changed = true,
|
|
323
|
+
_ => {},
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
changed
|
|
329
|
+
},
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
47
333
|
impl Renderer {
|
|
48
334
|
/// Name of the currently-active plugin (used as the key into
|
|
49
|
-
/// `
|
|
335
|
+
/// `PanelState::buckets`). Returns `None` when no plugin has been
|
|
50
336
|
/// activated yet.
|
|
51
337
|
fn active_plugin_name(&self) -> Option<String> {
|
|
52
|
-
Some(self.
|
|
338
|
+
Some(self.metadata().name.clone()).filter(|n| !n.is_empty())
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/// Rewrite the named plugin's bucket: `f` edits a COPY, which is then
|
|
342
|
+
/// swapped in as one new [`crate::session::PanelState`].
|
|
343
|
+
fn edit_bucket<R>(&self, name: &str, f: impl FnOnce(&mut PluginScopedConfig) -> R) -> R {
|
|
344
|
+
let state = self.state();
|
|
345
|
+
let mut bucket = state.bucket(name);
|
|
346
|
+
let result = f(&mut bucket);
|
|
347
|
+
self.swap(state.with_bucket(name, bucket));
|
|
348
|
+
result
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/// The active plugin as a [`PluginTarget`], selecting the registry default
|
|
352
|
+
/// when none is selected yet (the command half, like
|
|
353
|
+
/// [`Renderer::ensure_plugin_selected`]).
|
|
354
|
+
pub fn active_target(&self) -> ApiResult<PluginTarget> {
|
|
355
|
+
let element = self.ensure_plugin_selected()?;
|
|
356
|
+
Ok(PluginTarget {
|
|
357
|
+
element,
|
|
358
|
+
static_config: self.metadata(),
|
|
359
|
+
})
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/// The plugin at registry index `idx` as a [`PluginTarget`], WITHOUT
|
|
363
|
+
/// selecting it — for validating against a swap target before the swap.
|
|
364
|
+
pub fn target_at(&self, idx: usize) -> ApiResult<PluginTarget> {
|
|
365
|
+
let mut st = self.borrow_mut();
|
|
366
|
+
let element = st.plugin_store.plugins().get(idx).cloned();
|
|
367
|
+
let static_config = st.plugin_store.plugin_configs().get(idx).cloned();
|
|
368
|
+
Ok(PluginTarget {
|
|
369
|
+
element: element.ok_or("No Plugin")?,
|
|
370
|
+
static_config: static_config.ok_or("No Plugin")?,
|
|
371
|
+
})
|
|
53
372
|
}
|
|
54
373
|
|
|
55
|
-
///
|
|
374
|
+
/// Write a validated plugin-level update into `target`'s bucket.
|
|
375
|
+
pub fn apply_plugin_config(
|
|
376
|
+
&self,
|
|
377
|
+
target: &PluginTarget,
|
|
378
|
+
validated: ValidatedPluginConfig,
|
|
379
|
+
) -> bool {
|
|
380
|
+
self.edit_bucket(&target.static_config.name, |bucket| {
|
|
381
|
+
apply_plugin_config_to(bucket, validated)
|
|
382
|
+
})
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/// Write a validated per-column update into `target`'s bucket.
|
|
386
|
+
pub fn apply_columns_config(
|
|
387
|
+
&self,
|
|
388
|
+
target: &PluginTarget,
|
|
389
|
+
validated: ValidatedColumnsConfig,
|
|
390
|
+
) -> bool {
|
|
391
|
+
self.edit_bucket(&target.static_config.name, |bucket| {
|
|
392
|
+
apply_columns_config_to(bucket, validated)
|
|
393
|
+
})
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/// The plugin a [`PluginUpdate`] would END on, resolved WITHOUT selecting
|
|
397
|
+
/// anything: the swap target when the update names a different plugin (with
|
|
398
|
+
/// its store index, to commit), else the selected plugin, else the registry
|
|
399
|
+
/// default a first run would select.
|
|
400
|
+
pub fn resolve_target(
|
|
401
|
+
&self,
|
|
402
|
+
update: &PluginUpdate,
|
|
403
|
+
) -> ApiResult<(PluginTarget, Option<usize>)> {
|
|
404
|
+
if let Some((idx, _)) = self.resolve_plugin_update(update) {
|
|
405
|
+
return Ok((self.target_at(idx)?, Some(idx)));
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if let Some(plugin) = self.state().plugin.as_ref() {
|
|
409
|
+
return Ok((self.target_at(plugin.idx)?, None));
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
let (idx, _) = self
|
|
413
|
+
.resolve_plugin_update(&OptionalUpdate::SetDefault)
|
|
414
|
+
.ok_or("No Plugin")?;
|
|
415
|
+
|
|
416
|
+
Ok((self.target_at(idx)?, Some(idx)))
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/// The COMMIT half of a restore, as one swap of this renderer's state.
|
|
420
|
+
pub fn commit(&self, prepared: PreparedRenderer) -> RendererChanges {
|
|
421
|
+
let PreparedRenderer {
|
|
422
|
+
plugin,
|
|
423
|
+
target_name,
|
|
424
|
+
plugin_config,
|
|
425
|
+
columns_config,
|
|
426
|
+
theme,
|
|
427
|
+
} = prepared;
|
|
428
|
+
|
|
429
|
+
let mut next = (*self.state()).clone();
|
|
430
|
+
if let Some(plugin) = plugin {
|
|
431
|
+
next = next.with_plugin(plugin);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
let mut bucket = next.bucket(&target_name);
|
|
435
|
+
let changes = RendererChanges {
|
|
436
|
+
plugin_config: apply_plugin_config_to(&mut bucket, plugin_config),
|
|
437
|
+
columns_config: apply_columns_config_to(&mut bucket, columns_config),
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
if changes.plugin_config || changes.columns_config {
|
|
441
|
+
next = next.with_bucket(&target_name, bucket);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
if let Some(theme) = theme {
|
|
445
|
+
next = next.with_theme(theme);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
self.swap(next);
|
|
449
|
+
changes
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/// Validate BOTH bucket updates of a restore against `target` and the view
|
|
453
|
+
/// the restore would build (`view_config`, whose columns have the types in
|
|
454
|
+
/// `view_schema`) — before either is written, and without a `View`.
|
|
455
|
+
#[allow(clippy::too_many_arguments)]
|
|
456
|
+
pub fn prepare_buckets(
|
|
457
|
+
&self,
|
|
458
|
+
target: &PluginTarget,
|
|
459
|
+
session: &Session,
|
|
460
|
+
view_config: &ViewConfig,
|
|
461
|
+
view_schema: Option<&HashMap<String, ColumnType>>,
|
|
462
|
+
rebinds: bool,
|
|
463
|
+
plugin_config: PluginConfigUpdate,
|
|
464
|
+
columns_config: ColumnConfigUpdate,
|
|
465
|
+
) -> ApiResult<(ValidatedPluginConfig, ValidatedColumnsConfig)> {
|
|
466
|
+
let current = self.state().bucket(&target.static_config.name);
|
|
467
|
+
let plugin = validate_plugin_config(¤t.plugin, plugin_config, &|merged| {
|
|
468
|
+
if !declares(target, "plugin_config_schema") {
|
|
469
|
+
return Ok(None);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
self.query_plugin_config_schema_for(target, view_config, Some(merged))
|
|
473
|
+
.map(Some)
|
|
474
|
+
.map_err(|e| ValidationError(format!("`plugin_config` schema failed: {e}")))
|
|
475
|
+
})?;
|
|
476
|
+
|
|
477
|
+
let mut ends_with = current.clone();
|
|
478
|
+
apply_plugin_config_to(&mut ends_with, plugin.clone());
|
|
479
|
+
let columns = validate_columns_config(
|
|
480
|
+
columns_config,
|
|
481
|
+
&|col, cfg| {
|
|
482
|
+
let Some(view_schema) = view_schema else {
|
|
483
|
+
return Ok(None);
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
if !declares(target, "column_config_schema") {
|
|
487
|
+
return Ok(None);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
let env = ColumnSchemaEnv {
|
|
491
|
+
view_config,
|
|
492
|
+
session,
|
|
493
|
+
view_schema: Some(view_schema),
|
|
494
|
+
column_stats: !rebinds,
|
|
495
|
+
plugin_config: &ends_with.plugin,
|
|
496
|
+
};
|
|
497
|
+
|
|
498
|
+
self.query_column_config_schema_for(target, &env, col, Some(cfg))
|
|
499
|
+
.map(Some)
|
|
500
|
+
.map_err(|e| {
|
|
501
|
+
ValidationError(format!("`columns_config[\"{col}\"]` schema failed: {e}"))
|
|
502
|
+
})
|
|
503
|
+
},
|
|
504
|
+
&|col| view_schema.and_then(|x| x.get(col).copied()),
|
|
505
|
+
&|col, cfg| self.resolve_css_refs_in(col, cfg),
|
|
506
|
+
)?;
|
|
507
|
+
|
|
508
|
+
Ok((plugin, columns))
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/// The active plugin's per-column config map as the UI and `save()` see it
|
|
512
|
+
/// — pending style edits included.
|
|
56
513
|
pub fn all_columns_configs(&self) -> ColumnConfigMap {
|
|
57
514
|
self.active_plugin_name()
|
|
58
|
-
.
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
515
|
+
.map(|n| self.projected_bucket(&n).columns)
|
|
516
|
+
.unwrap_or_default()
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/// The active plugin's COMMITTED per-column config map — what a running
|
|
520
|
+
/// op's step reads, since the edits queued behind it have not happened.
|
|
521
|
+
pub fn committed_columns_configs(&self) -> ColumnConfigMap {
|
|
522
|
+
self.active_plugin_name()
|
|
523
|
+
.map(|n| self.state().bucket(&n).columns)
|
|
64
524
|
.unwrap_or_default()
|
|
65
525
|
}
|
|
66
526
|
|
|
@@ -119,7 +579,7 @@ impl Renderer {
|
|
|
119
579
|
view_config: &ViewConfig,
|
|
120
580
|
session: &Session,
|
|
121
581
|
) -> ColumnConfigMap {
|
|
122
|
-
let mut configs = self.
|
|
582
|
+
let mut configs = self.committed_columns_configs();
|
|
123
583
|
let mut to_warm: Vec<String> = vec![];
|
|
124
584
|
for (col, entry) in &configs {
|
|
125
585
|
if session
|
|
@@ -140,6 +600,7 @@ impl Renderer {
|
|
|
140
600
|
ControlSpec::Number {
|
|
141
601
|
key,
|
|
142
602
|
include: Some(true),
|
|
603
|
+
default_stat: Some(_),
|
|
143
604
|
..
|
|
144
605
|
} if !entry.contains_key(key)
|
|
145
606
|
)
|
|
@@ -194,12 +655,7 @@ impl Renderer {
|
|
|
194
655
|
/// Clear the active plugin's per-column config map.
|
|
195
656
|
pub fn reset_columns_configs(&self) {
|
|
196
657
|
if let Some(n) = self.active_plugin_name() {
|
|
197
|
-
self.
|
|
198
|
-
.plugin_states
|
|
199
|
-
.entry(n)
|
|
200
|
-
.or_default()
|
|
201
|
-
.columns
|
|
202
|
-
.clear();
|
|
658
|
+
self.edit_bucket(&n, |bucket| bucket.columns.clear());
|
|
203
659
|
}
|
|
204
660
|
}
|
|
205
661
|
|
|
@@ -210,83 +666,43 @@ impl Renderer {
|
|
|
210
666
|
column_name: &str,
|
|
211
667
|
) -> Option<serde_json::Map<String, serde_json::Value>> {
|
|
212
668
|
let n = self.active_plugin_name()?;
|
|
213
|
-
self.
|
|
214
|
-
.plugin_states
|
|
215
|
-
.get(&n)?
|
|
216
|
-
.columns
|
|
217
|
-
.get(column_name)
|
|
218
|
-
.cloned()
|
|
669
|
+
self.projected_bucket(&n).columns.get(column_name).cloned()
|
|
219
670
|
}
|
|
220
671
|
|
|
221
|
-
/// Wholesale update the active plugin's per-column config map
|
|
672
|
+
/// Wholesale update the active plugin's per-column config map:
|
|
673
|
+
/// [`validate_columns_config`] against the active target, then
|
|
674
|
+
/// [`Renderer::apply_columns_config`].
|
|
222
675
|
pub fn update_columns_configs(
|
|
223
676
|
&self,
|
|
224
677
|
view_config: &ViewConfig,
|
|
225
678
|
session: &Session,
|
|
226
679
|
update: ColumnConfigUpdate,
|
|
227
680
|
) -> ApiResult<bool> {
|
|
228
|
-
|
|
681
|
+
if self.active_plugin_name().is_none() {
|
|
229
682
|
return Ok(false);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
match update {
|
|
233
|
-
OptionalUpdate::SetDefault => {
|
|
234
|
-
let mut st = self.borrow_mut();
|
|
235
|
-
let bucket = st.plugin_states.entry(n).or_default();
|
|
236
|
-
let was_nonempty = !bucket.columns.is_empty();
|
|
237
|
-
bucket.columns.clear();
|
|
238
|
-
Ok(was_nonempty)
|
|
239
|
-
},
|
|
240
|
-
OptionalUpdate::Missing => Ok(false),
|
|
241
|
-
OptionalUpdate::Update(map) => {
|
|
242
|
-
let mut stripped: Vec<(String, serde_json::Map<String, serde_json::Value>)> =
|
|
243
|
-
Vec::with_capacity(map.len());
|
|
244
|
-
for (col, mut cfg) in map {
|
|
245
|
-
if let Ok(schema) =
|
|
246
|
-
self.query_column_config_schema(view_config, session, &col, Some(&cfg))
|
|
247
|
-
{
|
|
248
|
-
let active = schema.active_keys();
|
|
249
|
-
cfg.retain(|k, _| active.contains(k));
|
|
250
|
-
let errors = normalize_values(&schema, &mut cfg);
|
|
251
|
-
if let Some((key, error)) = errors.first() {
|
|
252
|
-
let ty = session
|
|
253
|
-
.metadata()
|
|
254
|
-
.get_column_view_type(&col)
|
|
255
|
-
.map(|t| format!(" for a {t} column"))
|
|
256
|
-
.unwrap_or_default();
|
|
257
|
-
|
|
258
|
-
return Err(ApiError::from(JsValue::from_str(&format!(
|
|
259
|
-
"Invalid `columns_config[\"{col}\"].{key}`{ty}: {error}"
|
|
260
|
-
))));
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
self.resolve_css_refs_in(&col, &mut cfg);
|
|
264
|
-
strip_default_values(&schema, &mut cfg);
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
stripped.push((col, cfg));
|
|
268
|
-
}
|
|
683
|
+
}
|
|
269
684
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
Some(old) if old != cfg => changed = true,
|
|
282
|
-
_ => {},
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
}
|
|
685
|
+
let target = self.active_target()?;
|
|
686
|
+
let validated = validate_columns_config(
|
|
687
|
+
update,
|
|
688
|
+
&|col, cfg| {
|
|
689
|
+
let env = ColumnSchemaEnv {
|
|
690
|
+
view_config,
|
|
691
|
+
session,
|
|
692
|
+
view_schema: None,
|
|
693
|
+
column_stats: true,
|
|
694
|
+
plugin_config: &self.committed_plugin_config(),
|
|
695
|
+
};
|
|
286
696
|
|
|
287
|
-
Ok(
|
|
697
|
+
Ok(self
|
|
698
|
+
.query_column_config_schema_for(&target, &env, col, Some(cfg))
|
|
699
|
+
.ok())
|
|
288
700
|
},
|
|
289
|
-
|
|
701
|
+
&|col| session.metadata().get_column_view_type(col),
|
|
702
|
+
&|col, cfg| self.resolve_css_refs_in(col, cfg),
|
|
703
|
+
)?;
|
|
704
|
+
|
|
705
|
+
Ok(self.apply_columns_config(&target, validated))
|
|
290
706
|
}
|
|
291
707
|
|
|
292
708
|
/// Apply a single schema-field update from the column-style UI to
|
|
@@ -304,7 +720,8 @@ impl Renderer {
|
|
|
304
720
|
return;
|
|
305
721
|
};
|
|
306
722
|
|
|
307
|
-
let current_value = self.
|
|
723
|
+
let current_value = self.state().bucket(&n).columns.get(&column_name).cloned();
|
|
724
|
+
|
|
308
725
|
if let Ok(schema) = self.query_column_config_schema(
|
|
309
726
|
view_config,
|
|
310
727
|
session,
|
|
@@ -319,9 +736,7 @@ impl Renderer {
|
|
|
319
736
|
strip_default_values(&schema, &mut update.value);
|
|
320
737
|
}
|
|
321
738
|
|
|
322
|
-
let next = {
|
|
323
|
-
let mut st = self.borrow_mut();
|
|
324
|
-
let bucket = st.plugin_states.entry(n.clone()).or_default();
|
|
739
|
+
let next = self.edit_bucket(&n, |bucket| {
|
|
325
740
|
let entry = bucket.columns.entry(column_name.clone()).or_default();
|
|
326
741
|
for k in &update.keys {
|
|
327
742
|
entry.remove(k);
|
|
@@ -333,7 +748,7 @@ impl Renderer {
|
|
|
333
748
|
}
|
|
334
749
|
|
|
335
750
|
entry.clone()
|
|
336
|
-
};
|
|
751
|
+
});
|
|
337
752
|
|
|
338
753
|
// The schema AFTER the update decides which keys survive and, for a
|
|
339
754
|
// key whose control kind follows another key (`fg_color` reads as a
|
|
@@ -345,44 +760,45 @@ impl Renderer {
|
|
|
345
760
|
.query_column_config_schema(view_config, session, &column_name, Some(&next))
|
|
346
761
|
.ok();
|
|
347
762
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
763
|
+
self.edit_bucket(&n, |bucket| {
|
|
764
|
+
if let Some(entry) = bucket.columns.get_mut(&column_name) {
|
|
765
|
+
if let Some(schema) = &after {
|
|
766
|
+
let active = schema.active_keys();
|
|
767
|
+
entry.retain(|k, _| active.contains(k));
|
|
768
|
+
for (key, error) in normalize_values(schema, entry) {
|
|
769
|
+
tracing::warn!(
|
|
770
|
+
"Dropping `{column_name}`.`{key}` after a mode change: {error}"
|
|
771
|
+
);
|
|
772
|
+
}
|
|
356
773
|
}
|
|
357
|
-
}
|
|
358
774
|
|
|
359
|
-
|
|
360
|
-
|
|
775
|
+
if entry.is_empty() {
|
|
776
|
+
bucket.columns.remove(&column_name);
|
|
777
|
+
}
|
|
361
778
|
}
|
|
362
|
-
}
|
|
779
|
+
})
|
|
363
780
|
}
|
|
364
781
|
|
|
365
|
-
///
|
|
782
|
+
/// The active plugin's plugin-level config map as the UI and `save()` see
|
|
783
|
+
/// it — pending settings edits included.
|
|
366
784
|
pub fn get_plugin_config(&self) -> serde_json::Map<String, serde_json::Value> {
|
|
367
785
|
self.active_plugin_name()
|
|
368
|
-
.
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
786
|
+
.map(|n| self.projected_bucket(&n).plugin)
|
|
787
|
+
.unwrap_or_default()
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
/// The active plugin's COMMITTED plugin-level config map — what a running
|
|
791
|
+
/// op's step reads.
|
|
792
|
+
pub fn committed_plugin_config(&self) -> serde_json::Map<String, serde_json::Value> {
|
|
793
|
+
self.active_plugin_name()
|
|
794
|
+
.map(|n| self.state().bucket(&n).plugin)
|
|
374
795
|
.unwrap_or_default()
|
|
375
796
|
}
|
|
376
797
|
|
|
377
798
|
/// Clear the active plugin's plugin-level config map.
|
|
378
799
|
pub fn reset_plugin_config(&self) {
|
|
379
800
|
if let Some(n) = self.active_plugin_name() {
|
|
380
|
-
self.
|
|
381
|
-
.plugin_states
|
|
382
|
-
.entry(n)
|
|
383
|
-
.or_default()
|
|
384
|
-
.plugin
|
|
385
|
-
.clear();
|
|
801
|
+
self.edit_bucket(&n, |bucket| bucket.plugin.clear());
|
|
386
802
|
}
|
|
387
803
|
}
|
|
388
804
|
|
|
@@ -392,12 +808,25 @@ impl Renderer {
|
|
|
392
808
|
fn query_plugin_config_schema(
|
|
393
809
|
&self,
|
|
394
810
|
view_config: &ViewConfig,
|
|
395
|
-
current_value: Option<&
|
|
811
|
+
current_value: Option<&ConfigMap>,
|
|
812
|
+
) -> ApiResult<ColumnConfigSchema> {
|
|
813
|
+
let target = self.active_target()?;
|
|
814
|
+
self.query_plugin_config_schema_for(&target, view_config, current_value)
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
/// [`Self::query_plugin_config_schema`] against an explicit target.
|
|
818
|
+
fn query_plugin_config_schema_for(
|
|
819
|
+
&self,
|
|
820
|
+
target: &PluginTarget,
|
|
821
|
+
view_config: &ViewConfig,
|
|
822
|
+
current_value: Option<&ConfigMap>,
|
|
396
823
|
) -> ApiResult<ColumnConfigSchema> {
|
|
397
|
-
let plugin = self.ensure_plugin_selected()?;
|
|
398
824
|
let view_config_js = JsValue::from_serde_ext(view_config).unwrap_or(JsValue::NULL);
|
|
399
825
|
let current_js = JsValue::from_serde_ext(¤t_value).unwrap_or(JsValue::NULL);
|
|
400
|
-
let raw =
|
|
826
|
+
let raw = target
|
|
827
|
+
.element
|
|
828
|
+
._plugin_config_schema(&view_config_js, ¤t_js)?;
|
|
829
|
+
|
|
401
830
|
serde_wasm_bindgen::from_value::<ColumnConfigSchema>(raw)
|
|
402
831
|
.map(|schema| schema.canonicalize())
|
|
403
832
|
.map_err(|e| e.into())
|
|
@@ -411,11 +840,40 @@ impl Renderer {
|
|
|
411
840
|
view_config: &ViewConfig,
|
|
412
841
|
session: &Session,
|
|
413
842
|
column_name: &str,
|
|
414
|
-
current_value: Option<&
|
|
843
|
+
current_value: Option<&ConfigMap>,
|
|
415
844
|
) -> ApiResult<ColumnConfigSchema> {
|
|
416
|
-
let
|
|
417
|
-
let
|
|
418
|
-
|
|
845
|
+
let target = self.active_target()?;
|
|
846
|
+
let env = ColumnSchemaEnv {
|
|
847
|
+
view_config,
|
|
848
|
+
session,
|
|
849
|
+
view_schema: None,
|
|
850
|
+
column_stats: true,
|
|
851
|
+
plugin_config: &self.committed_plugin_config(),
|
|
852
|
+
};
|
|
853
|
+
|
|
854
|
+
self.query_column_config_schema_for(&target, &env, column_name, current_value)
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
/// [`Self::query_column_config_schema`] against an explicit target: the
|
|
858
|
+
/// column's group comes from the TARGET's static config, and its view type
|
|
859
|
+
/// from `view_schema` when given (a config not yet bound to a `View`), else
|
|
860
|
+
/// from the bound `View`'s schema.
|
|
861
|
+
fn query_column_config_schema_for(
|
|
862
|
+
&self,
|
|
863
|
+
target: &PluginTarget,
|
|
864
|
+
env: &ColumnSchemaEnv<'_>,
|
|
865
|
+
column_name: &str,
|
|
866
|
+
current_value: Option<&ConfigMap>,
|
|
867
|
+
) -> ApiResult<ColumnConfigSchema> {
|
|
868
|
+
let ColumnSchemaEnv {
|
|
869
|
+
view_config,
|
|
870
|
+
session,
|
|
871
|
+
view_schema,
|
|
872
|
+
column_stats,
|
|
873
|
+
plugin_config,
|
|
874
|
+
} = *env;
|
|
875
|
+
|
|
876
|
+
let names = &target.static_config.config_column_names;
|
|
419
877
|
let group = view_config
|
|
420
878
|
.columns
|
|
421
879
|
.iter()
|
|
@@ -423,115 +881,84 @@ impl Renderer {
|
|
|
423
881
|
.and_then(|idx| names.get(idx))
|
|
424
882
|
.map(|s| s.as_str());
|
|
425
883
|
|
|
426
|
-
if !
|
|
427
|
-
|
|
428
|
-
}
|
|
429
|
-
|
|
884
|
+
let role = if !is_pivot_only_column(column_name, view_config) {
|
|
885
|
+
ColumnRole::Column
|
|
886
|
+
} else if view_config.split_by.iter().any(|x| x == column_name) {
|
|
887
|
+
ColumnRole::SplitBy
|
|
888
|
+
} else {
|
|
889
|
+
ColumnRole::GroupBy
|
|
890
|
+
};
|
|
891
|
+
|
|
892
|
+
let view_type = match view_schema {
|
|
893
|
+
Some(view_schema) => view_schema.get(column_name).copied(),
|
|
894
|
+
None => {
|
|
895
|
+
if !session.metadata().has_view_schema() {
|
|
896
|
+
return Err(JsValue::from("view_schema not initialized").into());
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
session.metadata().get_column_view_type(column_name)
|
|
900
|
+
},
|
|
901
|
+
};
|
|
902
|
+
|
|
903
|
+
let view_type = match role {
|
|
904
|
+
ColumnRole::Column => view_type,
|
|
905
|
+
_ => session
|
|
906
|
+
.metadata()
|
|
907
|
+
.get_column_table_type(column_name)
|
|
908
|
+
.or(view_type),
|
|
909
|
+
};
|
|
910
|
+
|
|
911
|
+
let Some(view_type) = view_type else {
|
|
430
912
|
return Ok(ColumnConfigSchema { fields: vec![] });
|
|
431
913
|
};
|
|
432
914
|
|
|
433
915
|
let current_js = JsValue::from_serde_ext(¤t_value).unwrap_or(JsValue::NULL);
|
|
434
916
|
let view_config_js = JsValue::from_serde_ext(view_config).unwrap_or(JsValue::NULL);
|
|
435
|
-
let
|
|
436
|
-
let
|
|
437
|
-
"abs_max": stats.abs_max,
|
|
438
|
-
});
|
|
439
|
-
let stats_js = JsValue::from_serde_ext(&stats_json).unwrap_or(JsValue::NULL);
|
|
440
|
-
|
|
441
|
-
let raw = plugin._column_config_schema(
|
|
917
|
+
let plugin_config_js = JsValue::from_serde_ext(plugin_config).unwrap_or(JsValue::NULL);
|
|
918
|
+
let raw = target.element._column_config_schema(
|
|
442
919
|
&view_type.to_string(),
|
|
443
920
|
group,
|
|
444
921
|
column_name,
|
|
445
922
|
¤t_js,
|
|
446
923
|
&view_config_js,
|
|
447
|
-
&
|
|
924
|
+
&plugin_config_js,
|
|
925
|
+
role.as_str(),
|
|
448
926
|
)?;
|
|
449
927
|
|
|
928
|
+
let abs_max = column_stats
|
|
929
|
+
.then(|| {
|
|
930
|
+
session
|
|
931
|
+
.get_column_stats(column_name)
|
|
932
|
+
.and_then(|s| s.abs_max)
|
|
933
|
+
})
|
|
934
|
+
.flatten();
|
|
935
|
+
|
|
450
936
|
serde_wasm_bindgen::from_value::<ColumnConfigSchema>(raw)
|
|
451
|
-
.map(|schema| schema.canonicalize())
|
|
937
|
+
.map(|schema| schema.canonicalize().resolve_stat_defaults(abs_max))
|
|
452
938
|
.map_err(|e| e.into())
|
|
453
939
|
}
|
|
454
940
|
|
|
455
|
-
/// Merge an update into the active plugin's plugin-level config map
|
|
456
|
-
///
|
|
457
|
-
///
|
|
941
|
+
/// Merge an update into the active plugin's plugin-level config map:
|
|
942
|
+
/// [`validate_plugin_config`] against the active target, then
|
|
943
|
+
/// [`Renderer::apply_plugin_config`].
|
|
458
944
|
pub fn update_plugin_config(
|
|
459
945
|
&self,
|
|
460
946
|
view_config: &ViewConfig,
|
|
461
947
|
update: PluginConfigUpdate,
|
|
462
948
|
) -> ApiResult<bool> {
|
|
463
|
-
|
|
949
|
+
if self.active_plugin_name().is_none() {
|
|
464
950
|
return Ok(false);
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
match update {
|
|
468
|
-
OptionalUpdate::SetDefault => {
|
|
469
|
-
let mut st = self.borrow_mut();
|
|
470
|
-
let bucket = st.plugin_states.entry(n).or_default();
|
|
471
|
-
let changed = !bucket.plugin.is_empty();
|
|
472
|
-
bucket.plugin.clear();
|
|
473
|
-
Ok(changed)
|
|
474
|
-
},
|
|
475
|
-
OptionalUpdate::Missing => Ok(false),
|
|
476
|
-
OptionalUpdate::Update(mut map) => {
|
|
477
|
-
let mut merged = self.get_plugin_config();
|
|
478
|
-
for (k, v) in &map {
|
|
479
|
-
merged.insert(k.clone(), v.clone());
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
let schema = self
|
|
483
|
-
.query_plugin_config_schema(view_config, Some(&merged))
|
|
484
|
-
.ok();
|
|
485
|
-
|
|
486
|
-
let mut active = None;
|
|
487
|
-
if let Some(s) = &schema {
|
|
488
|
-
let keys = s.active_keys();
|
|
489
|
-
map.retain(|k, _| keys.contains(k));
|
|
490
|
-
active = Some(keys);
|
|
491
|
-
let errors = normalize_values(s, &mut map);
|
|
492
|
-
if let Some((key, error)) = errors.first() {
|
|
493
|
-
return Err(ApiError::from(JsValue::from_str(&format!(
|
|
494
|
-
"Invalid `plugin_config.{key}`: {error}"
|
|
495
|
-
))));
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
let mut st = self.borrow_mut();
|
|
500
|
-
let bucket = st.plugin_states.entry(n).or_default();
|
|
501
|
-
let mut changed = false;
|
|
502
|
-
if let Some(s) = &schema {
|
|
503
|
-
let leaves = s.leaf_fields();
|
|
504
|
-
map.retain(|key, value| {
|
|
505
|
-
let is_default = leaves
|
|
506
|
-
.iter()
|
|
507
|
-
.any(|spec| matches_declared_default(spec, key, value));
|
|
508
|
-
if is_default {
|
|
509
|
-
if bucket.plugin.remove(key).is_some() {
|
|
510
|
-
changed = true;
|
|
511
|
-
}
|
|
512
|
-
false
|
|
513
|
-
} else {
|
|
514
|
-
true
|
|
515
|
-
}
|
|
516
|
-
});
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
for (k, v) in map {
|
|
520
|
-
let prev = bucket.plugin.insert(k, v.clone());
|
|
521
|
-
if prev.as_ref() != Some(&v) {
|
|
522
|
-
changed = true;
|
|
523
|
-
}
|
|
524
|
-
}
|
|
951
|
+
}
|
|
525
952
|
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
953
|
+
let target = self.active_target()?;
|
|
954
|
+
let current = self.committed_plugin_config();
|
|
955
|
+
let validated = validate_plugin_config(¤t, update, &|merged| {
|
|
956
|
+
Ok(self
|
|
957
|
+
.query_plugin_config_schema_for(&target, view_config, Some(merged))
|
|
958
|
+
.ok())
|
|
959
|
+
})?;
|
|
531
960
|
|
|
532
|
-
|
|
533
|
-
},
|
|
534
|
-
}
|
|
961
|
+
Ok(self.apply_plugin_config(&target, validated))
|
|
535
962
|
}
|
|
536
963
|
|
|
537
964
|
/// Apply a single schema-field update from the plugin-settings UI,
|
|
@@ -545,7 +972,7 @@ impl Renderer {
|
|
|
545
972
|
return false;
|
|
546
973
|
};
|
|
547
974
|
|
|
548
|
-
let mut next = self.
|
|
975
|
+
let mut next = self.committed_plugin_config();
|
|
549
976
|
for k in &update.keys {
|
|
550
977
|
match update.value.get(k) {
|
|
551
978
|
Some(v) => {
|
|
@@ -569,29 +996,28 @@ impl Renderer {
|
|
|
569
996
|
strip_default_values(schema, &mut update.value);
|
|
570
997
|
}
|
|
571
998
|
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
999
|
+
self.edit_bucket(&n, |bucket| {
|
|
1000
|
+
let mut changed = false;
|
|
1001
|
+
for k in &update.keys {
|
|
1002
|
+
if let Some(v) = update.value.get(k) {
|
|
1003
|
+
let prev = bucket.plugin.insert(k.to_string(), v.clone());
|
|
1004
|
+
if prev.as_ref() != Some(v) {
|
|
1005
|
+
changed = true;
|
|
1006
|
+
}
|
|
1007
|
+
} else if bucket.plugin.remove(k).is_some() {
|
|
580
1008
|
changed = true;
|
|
581
1009
|
}
|
|
582
|
-
} else if bucket.plugin.remove(k).is_some() {
|
|
583
|
-
changed = true;
|
|
584
1010
|
}
|
|
585
|
-
}
|
|
586
1011
|
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
1012
|
+
if let Some(schema) = &schema {
|
|
1013
|
+
let active = schema.active_keys();
|
|
1014
|
+
let before = bucket.plugin.len();
|
|
1015
|
+
bucket.plugin.retain(|k, _| active.contains(k));
|
|
1016
|
+
changed |= bucket.plugin.len() != before;
|
|
1017
|
+
}
|
|
593
1018
|
|
|
594
|
-
|
|
1019
|
+
changed
|
|
1020
|
+
})
|
|
595
1021
|
}
|
|
596
1022
|
}
|
|
597
1023
|
|
|
@@ -701,264 +1127,3 @@ fn css_matches(kind: CssKind, value: &Value, default: &str) -> bool {
|
|
|
701
1127
|
.and_then(|src| kind.canonicalize(src).ok())
|
|
702
1128
|
.is_some_and(|canonical| canonical == default)
|
|
703
1129
|
}
|
|
704
|
-
|
|
705
|
-
#[cfg(test)]
|
|
706
|
-
mod tests {
|
|
707
|
-
use serde_json::json;
|
|
708
|
-
|
|
709
|
-
use super::*;
|
|
710
|
-
|
|
711
|
-
#[test]
|
|
712
|
-
fn palette_default_matches_canonically() {
|
|
713
|
-
let spec = ControlSpec::Palette {
|
|
714
|
-
key: "palette".to_owned(),
|
|
715
|
-
default: "linear-gradient(to right, #0366d6, #ff7f0e)".to_owned(),
|
|
716
|
-
max: None,
|
|
717
|
-
};
|
|
718
|
-
|
|
719
|
-
assert!(matches_declared_default(
|
|
720
|
-
&spec,
|
|
721
|
-
"palette",
|
|
722
|
-
&json!("linear-gradient(to right, #0366d6, #ff7f0e)")
|
|
723
|
-
));
|
|
724
|
-
|
|
725
|
-
assert!(matches_declared_default(
|
|
726
|
-
&spec,
|
|
727
|
-
"palette",
|
|
728
|
-
&json!("linear-gradient(90deg, RGB(3,102,214), #FF7F0E)")
|
|
729
|
-
));
|
|
730
|
-
|
|
731
|
-
assert!(!matches_declared_default(
|
|
732
|
-
&spec,
|
|
733
|
-
"palette",
|
|
734
|
-
&json!("linear-gradient(to right, #ff7f0e, #0366d6)")
|
|
735
|
-
));
|
|
736
|
-
|
|
737
|
-
assert!(!matches_declared_default(
|
|
738
|
-
&spec,
|
|
739
|
-
"palette",
|
|
740
|
-
&json!("var(--psp-user--palette-1)")
|
|
741
|
-
));
|
|
742
|
-
|
|
743
|
-
assert!(!matches_declared_default(
|
|
744
|
-
&spec,
|
|
745
|
-
"palette",
|
|
746
|
-
&json!(["#0366d6"])
|
|
747
|
-
));
|
|
748
|
-
assert!(!matches_declared_default(
|
|
749
|
-
&spec,
|
|
750
|
-
"other",
|
|
751
|
-
&json!("linear-gradient(to right, #0366d6, #ff7f0e)")
|
|
752
|
-
));
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
#[test]
|
|
756
|
-
fn gradient_default_matches_canonical_values() {
|
|
757
|
-
let spec = ControlSpec::GradientStops {
|
|
758
|
-
key: "gradient".to_owned(),
|
|
759
|
-
default: "linear-gradient(to right, #0366d6 0%, #ff7f0e 33.3%)".to_owned(),
|
|
760
|
-
discrete: false,
|
|
761
|
-
};
|
|
762
|
-
|
|
763
|
-
assert!(matches_declared_default(
|
|
764
|
-
&spec,
|
|
765
|
-
"gradient",
|
|
766
|
-
&json!("linear-gradient(#0366d6, #ff7f0e 33.3%)")
|
|
767
|
-
));
|
|
768
|
-
|
|
769
|
-
assert!(!matches_declared_default(
|
|
770
|
-
&spec,
|
|
771
|
-
"gradient",
|
|
772
|
-
&json!("linear-gradient(to right, #0366d6 0%, #ff7f0e 33.4%)")
|
|
773
|
-
));
|
|
774
|
-
|
|
775
|
-
let color = ControlSpec::Color {
|
|
776
|
-
key: "color".to_owned(),
|
|
777
|
-
default: "#ff7f0e".to_owned(),
|
|
778
|
-
};
|
|
779
|
-
|
|
780
|
-
assert!(matches_declared_default(&color, "color", &json!("#FF7F0E")));
|
|
781
|
-
assert!(matches_declared_default(
|
|
782
|
-
&color,
|
|
783
|
-
"color",
|
|
784
|
-
&json!("rgb(255, 127, 14)")
|
|
785
|
-
));
|
|
786
|
-
assert!(!matches_declared_default(
|
|
787
|
-
&color,
|
|
788
|
-
"color",
|
|
789
|
-
&json!("var(--psp-user--color-1)")
|
|
790
|
-
));
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
#[test]
|
|
794
|
-
fn alignment_strips_only_a_declared_default() {
|
|
795
|
-
let schema = ColumnConfigSchema {
|
|
796
|
-
fields: vec![
|
|
797
|
-
ControlSpec::Alignment {
|
|
798
|
-
key: "align".to_owned(),
|
|
799
|
-
default: None,
|
|
800
|
-
corners: false,
|
|
801
|
-
},
|
|
802
|
-
ControlSpec::Alignment {
|
|
803
|
-
key: "legend_anchor".to_owned(),
|
|
804
|
-
default: Some(Alignment::TopRight),
|
|
805
|
-
corners: true,
|
|
806
|
-
},
|
|
807
|
-
],
|
|
808
|
-
};
|
|
809
|
-
|
|
810
|
-
let mut map = json!({ "align": "center", "legend_anchor": "top-right" })
|
|
811
|
-
.as_object()
|
|
812
|
-
.unwrap()
|
|
813
|
-
.clone();
|
|
814
|
-
|
|
815
|
-
strip_default_values(&schema, &mut map);
|
|
816
|
-
assert_eq!(
|
|
817
|
-
map,
|
|
818
|
-
json!({ "align": "center" }).as_object().unwrap().clone()
|
|
819
|
-
);
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
#[test]
|
|
823
|
-
fn strip_default_values_sees_through_groups() {
|
|
824
|
-
let leaves = vec![
|
|
825
|
-
ControlSpec::Bool {
|
|
826
|
-
key: "flag".to_owned(),
|
|
827
|
-
default: false,
|
|
828
|
-
},
|
|
829
|
-
ControlSpec::Number {
|
|
830
|
-
key: "size".to_owned(),
|
|
831
|
-
default: 3.0,
|
|
832
|
-
include: None,
|
|
833
|
-
min: None,
|
|
834
|
-
max: None,
|
|
835
|
-
step: None,
|
|
836
|
-
},
|
|
837
|
-
];
|
|
838
|
-
|
|
839
|
-
let flat = ColumnConfigSchema {
|
|
840
|
-
fields: leaves.clone(),
|
|
841
|
-
};
|
|
842
|
-
|
|
843
|
-
let grouped = ColumnConfigSchema {
|
|
844
|
-
fields: vec![ControlSpec::Group {
|
|
845
|
-
key: "section".to_owned(),
|
|
846
|
-
fields: leaves,
|
|
847
|
-
}],
|
|
848
|
-
};
|
|
849
|
-
|
|
850
|
-
let src = json!({ "flag": false, "size": 4.0, "foreign": 1 })
|
|
851
|
-
.as_object()
|
|
852
|
-
.unwrap()
|
|
853
|
-
.clone();
|
|
854
|
-
|
|
855
|
-
let mut a = src.clone();
|
|
856
|
-
let mut b = src;
|
|
857
|
-
strip_default_values(&flat, &mut a);
|
|
858
|
-
strip_default_values(&grouped, &mut b);
|
|
859
|
-
assert_eq!(a, b);
|
|
860
|
-
assert_eq!(
|
|
861
|
-
a,
|
|
862
|
-
json!({ "size": 4.0, "foreign": 1 })
|
|
863
|
-
.as_object()
|
|
864
|
-
.unwrap()
|
|
865
|
-
.clone()
|
|
866
|
-
);
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
#[test]
|
|
870
|
-
fn normalize_values_rejects_enum_values_outside_the_variants() {
|
|
871
|
-
let schema = ColumnConfigSchema {
|
|
872
|
-
fields: vec![ControlSpec::Group {
|
|
873
|
-
key: "color".to_owned(),
|
|
874
|
-
fields: vec![ControlSpec::Enum {
|
|
875
|
-
key: "fg_mode".to_owned(),
|
|
876
|
-
default: "color".to_owned(),
|
|
877
|
-
variants: vec![
|
|
878
|
-
EnumVariant {
|
|
879
|
-
value: "disabled".to_owned(),
|
|
880
|
-
label: None,
|
|
881
|
-
},
|
|
882
|
-
EnumVariant {
|
|
883
|
-
value: "color".to_owned(),
|
|
884
|
-
label: None,
|
|
885
|
-
},
|
|
886
|
-
],
|
|
887
|
-
}],
|
|
888
|
-
}],
|
|
889
|
-
};
|
|
890
|
-
|
|
891
|
-
let mut map = json!({ "fg_mode": "color", "other": "series" })
|
|
892
|
-
.as_object()
|
|
893
|
-
.unwrap()
|
|
894
|
-
.clone();
|
|
895
|
-
|
|
896
|
-
assert!(normalize_values(&schema, &mut map).is_empty());
|
|
897
|
-
assert_eq!(map.len(), 2);
|
|
898
|
-
|
|
899
|
-
let mut map = json!({ "fg_mode": "series" }).as_object().unwrap().clone();
|
|
900
|
-
let errors = normalize_values(&schema, &mut map);
|
|
901
|
-
assert_eq!(errors.len(), 1);
|
|
902
|
-
assert_eq!(errors[0].0, "fg_mode");
|
|
903
|
-
assert_eq!(errors[0].1, "\"series\" is not one of `disabled`, `color`");
|
|
904
|
-
assert!(map.is_empty());
|
|
905
|
-
|
|
906
|
-
let mut map = json!({ "fg_mode": 3 }).as_object().unwrap().clone();
|
|
907
|
-
let errors = normalize_values(&schema, &mut map);
|
|
908
|
-
assert_eq!(errors.len(), 1);
|
|
909
|
-
assert!(map.is_empty());
|
|
910
|
-
}
|
|
911
|
-
|
|
912
|
-
#[test]
|
|
913
|
-
fn normalize_values_canonicalizes_and_reports() {
|
|
914
|
-
let schema = ColumnConfigSchema {
|
|
915
|
-
fields: vec![
|
|
916
|
-
ControlSpec::Color {
|
|
917
|
-
key: "color".to_owned(),
|
|
918
|
-
default: "#000000".to_owned(),
|
|
919
|
-
},
|
|
920
|
-
ControlSpec::Palette {
|
|
921
|
-
key: "palette".to_owned(),
|
|
922
|
-
default: "linear-gradient(to right, #000000)".to_owned(),
|
|
923
|
-
max: None,
|
|
924
|
-
},
|
|
925
|
-
ControlSpec::GradientStops {
|
|
926
|
-
key: "gradient".to_owned(),
|
|
927
|
-
default: "linear-gradient(to right, #000000 0%, #ffffff 100%)".to_owned(),
|
|
928
|
-
discrete: false,
|
|
929
|
-
},
|
|
930
|
-
ControlSpec::Bool {
|
|
931
|
-
key: "flag".to_owned(),
|
|
932
|
-
default: false,
|
|
933
|
-
},
|
|
934
|
-
],
|
|
935
|
-
};
|
|
936
|
-
|
|
937
|
-
let mut map = json!({
|
|
938
|
-
"color": "RGB(255,0,0)",
|
|
939
|
-
"palette": "var(--psp-user--palette-warm)",
|
|
940
|
-
"gradient": [{ "color": "#000000", "offset": 0 }],
|
|
941
|
-
"flag": true,
|
|
942
|
-
"foreign": "linear-gradient(red, blue)",
|
|
943
|
-
})
|
|
944
|
-
.as_object()
|
|
945
|
-
.unwrap()
|
|
946
|
-
.clone();
|
|
947
|
-
|
|
948
|
-
let errors = normalize_values(&schema, &mut map);
|
|
949
|
-
assert_eq!(errors.len(), 1);
|
|
950
|
-
assert_eq!(errors[0].0, "gradient");
|
|
951
|
-
assert_eq!(
|
|
952
|
-
map,
|
|
953
|
-
json!({
|
|
954
|
-
"color": "#ff0000",
|
|
955
|
-
"palette": "var(--psp-user--palette-warm)",
|
|
956
|
-
"flag": true,
|
|
957
|
-
"foreign": "linear-gradient(red, blue)",
|
|
958
|
-
})
|
|
959
|
-
.as_object()
|
|
960
|
-
.unwrap()
|
|
961
|
-
.clone()
|
|
962
|
-
);
|
|
963
|
-
}
|
|
964
|
-
}
|