@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
|
@@ -166,7 +166,7 @@ impl Component for PerspectiveViewer {
|
|
|
166
166
|
let renderer_props = active_renderer.to_props(None);
|
|
167
167
|
let presentation_props = ctx.props().presentation.to_props(PtrEqRc::new(vec![]));
|
|
168
168
|
let on_close_column_settings = ctx.link().callback(|_| OpenColumnSettings {
|
|
169
|
-
|
|
169
|
+
target: None,
|
|
170
170
|
sender: None,
|
|
171
171
|
toggle: false,
|
|
172
172
|
});
|
|
@@ -319,8 +319,8 @@ impl Component for PerspectiveViewer {
|
|
|
319
319
|
ClearGlobalFilters => self.on_clear_global_filters(ctx),
|
|
320
320
|
|
|
321
321
|
// Settings sidebar + divider pump + column settings (`settings.rs`)
|
|
322
|
-
ToggleSettingsInit(update, resolve) => {
|
|
323
|
-
self.on_toggle_settings_init(ctx, update, resolve)
|
|
322
|
+
ToggleSettingsInit(update, announce, resolve) => {
|
|
323
|
+
self.on_toggle_settings_init(ctx, update, announce, resolve)
|
|
324
324
|
},
|
|
325
325
|
ToggleSettingsComplete(update, resolve) => {
|
|
326
326
|
self.on_toggle_settings_complete(ctx, update, resolve)
|
|
@@ -333,25 +333,29 @@ impl Component for PerspectiveViewer {
|
|
|
333
333
|
SettingsPanelTabChanged(tab) => self.on_settings_panel_tab_changed(tab),
|
|
334
334
|
SettingsPanelAutoWidth(w) => self.on_settings_panel_auto_width(w),
|
|
335
335
|
OpenColumnSettings {
|
|
336
|
-
|
|
336
|
+
target,
|
|
337
337
|
sender,
|
|
338
338
|
toggle,
|
|
339
|
-
} => self.on_open_column_settings(ctx,
|
|
339
|
+
} => self.on_open_column_settings(ctx, target, sender, toggle),
|
|
340
340
|
ColumnSettingsPanelSizeUpdate(x) => self.on_column_settings_panel_size_update(x),
|
|
341
341
|
ColumnSettingsPanelAutoWidth(w) => self.on_column_settings_panel_auto_width(w),
|
|
342
|
-
ToggleColumnSettingsPin => self.on_toggle_column_settings_pin(),
|
|
342
|
+
ToggleColumnSettingsPin => self.on_toggle_column_settings_pin(ctx),
|
|
343
|
+
ToggleColumnSettingsPinComplete(resolve) => {
|
|
344
|
+
self.on_toggle_column_settings_pin_complete(resolve)
|
|
345
|
+
},
|
|
343
346
|
ColumnSettingsTabChanged(tab) => self.on_column_settings_tab_changed(ctx, tab),
|
|
344
347
|
ToggleDebug => self.on_toggle_debug(ctx),
|
|
345
348
|
|
|
346
349
|
// Value-semantic snapshot plumbing (`snapshots.rs`)
|
|
347
|
-
UpdateSession(props) => self.on_update_session(*props),
|
|
350
|
+
UpdateSession(props) => self.on_update_session(ctx, *props),
|
|
348
351
|
UpdateSessionStats(stats, has_table) => self.on_update_session_stats(stats, has_table),
|
|
349
352
|
UpdateGlobalFilters => self.on_update_global_filters(ctx),
|
|
350
353
|
UpdateRenderer(props) => self.on_update_renderer(*props),
|
|
351
354
|
UpdatePresentation(props) => self.on_update_presentation(ctx, *props),
|
|
352
355
|
UpdateSettingsOpen(open) => self.on_update_settings_open(open),
|
|
353
356
|
UpdateIsWorkspace(is_workspace) => self.on_update_is_workspace(is_workspace),
|
|
354
|
-
UpdateColumnSettings(ocs) => self.on_update_column_settings(*ocs),
|
|
357
|
+
UpdateColumnSettings(ocs) => self.on_update_column_settings(ctx, *ocs),
|
|
358
|
+
UpdateColumnSettingsCommit(resolve) => self.on_update_column_settings_commit(resolve),
|
|
355
359
|
UpdateDragDrop(props) => self.on_update_dragdrop(*props),
|
|
356
360
|
UpdateInFlight(count) => self.on_update_in_flight(count),
|
|
357
361
|
}
|
|
@@ -15,18 +15,15 @@ use std::collections::HashSet;
|
|
|
15
15
|
use serde::{Deserialize, Serialize};
|
|
16
16
|
use serde_json::Value;
|
|
17
17
|
|
|
18
|
-
use super::{
|
|
18
|
+
use super::{
|
|
19
|
+
CssKind, CustomNumberFormatConfig, DatetimeFormatType, KeyValueOpts,
|
|
20
|
+
NumberSeriesStyleDefaultConfig,
|
|
21
|
+
};
|
|
19
22
|
|
|
20
23
|
/// The full schema for one column at one point in time. Plugins may return
|
|
21
24
|
/// different schemas for the same column based on the column's current
|
|
22
25
|
/// stored value (e.g. to hide dependent fields), so this is re-queried on
|
|
23
26
|
/// every field update.
|
|
24
|
-
///
|
|
25
|
-
/// Each entry is a [`ControlSpec`]. Primitive variants carry their own
|
|
26
|
-
/// `key` (JSON storage key) inline; the sidebar UI label is supplied by
|
|
27
|
-
/// CSS via `--psp-label--<key>--content`. Composite variants render a
|
|
28
|
-
/// self-contained Yew component that supplies its own labels and owns a
|
|
29
|
-
/// fixed key namespace via [`ControlSpec::serialized_keys`].
|
|
30
27
|
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
|
31
28
|
pub struct ColumnConfigSchema {
|
|
32
29
|
pub fields: Vec<ControlSpec>,
|
|
@@ -47,6 +44,21 @@ impl ColumnConfigSchema {
|
|
|
47
44
|
}
|
|
48
45
|
out
|
|
49
46
|
}
|
|
47
|
+
|
|
48
|
+
pub fn leaf_fields(&self) -> Vec<&ControlSpec> {
|
|
49
|
+
fn collect<'a>(fields: &'a [ControlSpec], out: &mut Vec<&'a ControlSpec>) {
|
|
50
|
+
for spec in fields {
|
|
51
|
+
match spec {
|
|
52
|
+
ControlSpec::Group { fields, .. } => collect(fields, out),
|
|
53
|
+
leaf => out.push(leaf),
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let mut out = vec![];
|
|
59
|
+
collect(&self.fields, &mut out);
|
|
60
|
+
out
|
|
61
|
+
}
|
|
50
62
|
}
|
|
51
63
|
|
|
52
64
|
/// Discriminated union of widget kinds the viewer can render. Composite
|
|
@@ -93,33 +105,26 @@ pub enum ControlSpec {
|
|
|
93
105
|
key: String,
|
|
94
106
|
default: String,
|
|
95
107
|
},
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
ColorRange {
|
|
103
|
-
key_pos: String,
|
|
104
|
-
key_neg: String,
|
|
105
|
-
default_pos: String,
|
|
106
|
-
default_neg: String,
|
|
107
|
-
/// When `true`, the bar renders as a continuous gradient
|
|
108
|
-
/// (e.g. for `gradient` color modes); when `false`, the bar
|
|
109
|
-
/// renders as a hard pos/neg split. Currently only changes the
|
|
110
|
-
/// visual; pos/neg semantics are unchanged.
|
|
111
|
-
#[serde(default)]
|
|
112
|
-
is_gradient: bool,
|
|
108
|
+
Palette {
|
|
109
|
+
key: String,
|
|
110
|
+
default: String,
|
|
111
|
+
|
|
112
|
+
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
113
|
+
max: Option<usize>,
|
|
113
114
|
},
|
|
115
|
+
GradientStops {
|
|
116
|
+
key: String,
|
|
117
|
+
default: String,
|
|
114
118
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
119
|
+
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
|
120
|
+
discrete: bool,
|
|
121
|
+
},
|
|
122
|
+
DatetimeFormat {
|
|
123
|
+
/// Plugin-declared default `date_format`, shown by the editor in
|
|
124
|
+
/// unedited fields and elided from serialized configs.
|
|
125
|
+
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
126
|
+
default: Option<DatetimeFormatType>,
|
|
127
|
+
},
|
|
123
128
|
StringFormat,
|
|
124
129
|
NumberSeriesStyle {
|
|
125
130
|
default: NumberSeriesStyleDefaultConfig,
|
|
@@ -127,8 +132,19 @@ pub enum ControlSpec {
|
|
|
127
132
|
Symbols {
|
|
128
133
|
default: KeyValueOpts,
|
|
129
134
|
},
|
|
130
|
-
NumberFormat
|
|
135
|
+
NumberFormat {
|
|
136
|
+
/// Plugin-declared default format, keyed like `number_format`
|
|
137
|
+
/// itself.
|
|
138
|
+
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
139
|
+
default: Option<CustomNumberFormatConfig>,
|
|
140
|
+
},
|
|
131
141
|
AggregateDepth,
|
|
142
|
+
|
|
143
|
+
Group {
|
|
144
|
+
key: String,
|
|
145
|
+
#[serde(default)]
|
|
146
|
+
fields: Vec<ControlSpec>,
|
|
147
|
+
},
|
|
132
148
|
}
|
|
133
149
|
|
|
134
150
|
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
|
@@ -138,6 +154,143 @@ pub struct EnumVariant {
|
|
|
138
154
|
pub label: Option<String>,
|
|
139
155
|
}
|
|
140
156
|
|
|
157
|
+
/// One stop of a [`ControlSpec::GradientStops`] value in its in-memory
|
|
158
|
+
/// form: a `#rrggbb` color at `offset` ∈ `[0, 1]`.
|
|
159
|
+
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
|
160
|
+
pub struct GradientStopSpec {
|
|
161
|
+
pub color: String,
|
|
162
|
+
pub offset: f64,
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/// The [`ControlSpec::GradientStops`] canonical stop order: offsets
|
|
166
|
+
/// clamped to `[0, 1]` and rounded to 3 decimals, stops sorted stably
|
|
167
|
+
/// by offset.
|
|
168
|
+
pub fn canonicalize_gradient_stops(mut stops: Vec<GradientStopSpec>) -> Vec<GradientStopSpec> {
|
|
169
|
+
for stop in &mut stops {
|
|
170
|
+
stop.offset = (stop.offset.clamp(0.0, 1.0) * 1000.0).round() / 1000.0;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
stops.sort_by(|a, b| {
|
|
174
|
+
a.offset
|
|
175
|
+
.partial_cmp(&b.offset)
|
|
176
|
+
.unwrap_or(std::cmp::Ordering::Equal)
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
stops
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/// Fit `stops` to a `discrete` field's fixed pair: an over-length value
|
|
183
|
+
/// keeps only its two end colors, pinned to `0`/`1`.
|
|
184
|
+
pub fn discrete_pair(stops: Vec<GradientStopSpec>) -> Vec<GradientStopSpec> {
|
|
185
|
+
let stops = canonicalize_gradient_stops(stops);
|
|
186
|
+
match (stops.first(), stops.last()) {
|
|
187
|
+
(Some(first), Some(last)) if stops.len() > 2 => vec![
|
|
188
|
+
GradientStopSpec {
|
|
189
|
+
color: first.color.clone(),
|
|
190
|
+
offset: 0.0,
|
|
191
|
+
},
|
|
192
|
+
GradientStopSpec {
|
|
193
|
+
color: last.color.clone(),
|
|
194
|
+
offset: 1.0,
|
|
195
|
+
},
|
|
196
|
+
],
|
|
197
|
+
_ => stops,
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
impl ColumnConfigSchema {
|
|
202
|
+
pub fn canonicalize(self) -> Self {
|
|
203
|
+
self.canonicalize_defaults().group_format_controls()
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
pub fn group_format_controls(mut self) -> Self {
|
|
207
|
+
fn is_format(spec: &ControlSpec) -> bool {
|
|
208
|
+
matches!(
|
|
209
|
+
spec,
|
|
210
|
+
ControlSpec::NumberFormat { .. }
|
|
211
|
+
| ControlSpec::DatetimeFormat { .. }
|
|
212
|
+
| ControlSpec::StringFormat
|
|
213
|
+
)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
fn walk(fields: &mut Vec<ControlSpec>) {
|
|
217
|
+
for spec in fields.iter_mut() {
|
|
218
|
+
if let ControlSpec::Group { key, fields } = spec
|
|
219
|
+
&& key != "format"
|
|
220
|
+
{
|
|
221
|
+
walk(fields);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
let first = fields.iter().position(is_format);
|
|
226
|
+
if let Some(first) = first {
|
|
227
|
+
let mut formats = vec![];
|
|
228
|
+
let mut i = first;
|
|
229
|
+
while i < fields.len() {
|
|
230
|
+
if is_format(&fields[i]) {
|
|
231
|
+
formats.push(fields.remove(i));
|
|
232
|
+
} else {
|
|
233
|
+
i += 1;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
fields.insert(first, ControlSpec::Group {
|
|
238
|
+
key: "format".to_owned(),
|
|
239
|
+
fields: formats,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
walk(&mut self.fields);
|
|
245
|
+
self
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/// Canonicalize every CSS-valued default at schema ingest, dropping
|
|
249
|
+
/// (and logging) fields whose default fails its kind's reader.
|
|
250
|
+
pub fn canonicalize_defaults(mut self) -> Self {
|
|
251
|
+
fn canonicalize_specs(fields: &mut Vec<ControlSpec>) {
|
|
252
|
+
fields.retain_mut(|spec| {
|
|
253
|
+
let (kind, key, default) = match spec {
|
|
254
|
+
ControlSpec::Group { fields, .. } => {
|
|
255
|
+
canonicalize_specs(fields);
|
|
256
|
+
return !fields.is_empty();
|
|
257
|
+
},
|
|
258
|
+
ControlSpec::Color { key, default } => (CssKind::Color, key, default),
|
|
259
|
+
ControlSpec::Palette { key, default, .. } => (CssKind::Palette, key, default),
|
|
260
|
+
ControlSpec::GradientStops { key, default, .. } => {
|
|
261
|
+
(CssKind::Gradient, key, default)
|
|
262
|
+
},
|
|
263
|
+
_ => return true,
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
match kind.canonicalize(default) {
|
|
267
|
+
Ok(canonical) => {
|
|
268
|
+
*default = canonical;
|
|
269
|
+
true
|
|
270
|
+
},
|
|
271
|
+
Err(error) => {
|
|
272
|
+
tracing::error!("Dropping `{key}` — invalid schema default: {error}");
|
|
273
|
+
false
|
|
274
|
+
},
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
canonicalize_specs(&mut self.fields);
|
|
280
|
+
self
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/// The CSS kind of the control owning `key`, if it is CSS-valued.
|
|
284
|
+
pub fn css_kind_of(&self, key: &str) -> Option<CssKind> {
|
|
285
|
+
self.leaf_fields().into_iter().find_map(|spec| match spec {
|
|
286
|
+
ControlSpec::Color { key: k, .. } if k == key => Some(CssKind::Color),
|
|
287
|
+
ControlSpec::Palette { key: k, .. } if k == key => Some(CssKind::Palette),
|
|
288
|
+
ControlSpec::GradientStops { key: k, .. } if k == key => Some(CssKind::Gradient),
|
|
289
|
+
_ => None,
|
|
290
|
+
})
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
141
294
|
impl ControlSpec {
|
|
142
295
|
/// Top-level JSON keys this control owns when its value is serialized
|
|
143
296
|
/// into a column's config map. For primitives this is just `[key]`;
|
|
@@ -146,20 +299,22 @@ impl ControlSpec {
|
|
|
146
299
|
/// `columns_config` blob passed to `plugin.restore()`.
|
|
147
300
|
pub fn serialized_keys(&self) -> Vec<&str> {
|
|
148
301
|
match self {
|
|
149
|
-
ControlSpec::DatetimeFormat => vec!["date_format"],
|
|
302
|
+
ControlSpec::DatetimeFormat { .. } => vec!["date_format"],
|
|
150
303
|
ControlSpec::StringFormat => vec!["format"],
|
|
151
304
|
ControlSpec::NumberSeriesStyle { .. } => vec!["chart_type", "stack"],
|
|
152
305
|
ControlSpec::Symbols { .. } => vec!["symbols"],
|
|
153
|
-
ControlSpec::NumberFormat => vec!["number_format"],
|
|
306
|
+
ControlSpec::NumberFormat { .. } => vec!["number_format"],
|
|
154
307
|
ControlSpec::AggregateDepth => vec!["aggregate_depth"],
|
|
155
|
-
ControlSpec::ColorRange {
|
|
156
|
-
key_pos, key_neg, ..
|
|
157
|
-
} => vec![key_pos.as_str(), key_neg.as_str()],
|
|
158
308
|
ControlSpec::Enum { key, .. }
|
|
159
309
|
| ControlSpec::Bool { key, .. }
|
|
160
310
|
| ControlSpec::Number { key, .. }
|
|
161
311
|
| ControlSpec::String { key, .. }
|
|
162
|
-
| ControlSpec::Color { key, .. }
|
|
312
|
+
| ControlSpec::Color { key, .. }
|
|
313
|
+
| ControlSpec::Palette { key, .. }
|
|
314
|
+
| ControlSpec::GradientStops { key, .. } => vec![key.as_str()],
|
|
315
|
+
ControlSpec::Group { fields, .. } => {
|
|
316
|
+
fields.iter().flat_map(|f| f.serialized_keys()).collect()
|
|
317
|
+
},
|
|
163
318
|
}
|
|
164
319
|
}
|
|
165
320
|
}
|
|
@@ -168,12 +323,6 @@ impl ControlSpec {
|
|
|
168
323
|
/// declares which top-level keys the update is allowed to write
|
|
169
324
|
/// (`keys` — equivalent to the field's [`ControlSpec::serialized_keys`])
|
|
170
325
|
/// and a partial new sub-state (`value`).
|
|
171
|
-
///
|
|
172
|
-
/// Apply semantics: keys in `keys` are *cleared* from the column's config
|
|
173
|
-
/// map, then keys present in `value` are *inserted*. Defaults are
|
|
174
|
-
/// pre-stripped by the caller (typically via `skip_serializing_if`), so
|
|
175
|
-
/// "no value set for key K" means the schema default applies and K is
|
|
176
|
-
/// not serialized.
|
|
177
326
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
|
178
327
|
pub struct ColumnConfigFieldUpdate {
|
|
179
328
|
pub keys: Vec<String>,
|
|
@@ -193,3 +342,244 @@ pub fn filter_to_schema(
|
|
|
193
342
|
.map(|(k, v)| (k.clone(), v.clone()))
|
|
194
343
|
.collect()
|
|
195
344
|
}
|
|
345
|
+
|
|
346
|
+
#[cfg(test)]
|
|
347
|
+
mod tests {
|
|
348
|
+
use serde_json::json;
|
|
349
|
+
|
|
350
|
+
use super::*;
|
|
351
|
+
|
|
352
|
+
fn color(key: &str, default: &str) -> ControlSpec {
|
|
353
|
+
ControlSpec::Color {
|
|
354
|
+
key: key.to_owned(),
|
|
355
|
+
default: default.to_owned(),
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
fn flag(key: &str) -> ControlSpec {
|
|
360
|
+
ControlSpec::Bool {
|
|
361
|
+
key: key.to_owned(),
|
|
362
|
+
default: false,
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
fn group(key: &str, fields: Vec<ControlSpec>) -> ControlSpec {
|
|
367
|
+
ControlSpec::Group {
|
|
368
|
+
key: key.to_owned(),
|
|
369
|
+
fields,
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
#[test]
|
|
374
|
+
fn group_deserializes_recursively() {
|
|
375
|
+
let schema: ColumnConfigSchema = serde_json::from_value(json!({
|
|
376
|
+
"fields": [{
|
|
377
|
+
"kind": "Group",
|
|
378
|
+
"key": "legend",
|
|
379
|
+
"fields": [
|
|
380
|
+
{ "kind": "Bool", "key": "legend_on", "default": false },
|
|
381
|
+
{
|
|
382
|
+
"kind": "Group",
|
|
383
|
+
"key": "inner",
|
|
384
|
+
"fields": [{ "kind": "Color", "key": "color", "default": "#ff0000" }]
|
|
385
|
+
}
|
|
386
|
+
]
|
|
387
|
+
}]
|
|
388
|
+
}))
|
|
389
|
+
.unwrap();
|
|
390
|
+
|
|
391
|
+
let keys = schema.active_keys();
|
|
392
|
+
assert_eq!(
|
|
393
|
+
keys,
|
|
394
|
+
HashSet::from(["legend_on".to_owned(), "color".to_owned()])
|
|
395
|
+
);
|
|
396
|
+
|
|
397
|
+
let leaves = schema.leaf_fields();
|
|
398
|
+
assert_eq!(leaves.len(), 2);
|
|
399
|
+
assert!(
|
|
400
|
+
leaves
|
|
401
|
+
.iter()
|
|
402
|
+
.all(|s| !matches!(s, ControlSpec::Group { .. }))
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
#[test]
|
|
407
|
+
fn grouped_schema_is_equivalent_to_flat() {
|
|
408
|
+
let flat = ColumnConfigSchema {
|
|
409
|
+
fields: vec![flag("stack"), color("color", "#0366d6")],
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
let grouped = ColumnConfigSchema {
|
|
413
|
+
fields: vec![group("series", vec![
|
|
414
|
+
flag("stack"),
|
|
415
|
+
color("color", "#0366d6"),
|
|
416
|
+
])],
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
assert_eq!(flat.active_keys(), grouped.active_keys());
|
|
420
|
+
assert_eq!(flat.css_kind_of("color"), grouped.css_kind_of("color"));
|
|
421
|
+
assert_eq!(flat.css_kind_of("stack"), grouped.css_kind_of("stack"));
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
#[test]
|
|
425
|
+
fn format_controls_group_and_merge() {
|
|
426
|
+
let schema = ColumnConfigSchema {
|
|
427
|
+
fields: vec![
|
|
428
|
+
ControlSpec::NumberFormat { default: None },
|
|
429
|
+
flag("flag"),
|
|
430
|
+
ControlSpec::StringFormat,
|
|
431
|
+
],
|
|
432
|
+
}
|
|
433
|
+
.group_format_controls();
|
|
434
|
+
|
|
435
|
+
assert_eq!(schema.fields.len(), 2);
|
|
436
|
+
let ControlSpec::Group { key, fields } = &schema.fields[0] else {
|
|
437
|
+
panic!("expected format group first");
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
assert_eq!(key, "format");
|
|
441
|
+
assert!(matches!(fields[0], ControlSpec::NumberFormat { .. }));
|
|
442
|
+
assert!(matches!(fields[1], ControlSpec::StringFormat));
|
|
443
|
+
assert!(matches!(&schema.fields[1], ControlSpec::Bool { .. }));
|
|
444
|
+
|
|
445
|
+
assert_eq!(
|
|
446
|
+
schema.active_keys(),
|
|
447
|
+
HashSet::from([
|
|
448
|
+
"number_format".to_owned(),
|
|
449
|
+
"format".to_owned(),
|
|
450
|
+
"flag".to_owned()
|
|
451
|
+
])
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
#[test]
|
|
456
|
+
fn format_grouping_recurses_but_never_double_wraps() {
|
|
457
|
+
let schema = ColumnConfigSchema {
|
|
458
|
+
fields: vec![
|
|
459
|
+
group("format", vec![ControlSpec::NumberFormat { default: None }]),
|
|
460
|
+
group("styling", vec![flag("x"), ControlSpec::DatetimeFormat {
|
|
461
|
+
default: None,
|
|
462
|
+
}]),
|
|
463
|
+
],
|
|
464
|
+
}
|
|
465
|
+
.group_format_controls();
|
|
466
|
+
|
|
467
|
+
let ControlSpec::Group { key, fields } = &schema.fields[0] else {
|
|
468
|
+
panic!("expected group");
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
assert_eq!(key, "format");
|
|
472
|
+
assert!(matches!(fields[0], ControlSpec::NumberFormat { .. }));
|
|
473
|
+
|
|
474
|
+
let ControlSpec::Group { fields, .. } = &schema.fields[1] else {
|
|
475
|
+
panic!("expected group");
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
assert!(matches!(
|
|
479
|
+
&fields[1],
|
|
480
|
+
ControlSpec::Group { key, fields }
|
|
481
|
+
if key == "format" && matches!(fields[0], ControlSpec::DatetimeFormat { .. })
|
|
482
|
+
));
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
#[test]
|
|
486
|
+
fn format_controls_deserialize_without_default_payload() {
|
|
487
|
+
let schema: ColumnConfigSchema = serde_json::from_value(json!({
|
|
488
|
+
"fields": [{ "kind": "NumberFormat" }, { "kind": "DatetimeFormat" }]
|
|
489
|
+
}))
|
|
490
|
+
.unwrap();
|
|
491
|
+
|
|
492
|
+
assert!(matches!(&schema.fields[0], ControlSpec::NumberFormat {
|
|
493
|
+
default: None
|
|
494
|
+
}));
|
|
495
|
+
assert!(matches!(&schema.fields[1], ControlSpec::DatetimeFormat {
|
|
496
|
+
default: None
|
|
497
|
+
}));
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
#[test]
|
|
501
|
+
fn number_format_default_payload_deserializes_flattened_families() {
|
|
502
|
+
let schema: ColumnConfigSchema = serde_json::from_value(json!({
|
|
503
|
+
"fields": [{
|
|
504
|
+
"kind": "NumberFormat",
|
|
505
|
+
"default": {
|
|
506
|
+
"notation": "compact",
|
|
507
|
+
"compactDisplay": "short",
|
|
508
|
+
"minimumFractionDigits": 0,
|
|
509
|
+
"maximumFractionDigits": 1
|
|
510
|
+
}
|
|
511
|
+
}]
|
|
512
|
+
}))
|
|
513
|
+
.unwrap();
|
|
514
|
+
|
|
515
|
+
let ControlSpec::NumberFormat {
|
|
516
|
+
default: Some(default),
|
|
517
|
+
} = &schema.fields[0]
|
|
518
|
+
else {
|
|
519
|
+
panic!("expected NumberFormat with default");
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
assert_eq!(
|
|
523
|
+
default._notation,
|
|
524
|
+
Some(crate::config::Notation::Compact(
|
|
525
|
+
crate::config::CompactDisplay::Short
|
|
526
|
+
))
|
|
527
|
+
);
|
|
528
|
+
assert_eq!(default._style, None);
|
|
529
|
+
assert_eq!(default.minimum_fraction_digits, Some(0.));
|
|
530
|
+
assert_eq!(default.maximum_fraction_digits, Some(1.));
|
|
531
|
+
assert_eq!(
|
|
532
|
+
schema.active_keys(),
|
|
533
|
+
HashSet::from(["number_format".to_owned()])
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
#[test]
|
|
538
|
+
fn datetime_format_default_payload_deserializes_simple_arm() {
|
|
539
|
+
let schema: ColumnConfigSchema = serde_json::from_value(json!({
|
|
540
|
+
"fields": [{
|
|
541
|
+
"kind": "DatetimeFormat",
|
|
542
|
+
"default": { "dateStyle": "medium", "timeStyle": "disabled" }
|
|
543
|
+
}]
|
|
544
|
+
}))
|
|
545
|
+
.unwrap();
|
|
546
|
+
|
|
547
|
+
let ControlSpec::DatetimeFormat {
|
|
548
|
+
default: Some(DatetimeFormatType::Simple(simple)),
|
|
549
|
+
} = &schema.fields[0]
|
|
550
|
+
else {
|
|
551
|
+
panic!("expected DatetimeFormat with Simple default");
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
assert_eq!(
|
|
555
|
+
simple.date_style,
|
|
556
|
+
crate::config::SimpleDatetimeFormat::Medium
|
|
557
|
+
);
|
|
558
|
+
assert_eq!(
|
|
559
|
+
simple.time_style,
|
|
560
|
+
crate::config::SimpleDatetimeFormat::Disabled
|
|
561
|
+
);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
#[test]
|
|
565
|
+
fn canonicalize_defaults_recurses_and_drops_empty_groups() {
|
|
566
|
+
let schema = ColumnConfigSchema {
|
|
567
|
+
fields: vec![
|
|
568
|
+
group("ok", vec![color("good", "RGB(255,0,0)"), flag("flag")]),
|
|
569
|
+
group("doomed", vec![color("bad", "not-a-color")]),
|
|
570
|
+
],
|
|
571
|
+
}
|
|
572
|
+
.canonicalize_defaults();
|
|
573
|
+
|
|
574
|
+
assert_eq!(schema.fields.len(), 1);
|
|
575
|
+
let ControlSpec::Group { key, fields } = &schema.fields[0] else {
|
|
576
|
+
panic!("expected group");
|
|
577
|
+
};
|
|
578
|
+
|
|
579
|
+
assert_eq!(key, "ok");
|
|
580
|
+
assert!(matches!(
|
|
581
|
+
&fields[0],
|
|
582
|
+
ControlSpec::Color { default, .. } if default == "#ff0000"
|
|
583
|
+
));
|
|
584
|
+
}
|
|
585
|
+
}
|