@perspective-dev/viewer 5.0.0 → 5.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cdn/perspective-viewer.js +9 -9
- package/dist/cdn/perspective-viewer.js.map +4 -4
- package/dist/css/botanical.css +1 -1
- package/dist/css/dracula.css +1 -1
- package/dist/css/gruvbox-dark.css +1 -1
- package/dist/css/gruvbox.css +1 -1
- package/dist/css/icons.css +1 -1
- package/dist/css/intl/de.css +1 -1
- package/dist/css/intl/es.css +1 -1
- package/dist/css/intl/fr.css +1 -1
- package/dist/css/intl/ja.css +1 -1
- package/dist/css/intl/pt.css +1 -1
- package/dist/css/intl/zh.css +1 -1
- package/dist/css/intl.css +1 -1
- package/dist/css/monokai.css +1 -1
- package/dist/css/phosphor.css +1 -1
- package/dist/css/pro-dark.css +1 -1
- package/dist/css/pro.css +1 -1
- package/dist/css/solarized-dark.css +1 -1
- package/dist/css/solarized.css +1 -1
- package/dist/css/themes.css +1 -1
- package/dist/css/vaporwave.css +1 -1
- package/dist/docs/perspective-docs.json +1 -0
- package/dist/esm/column-format.d.ts +19 -28
- package/dist/esm/perspective-viewer.d.ts +2 -0
- package/dist/esm/perspective-viewer.inline.js +8 -8
- package/dist/esm/perspective-viewer.inline.js.map +4 -4
- package/dist/esm/perspective-viewer.js +9 -9
- package/dist/esm/perspective-viewer.js.map +4 -4
- package/dist/esm/providers.d.ts +79 -0
- package/dist/esm/ts-rs/CompactDisplay.d.ts +5 -0
- package/dist/esm/ts-rs/CurrencyCode.d.ts +1 -0
- package/dist/esm/ts-rs/CurrencyDisplay.d.ts +1 -0
- package/dist/esm/ts-rs/CurrencyNumberFormatStyle.d.ts +8 -0
- package/dist/esm/ts-rs/CurrencySign.d.ts +1 -0
- package/dist/esm/ts-rs/CustomDatetimeFormat.d.ts +1 -0
- package/dist/esm/ts-rs/CustomDatetimeStyleConfig.d.ts +54 -0
- package/dist/esm/ts-rs/CustomNumberFormatConfig.d.ts +34 -0
- package/dist/esm/ts-rs/DatetimeColorMode.d.ts +1 -0
- package/dist/esm/ts-rs/DatetimeFormatType.d.ts +6 -0
- package/dist/esm/ts-rs/FormatMode.d.ts +1 -0
- package/dist/esm/ts-rs/FormatUnit.d.ts +1 -0
- package/dist/esm/ts-rs/Notation.d.ts +17 -0
- package/dist/esm/ts-rs/NumberFormatStyle.d.ts +18 -0
- package/dist/esm/ts-rs/PanelViewerConfig.d.ts +43 -1
- package/dist/esm/ts-rs/PluginStaticConfig.d.ts +35 -0
- package/dist/esm/ts-rs/RestoreOptions.d.ts +19 -0
- package/dist/esm/ts-rs/RoundingMode.d.ts +1 -0
- package/dist/esm/ts-rs/RoundingPriority.d.ts +1 -0
- package/dist/esm/ts-rs/SignDisplay.d.ts +1 -0
- package/dist/esm/ts-rs/SimpleDatetimeFormat.d.ts +1 -0
- package/dist/esm/ts-rs/SimpleDatetimeStyleConfig.d.ts +24 -0
- package/dist/esm/ts-rs/SplitRollupMode.d.ts +9 -0
- package/dist/esm/ts-rs/StringColorMode.d.ts +1 -0
- package/dist/esm/ts-rs/TrailingZeroDisplay.d.ts +1 -0
- package/dist/esm/ts-rs/Unit.d.ts +1 -0
- package/dist/esm/ts-rs/UnitDisplay.d.ts +1 -0
- package/dist/esm/ts-rs/UnitNumberFormatStyle.d.ts +6 -0
- package/dist/esm/ts-rs/UseGrouping.d.ts +1 -0
- package/dist/esm/ts-rs/ViewerConfig.d.ts +43 -1
- package/dist/esm/ts-rs/ViewerConfigInitial.d.ts +175 -0
- package/dist/esm/ts-rs/ViewerConfigUpdate.d.ts +72 -0
- package/dist/esm/ts-rs/WindowSort.d.ts +7 -0
- package/dist/esm/ts-rs/WindowSortDir.d.ts +6 -0
- package/dist/esm/ts-rs/WindowSpec.d.ts +47 -0
- package/dist/esm/ts-rs/Windows.d.ts +10 -0
- package/dist/esm/ts-rs/WorkspaceConfigUpdate.d.ts +7 -6
- package/dist/wasm/perspective-viewer.d.ts +222 -26
- package/dist/wasm/perspective-viewer.js +378 -125
- package/dist/wasm/perspective-viewer.wasm +0 -0
- package/dist/wasm/perspective-viewer.wasm.d.ts +18 -15
- package/package.json +2 -1
- package/src/css/_viewer_bundle.css +1 -0
- package/src/css/column-selector.css +9 -0
- package/src/css/column-settings-panel.css +67 -2
- package/src/css/column-style.css +4 -3
- package/src/css/config-selector.css +60 -4
- package/src/css/containers/context-menu.css +12 -6
- package/src/css/containers/tabs.css +2 -2
- package/src/css/form/code-editor.css +2 -3
- package/src/css/form/mirrored-textarea.css +81 -0
- package/src/css/viewer.css +336 -0
- package/src/rust/agent/client/mod.rs +19 -0
- package/src/rust/agent/client/protocol.rs +320 -0
- package/src/rust/agent/client/transport.rs +287 -0
- package/src/rust/agent/client/turn.rs +174 -0
- package/src/rust/agent/config.rs +159 -0
- package/src/rust/agent/docs.rs +293 -0
- package/src/rust/agent/markdown/block.rs +509 -0
- package/src/rust/agent/markdown/entity.rs +52 -0
- package/src/rust/agent/markdown/events.rs +71 -0
- package/src/rust/agent/markdown/inline.rs +497 -0
- package/src/rust/agent/markdown/mod.rs +466 -0
- package/src/rust/agent/mod.rs +23 -0
- package/src/rust/agent/preamble.md +16 -0
- package/src/rust/agent/runtime.rs +201 -0
- package/src/rust/agent/slot.rs +236 -0
- package/src/rust/agent/tools.rs +1110 -0
- package/src/rust/components/chat_panel.rs +289 -0
- package/src/rust/components/column_selector/active_column.rs +21 -27
- package/src/rust/components/column_selector/column_selector_column_row.rs +73 -0
- package/src/rust/components/column_selector/config_selector.rs +83 -8
- package/src/rust/components/column_selector/expr_edit_button.rs +7 -1
- package/src/rust/components/column_selector/inactive_column.rs +8 -1
- package/src/rust/components/column_selector.rs +27 -3
- package/src/rust/components/column_settings_sidebar/style_tab.rs +2 -1
- package/src/rust/components/column_settings_sidebar/window_tab.rs +34 -0
- package/src/rust/components/column_settings_sidebar.rs +163 -16
- package/src/rust/components/containers/dragdrop_list.rs +110 -3
- package/src/rust/components/containers/sidebar.rs +99 -22
- package/src/rust/components/containers/split_panel.rs +6 -0
- package/src/rust/components/editable_header.rs +2 -0
- package/src/rust/components/form/code_editor.rs +14 -23
- package/src/rust/components/form/debug.rs +8 -5
- package/src/rust/components/form/mirrored_textarea.rs +165 -0
- package/src/rust/components/form/mod.rs +1 -0
- package/src/rust/components/main_panel.rs +5 -0
- package/src/rust/components/mod.rs +3 -0
- package/src/rust/components/panel_menu.rs +7 -0
- package/src/rust/components/panel_tab.rs +5 -0
- package/src/rust/components/portal.rs +7 -0
- package/src/rust/components/settings_panel.rs +80 -2
- package/src/rust/components/viewer/msg.rs +2 -0
- package/src/rust/components/viewer/panels.rs +9 -14
- package/src/rust/components/viewer/render.rs +14 -0
- package/src/rust/components/viewer/settings.rs +33 -1
- package/src/rust/components/viewer.rs +2 -0
- package/src/rust/components/window_editor.rs +985 -0
- package/src/rust/config/datetime_column_style/custom.rs +17 -0
- package/src/rust/config/datetime_column_style/simple.rs +10 -0
- package/src/rust/config/number_string_format.rs +33 -0
- package/src/rust/config/options.rs +18 -0
- package/src/rust/config/plugin_static_config.rs +120 -5
- package/src/rust/config/viewer_config.rs +220 -1
- package/src/rust/config/workspace_config.rs +7 -6
- package/src/rust/custom_elements/viewer.rs +209 -18
- package/src/rust/custom_events.rs +29 -0
- package/src/rust/lib.rs +17 -1
- package/src/rust/presentation/column_locator.rs +16 -1
- package/src/rust/presentation.rs +8 -0
- package/src/rust/queries/columns_iter_set.rs +7 -1
- package/src/rust/queries/get_viewer_config.rs +5 -1
- package/src/rust/renderer/dispatch.rs +85 -58
- package/src/rust/renderer.rs +34 -1
- package/src/rust/session/column_defaults_update.rs +18 -0
- package/src/rust/session/drag_drop_update.rs +18 -0
- package/src/rust/session/metadata.rs +64 -9
- package/src/rust/session/replace_expression_update.rs +2 -0
- package/src/rust/session.rs +7 -5
- package/src/rust/tasks/create_panel.rs +18 -8
- package/src/rust/tasks/edit_window.rs +155 -0
- package/src/rust/tasks/mod.rs +2 -0
- package/src/rust/tasks/restore_panel.rs +34 -17
- package/src/rust/tasks/sync_update_panels.rs +7 -23
- package/src/rust/utils/browser/dragdrop.rs +12 -0
- package/src/rust/utils/browser/tests/debounce.rs +49 -0
- package/src/rust/utils/debounce.rs +76 -27
- package/src/rust/workspace.rs +112 -20
- package/src/svg/pin-icon.svg +5 -0
- package/src/themes/icons.css +1 -0
- package/src/themes/intl/de.css +23 -0
- package/src/themes/intl/es.css +23 -0
- package/src/themes/intl/fr.css +23 -0
- package/src/themes/intl/ja.css +23 -0
- package/src/themes/intl/pt.css +23 -0
- package/src/themes/intl/zh.css +23 -0
- package/src/themes/intl.css +26 -1
- package/src/ts/column-format.ts +67 -46
- package/src/ts/perspective-viewer.ts +2 -0
- package/src/ts/providers.ts +96 -0
- package/src/ts/ts-rs/CompactDisplay.ts +3 -0
- package/src/ts/ts-rs/CurrencyCode.ts +3 -0
- package/src/ts/ts-rs/CurrencyDisplay.ts +3 -0
- package/src/ts/ts-rs/CurrencyNumberFormatStyle.ts +6 -0
- package/src/ts/ts-rs/CurrencySign.ts +3 -0
- package/src/ts/ts-rs/CustomDatetimeFormat.ts +3 -0
- package/src/ts/ts-rs/CustomDatetimeStyleConfig.ts +54 -0
- package/src/ts/ts-rs/CustomNumberFormatConfig.ts +25 -0
- package/src/ts/ts-rs/DatetimeColorMode.ts +3 -0
- package/src/ts/ts-rs/DatetimeFormatType.ts +8 -0
- package/src/ts/ts-rs/FormatMode.ts +3 -0
- package/src/ts/ts-rs/FormatUnit.ts +3 -0
- package/src/ts/ts-rs/Notation.ts +11 -0
- package/src/ts/ts-rs/NumberFormatStyle.ts +12 -0
- package/src/ts/ts-rs/PanelViewerConfig.ts +48 -1
- package/src/ts/ts-rs/PluginStaticConfig.ts +36 -1
- package/src/ts/ts-rs/RestoreOptions.ts +20 -0
- package/src/ts/ts-rs/RoundingMode.ts +3 -0
- package/src/ts/ts-rs/RoundingPriority.ts +3 -0
- package/src/ts/ts-rs/SignDisplay.ts +3 -0
- package/src/ts/ts-rs/SimpleDatetimeFormat.ts +3 -0
- package/src/ts/ts-rs/SimpleDatetimeStyleConfig.ts +25 -0
- package/src/ts/ts-rs/SplitRollupMode.ts +11 -0
- package/src/ts/ts-rs/StringColorMode.ts +3 -0
- package/src/ts/ts-rs/TrailingZeroDisplay.ts +3 -0
- package/src/ts/ts-rs/Unit.ts +3 -0
- package/src/ts/ts-rs/UnitDisplay.ts +3 -0
- package/src/ts/ts-rs/UnitNumberFormatStyle.ts +5 -0
- package/src/ts/ts-rs/UseGrouping.ts +3 -0
- package/src/ts/ts-rs/ViewerConfig.ts +48 -1
- package/src/ts/ts-rs/ViewerConfigInitial.ts +164 -0
- package/src/ts/ts-rs/ViewerConfigUpdate.ts +81 -2
- package/src/ts/ts-rs/WindowSort.ts +9 -0
- package/src/ts/ts-rs/WindowSortDir.ts +8 -0
- package/src/ts/ts-rs/WindowSpec.ts +47 -0
- package/src/ts/ts-rs/Windows.ts +10 -0
- package/src/ts/ts-rs/WorkspaceConfigUpdate.ts +7 -6
- /package/dist/wasm/snippets/{perspective-viewer-daeef4ceb066e6c7 → perspective-viewer-02e461f3d7062b26}/inline0.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-daeef4ceb066e6c7 → perspective-viewer-02e461f3d7062b26}/inline1.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-daeef4ceb066e6c7 → perspective-viewer-02e461f3d7062b26}/inline2.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-daeef4ceb066e6c7 → perspective-viewer-02e461f3d7062b26}/inline3.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-daeef4ceb066e6c7 → perspective-viewer-02e461f3d7062b26}/inline4.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-daeef4ceb066e6c7 → perspective-viewer-02e461f3d7062b26}/inline5.js +0 -0
|
@@ -62,14 +62,22 @@ const fn numeric_default() -> u32 {
|
|
|
62
62
|
0
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
/// A datetime column's `date_format` in its `Custom` form: per-part
|
|
66
|
+
/// `Intl.DateTimeFormatOptions` overrides, discriminated from the `Simple`
|
|
67
|
+
/// preset form by the required `format: "custom"` key. Each part is a
|
|
68
|
+
/// [`CustomDatetimeFormat`] variant valid for that `Intl` option;
|
|
69
|
+
/// `"disabled"` omits the part from the formatted output entirely.
|
|
65
70
|
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, TS)]
|
|
66
71
|
pub struct CustomDatetimeStyleConfig {
|
|
67
72
|
format: FormatUnit,
|
|
68
73
|
|
|
74
|
+
/// An IANA time zone name (e.g. `"America/New_York"`); defaults to the
|
|
75
|
+
/// browser's local time zone.
|
|
69
76
|
#[serde(default)]
|
|
70
77
|
#[serde(rename = "timeZone", skip_serializing_if = "Option::is_none")]
|
|
71
78
|
pub time_zone: Option<String>,
|
|
72
79
|
|
|
80
|
+
/// Sub-second digits to display, 0-3 (0 = none).
|
|
73
81
|
#[serde(
|
|
74
82
|
skip_serializing_if = "is_zero",
|
|
75
83
|
rename = "fractionalSecondDigits",
|
|
@@ -77,27 +85,36 @@ pub struct CustomDatetimeStyleConfig {
|
|
|
77
85
|
)]
|
|
78
86
|
pub fractional_seconds: u32,
|
|
79
87
|
|
|
88
|
+
/// Defaults to `"numeric"`.
|
|
80
89
|
#[serde(skip_serializing_if = "is_numeric", default = "second_default")]
|
|
81
90
|
pub second: CustomDatetimeFormat,
|
|
82
91
|
|
|
92
|
+
/// Defaults to `"numeric"`.
|
|
83
93
|
#[serde(skip_serializing_if = "is_numeric", default = "second_default")]
|
|
84
94
|
pub minute: CustomDatetimeFormat,
|
|
85
95
|
|
|
96
|
+
/// Defaults to `"numeric"`.
|
|
86
97
|
#[serde(skip_serializing_if = "is_numeric", default = "second_default")]
|
|
87
98
|
pub hour: CustomDatetimeFormat,
|
|
88
99
|
|
|
100
|
+
/// Defaults to `"numeric"`.
|
|
89
101
|
#[serde(skip_serializing_if = "is_numeric", default = "second_default")]
|
|
90
102
|
pub day: CustomDatetimeFormat,
|
|
91
103
|
|
|
104
|
+
/// A name form (`"long"`/`"short"`/`"narrow"`); defaults to
|
|
105
|
+
/// `"disabled"` (weekday not shown).
|
|
92
106
|
#[serde(skip_serializing_if = "is_disabled", default = "weekday_default")]
|
|
93
107
|
pub weekday: CustomDatetimeFormat,
|
|
94
108
|
|
|
109
|
+
/// Numeric or name form; defaults to `"numeric"`.
|
|
95
110
|
#[serde(skip_serializing_if = "is_numeric", default = "second_default")]
|
|
96
111
|
pub month: CustomDatetimeFormat,
|
|
97
112
|
|
|
113
|
+
/// Defaults to `"2-digit"`.
|
|
98
114
|
#[serde(skip_serializing_if = "is_two_digit", default = "year_default")]
|
|
99
115
|
pub year: CustomDatetimeFormat,
|
|
100
116
|
|
|
117
|
+
/// 12-hour clock; defaults to `true`.
|
|
101
118
|
#[serde(skip_serializing_if = "is_true", default = "hour12_default")]
|
|
102
119
|
pub hour12: bool,
|
|
103
120
|
}
|
|
@@ -23,12 +23,20 @@ const fn time_style_default() -> SimpleDatetimeFormat {
|
|
|
23
23
|
SimpleDatetimeFormat::Medium
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
/// A datetime column's `date_format` in its `Simple` preset form:
|
|
27
|
+
/// `Intl.DateTimeFormatOptions`' `dateStyle`/`timeStyle` presets. This is
|
|
28
|
+
/// the default form (no `format` key); setting `format: "custom"` selects
|
|
29
|
+
/// the per-part `CustomDatetimeStyleConfig` instead.
|
|
26
30
|
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, TS)]
|
|
27
31
|
pub struct SimpleDatetimeStyleConfig {
|
|
32
|
+
/// An IANA time zone name (e.g. `"America/New_York"`); defaults to the
|
|
33
|
+
/// browser's local time zone.
|
|
28
34
|
#[serde(default)]
|
|
29
35
|
#[serde(rename = "timeZone", skip_serializing_if = "Option::is_none")]
|
|
30
36
|
pub time_zone: Option<String>,
|
|
31
37
|
|
|
38
|
+
/// Date preset breadth, or `"disabled"` to omit the date entirely;
|
|
39
|
+
/// defaults to `"short"`.
|
|
32
40
|
#[serde(
|
|
33
41
|
default = "date_style_default",
|
|
34
42
|
rename = "dateStyle",
|
|
@@ -36,6 +44,8 @@ pub struct SimpleDatetimeStyleConfig {
|
|
|
36
44
|
)]
|
|
37
45
|
pub date_style: SimpleDatetimeFormat,
|
|
38
46
|
|
|
47
|
+
/// Time preset breadth, or `"disabled"` to omit the time entirely;
|
|
48
|
+
/// defaults to `"medium"`.
|
|
39
49
|
#[serde(
|
|
40
50
|
default = "time_style_default",
|
|
41
51
|
rename = "timeStyle",
|
|
@@ -16,6 +16,11 @@ use serde::{Deserialize, Serialize};
|
|
|
16
16
|
use strum::{Display, EnumIter};
|
|
17
17
|
use ts_rs::TS;
|
|
18
18
|
|
|
19
|
+
/// The `style` family of a numeric column's `number_format` — serialized
|
|
20
|
+
/// FLATTENED into [`CustomNumberFormatConfig`]'s object, discriminated by
|
|
21
|
+
/// the `style` key (`"decimal"` default, `"currency"` + `currency`/
|
|
22
|
+
/// `currencyDisplay`/`currencySign`, `"percent"`, `"unit"` + `unit`/
|
|
23
|
+
/// `unitDisplay`), mirroring `Intl.NumberFormat` options.
|
|
19
24
|
#[derive(Serialize, Deserialize, Default, Debug, PartialEq, Clone, TS)]
|
|
20
25
|
#[serde(rename_all = "camelCase", tag = "style")]
|
|
21
26
|
pub enum NumberFormatStyle {
|
|
@@ -127,6 +132,11 @@ pub enum TrailingZeroDisplay {
|
|
|
127
132
|
StripIfInteger,
|
|
128
133
|
}
|
|
129
134
|
|
|
135
|
+
/// The `notation` family of a numeric column's `number_format` —
|
|
136
|
+
/// serialized FLATTENED into [`CustomNumberFormatConfig`]'s object,
|
|
137
|
+
/// discriminated by the `notation` key (`"standard"` default,
|
|
138
|
+
/// `"scientific"`, `"engineering"`, `"compact"` + `compactDisplay`),
|
|
139
|
+
/// mirroring `Intl.NumberFormat` options.
|
|
130
140
|
#[derive(Serialize, Deserialize, Default, Debug, PartialEq, Clone, TS)]
|
|
131
141
|
#[serde(rename_all = "camelCase", tag = "notation")]
|
|
132
142
|
pub enum Notation {
|
|
@@ -169,6 +179,14 @@ pub enum SignDisplay {
|
|
|
169
179
|
Never,
|
|
170
180
|
}
|
|
171
181
|
|
|
182
|
+
/// A numeric column's `number_format` (`columns_config` value) —
|
|
183
|
+
/// `Intl.NumberFormat`-shaped options, written by the Style tab's number
|
|
184
|
+
/// format editor and read by `createNumberFormatter`. The `style` and
|
|
185
|
+
/// `notation` families ([`NumberFormatStyle`] / [`Notation`]) are serde-
|
|
186
|
+
/// FLATTENED into this object but `#[ts(skip)]`'d (ts-rs cannot flatten
|
|
187
|
+
/// `Option<enum>`) — the package's `NumberFormatConfig` re-composes the
|
|
188
|
+
/// full wire type as `CustomNumberFormatConfig & Partial<NumberFormatStyle>
|
|
189
|
+
/// & Partial<Notation>` (see `column-format.ts`).
|
|
172
190
|
#[derive(Serialize, Deserialize, Debug, Default, PartialEq, Clone, TS)]
|
|
173
191
|
#[serde(rename_all = "camelCase")]
|
|
174
192
|
pub struct CustomNumberFormatConfig {
|
|
@@ -181,33 +199,42 @@ pub struct CustomNumberFormatConfig {
|
|
|
181
199
|
// these min/max props can all be specified but it results in possible conflicts
|
|
182
200
|
// may consider making them distinct options
|
|
183
201
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
202
|
+
#[ts(optional, as = "Option<_>")]
|
|
184
203
|
pub minimum_integer_digits: Option<f64>,
|
|
185
204
|
|
|
186
205
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
206
|
+
#[ts(optional, as = "Option<_>")]
|
|
187
207
|
pub minimum_fraction_digits: Option<f64>,
|
|
188
208
|
|
|
189
209
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
210
|
+
#[ts(optional, as = "Option<_>")]
|
|
190
211
|
pub maximum_fraction_digits: Option<f64>,
|
|
191
212
|
|
|
192
213
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
214
|
+
#[ts(optional, as = "Option<_>")]
|
|
193
215
|
pub minimum_significant_digits: Option<f64>,
|
|
194
216
|
|
|
195
217
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
218
|
+
#[ts(optional, as = "Option<_>")]
|
|
196
219
|
pub maximum_significant_digits: Option<f64>,
|
|
197
220
|
|
|
198
221
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
222
|
+
#[ts(optional, as = "Option<_>")]
|
|
199
223
|
pub rounding_priority: Option<RoundingPriority>,
|
|
200
224
|
|
|
201
225
|
// specific values https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#roundingincrement
|
|
202
226
|
// Only available with automatic rounding priority
|
|
203
227
|
// Cannot be mixed with sigfig rounding. (Does this mean max/min sigfig must be unset?)
|
|
204
228
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
229
|
+
#[ts(optional, as = "Option<_>")]
|
|
205
230
|
pub rounding_increment: Option<f64>,
|
|
206
231
|
|
|
207
232
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
233
|
+
#[ts(optional, as = "Option<_>")]
|
|
208
234
|
pub rounding_mode: Option<RoundingMode>,
|
|
209
235
|
|
|
210
236
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
237
|
+
#[ts(optional, as = "Option<_>")]
|
|
211
238
|
pub trailing_zero_display: Option<TrailingZeroDisplay>,
|
|
212
239
|
|
|
213
240
|
#[serde(flatten)]
|
|
@@ -215,10 +242,16 @@ pub struct CustomNumberFormatConfig {
|
|
|
215
242
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
216
243
|
pub _notation: Option<Notation>,
|
|
217
244
|
|
|
245
|
+
/// NOTE (audit 2026-08-05): serialized values are the STRINGS
|
|
246
|
+
/// `"always"`/`"auto"`/`"min2"` or the untagged BOOLEAN `false` —
|
|
247
|
+
/// the former hand-written TS `useGrouping?: boolean` was wrong for
|
|
248
|
+
/// the string cases.
|
|
218
249
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
250
|
+
#[ts(optional, as = "Option<_>")]
|
|
219
251
|
pub use_grouping: Option<UseGrouping>,
|
|
220
252
|
|
|
221
253
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
254
|
+
#[ts(optional, as = "Option<_>")]
|
|
222
255
|
pub sign_display: Option<SignDisplay>,
|
|
223
256
|
}
|
|
224
257
|
|
|
@@ -28,6 +28,24 @@ pub struct PanelOptions {
|
|
|
28
28
|
pub panel: Option<String>,
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
/// Options for the `restore()` method.
|
|
32
|
+
#[derive(Deserialize, Default, TS)]
|
|
33
|
+
pub struct RestoreOptions {
|
|
34
|
+
/// The target panel; the active panel when omitted.
|
|
35
|
+
#[ts(optional)]
|
|
36
|
+
pub panel: Option<String>,
|
|
37
|
+
|
|
38
|
+
/// When `true`, a failed restore only REJECTS the returned `Promise` —
|
|
39
|
+
/// the error is not committed to the viewer's visible error state, and
|
|
40
|
+
/// the session remains usable for subsequent calls. For programmatic
|
|
41
|
+
/// callers (e.g. the LLM agent's `set_view_config` tool) for whom a
|
|
42
|
+
/// failed config patch is feedback rather than a user-facing fault.
|
|
43
|
+
/// The config may be partially applied on failure; restore a
|
|
44
|
+
/// known-good config to recover exactly.
|
|
45
|
+
#[ts(optional)]
|
|
46
|
+
pub suppress_errors: Option<bool>,
|
|
47
|
+
}
|
|
48
|
+
|
|
31
49
|
/// The `eject` argument: the loaded client to remove by name; the active
|
|
32
50
|
/// panel's client when omitted.
|
|
33
51
|
#[derive(Deserialize, Default, TS)]
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
|
11
11
|
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
|
12
12
|
|
|
13
|
-
use perspective_client::config::GroupRollupMode;
|
|
13
|
+
use perspective_client::config::{GroupRollupMode, SplitRollupMode};
|
|
14
14
|
use serde::Deserialize;
|
|
15
15
|
use ts_rs::TS;
|
|
16
16
|
|
|
@@ -103,6 +103,13 @@ pub struct PluginStaticConfig {
|
|
|
103
103
|
#[ts(optional)]
|
|
104
104
|
pub group_rollup_modes: Option<Vec<GroupRollupMode>>,
|
|
105
105
|
|
|
106
|
+
/// Split-rollup modes the plugin accepts, in preference order.
|
|
107
|
+
/// The first entry that matches a feature flag becomes the default.
|
|
108
|
+
#[serde(default)]
|
|
109
|
+
#[ts(as = "Option<_>")]
|
|
110
|
+
#[ts(optional)]
|
|
111
|
+
pub split_rollup_modes: Option<Vec<SplitRollupMode>>,
|
|
112
|
+
|
|
106
113
|
/// Plugin load priority. Higher numbers win; ties resolve in
|
|
107
114
|
/// registration order. The highest-priority plugin is loaded by
|
|
108
115
|
/// default unless `restore({ plugin })` overrides it.
|
|
@@ -120,15 +127,70 @@ pub struct PluginStaticConfig {
|
|
|
120
127
|
#[ts(as = "Option<_>")]
|
|
121
128
|
#[ts(optional)]
|
|
122
129
|
pub can_render_column_styles: bool,
|
|
130
|
+
|
|
131
|
+
/// What `group_by` MEANS visually for this plugin, e.g. `"X Axis"`
|
|
132
|
+
/// for the Y-series charts or `"Hierarchy"` for treemap/sunburst.
|
|
133
|
+
/// `None` where the field has no visual role of its own and is a
|
|
134
|
+
/// plain aggregation key (the X/Y charts, whose axes both come from
|
|
135
|
+
/// `columns`).
|
|
136
|
+
///
|
|
137
|
+
/// This is the `group_by` counterpart of [`Self::config_column_names`]:
|
|
138
|
+
/// the same declaration that names the positional `columns` slots
|
|
139
|
+
/// should say what the other view fields draw, so that consumers —
|
|
140
|
+
/// the settings UI's field labels, and the agent's `list_plugins`
|
|
141
|
+
/// contract — read one source instead of restating the mapping.
|
|
142
|
+
#[serde(default)]
|
|
143
|
+
#[ts(as = "Option<_>")]
|
|
144
|
+
#[ts(optional)]
|
|
145
|
+
pub group_by_role: Option<String>,
|
|
146
|
+
|
|
147
|
+
/// What `split_by` MEANS visually for this plugin, e.g. `"Series"`.
|
|
148
|
+
/// See [`Self::group_by_role`].
|
|
149
|
+
#[serde(default)]
|
|
150
|
+
#[ts(as = "Option<_>")]
|
|
151
|
+
#[ts(optional)]
|
|
152
|
+
pub split_by_role: Option<String>,
|
|
153
|
+
|
|
154
|
+
/// `true` when this plugin CONNECTS its points in row order, so the
|
|
155
|
+
/// `View`'s row order is visible in the drawing: an unsorted config
|
|
156
|
+
/// renders the table's natural order, which reads as a tangle unless
|
|
157
|
+
/// the rows already arrive ordered along the axis. Declared rather
|
|
158
|
+
/// than inferred, because "unsorted" is not itself a mistake — a
|
|
159
|
+
/// pre-ordered table needs no `sort`, and the point plugins
|
|
160
|
+
/// (scatter, density) do not care at all.
|
|
161
|
+
#[serde(default)]
|
|
162
|
+
#[ts(as = "Option<_>")]
|
|
163
|
+
#[ts(optional)]
|
|
164
|
+
pub connects_row_order: bool,
|
|
123
165
|
}
|
|
124
166
|
|
|
125
167
|
impl PluginStaticConfig {
|
|
168
|
+
/// The number of leading `columns` slots that are POSITIONAL: a drop
|
|
169
|
+
/// there swaps with the column already present, and the slot's
|
|
170
|
+
/// meaning is fixed by [`Self::config_column_names`]. Everything from
|
|
171
|
+
/// this index on is the insert TAIL, which repeats the last named
|
|
172
|
+
/// role — which is why a `Y Line` (named slots: `["Y Axis"]`) takes
|
|
173
|
+
/// any number of columns as additional Y series, while an
|
|
174
|
+
/// `X/Y Line` (`["X Axis", "Y Axis", "Tooltip"]`) pins its two axes
|
|
175
|
+
/// and treats the rest as further tooltips.
|
|
176
|
+
///
|
|
177
|
+
/// The single definition of that rule: [`Self::is_swap`] and the
|
|
178
|
+
/// agent's `list_plugins` contract both derive from it, so the
|
|
179
|
+
/// convention cannot be read two different ways.
|
|
180
|
+
pub fn positional_columns(&self) -> usize {
|
|
181
|
+
self.config_column_names.len().saturating_sub(1)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/// The role that `columns` past the positional slots repeat, i.e.
|
|
185
|
+
/// the last named slot. `None` when the plugin names no slots.
|
|
186
|
+
pub fn tail_column_role(&self) -> Option<&str> {
|
|
187
|
+
self.config_column_names.last().map(|x| x.as_str())
|
|
188
|
+
}
|
|
189
|
+
|
|
126
190
|
/// `true` if dropping a column at `index` should swap with the
|
|
127
|
-
/// column already there rather than insert.
|
|
128
|
-
/// (`config_column_names[..len()-1]`) participate in swap behaviour;
|
|
129
|
-
/// the trailing unnamed tail inserts.
|
|
191
|
+
/// column already there rather than insert.
|
|
130
192
|
pub fn is_swap(&self, index: usize) -> bool {
|
|
131
|
-
!self.config_column_names.is_empty() && index < self.
|
|
193
|
+
!self.config_column_names.is_empty() && index < self.positional_columns()
|
|
132
194
|
}
|
|
133
195
|
|
|
134
196
|
pub fn get_group_rollups(&self, rollup_features: &[GroupRollupMode]) -> Vec<GroupRollupMode> {
|
|
@@ -141,4 +203,57 @@ impl PluginStaticConfig {
|
|
|
141
203
|
})
|
|
142
204
|
.unwrap_or_default()
|
|
143
205
|
}
|
|
206
|
+
|
|
207
|
+
pub fn get_split_rollups(&self, rollup_features: &[SplitRollupMode]) -> Vec<SplitRollupMode> {
|
|
208
|
+
self.split_rollup_modes
|
|
209
|
+
.clone()
|
|
210
|
+
.map(|x| {
|
|
211
|
+
x.into_iter()
|
|
212
|
+
.filter(|y| rollup_features.is_empty() || rollup_features.contains(y))
|
|
213
|
+
.collect()
|
|
214
|
+
})
|
|
215
|
+
.unwrap_or_default()
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
#[cfg(test)]
|
|
220
|
+
mod tests {
|
|
221
|
+
use super::*;
|
|
222
|
+
|
|
223
|
+
fn plugin(names: &[&str]) -> PluginStaticConfig {
|
|
224
|
+
PluginStaticConfig {
|
|
225
|
+
config_column_names: names.iter().map(|x| (*x).to_owned()).collect(),
|
|
226
|
+
..Default::default()
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/// The tail rule has ONE definition: `is_swap` and the role a
|
|
231
|
+
/// trailing column repeats must never disagree about where the
|
|
232
|
+
/// positional slots end.
|
|
233
|
+
#[test]
|
|
234
|
+
fn positional_slots_and_tail_agree() {
|
|
235
|
+
// `Y Line` — the only named slot IS the tail, so every column
|
|
236
|
+
// is another Y series and none of them swap.
|
|
237
|
+
let y_line = plugin(&["Y Axis"]);
|
|
238
|
+
assert_eq!(y_line.positional_columns(), 0);
|
|
239
|
+
assert_eq!(y_line.tail_column_role(), Some("Y Axis"));
|
|
240
|
+
assert!(!y_line.is_swap(0));
|
|
241
|
+
|
|
242
|
+
// `X/Y Line` — two pinned axes, then tooltips.
|
|
243
|
+
let xy_line = plugin(&["X Axis", "Y Axis", "Tooltip"]);
|
|
244
|
+
assert_eq!(xy_line.positional_columns(), 2);
|
|
245
|
+
assert_eq!(xy_line.tail_column_role(), Some("Tooltip"));
|
|
246
|
+
assert!(xy_line.is_swap(0));
|
|
247
|
+
assert!(xy_line.is_swap(1));
|
|
248
|
+
assert!(!xy_line.is_swap(2));
|
|
249
|
+
|
|
250
|
+
for index in 0..4 {
|
|
251
|
+
assert_eq!(xy_line.is_swap(index), index < xy_line.positional_columns());
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
let unnamed = plugin(&[]);
|
|
255
|
+
assert_eq!(unnamed.positional_columns(), 0);
|
|
256
|
+
assert_eq!(unnamed.tail_column_role(), None);
|
|
257
|
+
assert!(!unnamed.is_swap(0));
|
|
258
|
+
}
|
|
144
259
|
}
|
|
@@ -42,12 +42,42 @@ pub struct ViewerConfig<V: TS = String> {
|
|
|
42
42
|
/// whole-element config format.
|
|
43
43
|
#[derive(Debug, Default, Serialize, PartialEq, TS)]
|
|
44
44
|
pub struct PanelViewerConfig<V: TS = String> {
|
|
45
|
+
/// The `@perspective-dev/viewer` version that wrote this config,
|
|
46
|
+
/// stamped on save and used to migrate older tokens. Callers do not
|
|
47
|
+
/// set it.
|
|
45
48
|
pub version: V,
|
|
49
|
+
|
|
50
|
+
/// Per-column styling, keyed by column name. The viewer treats the
|
|
51
|
+
/// values as opaque — their shape is defined by the ACTIVE plugin,
|
|
52
|
+
/// so a config written for one plugin may carry keys another
|
|
53
|
+
/// ignores. Query the live shape with the `get_style_schema` agent
|
|
54
|
+
/// tool, or the plugin's `column_config_schema()`.
|
|
46
55
|
pub columns_config: ColumnConfigMap,
|
|
56
|
+
|
|
57
|
+
/// Name of the visualization plugin, from the set registered on the
|
|
58
|
+
/// page (the `list_plugins` agent tool, or the plugin picker). This
|
|
59
|
+
/// also decides what the view fields MEAN visually: `columns` is
|
|
60
|
+
/// positional and each plugin reads the positions differently, and
|
|
61
|
+
/// `group_by`/`split_by` draw different things per plugin.
|
|
47
62
|
pub plugin: String,
|
|
63
|
+
|
|
64
|
+
/// Plugin-wide settings (as opposed to the per-column
|
|
65
|
+
/// [`Self::columns_config`]). Opaque to the viewer and defined by
|
|
66
|
+
/// the active plugin; see `get_style_schema`.
|
|
48
67
|
pub plugin_config: serde_json::Map<String, Value>,
|
|
49
|
-
|
|
68
|
+
|
|
69
|
+
/// Name of the `Table` this panel renders, as hosted on the panel's
|
|
70
|
+
/// `Client`. Every placed panel has a table binding (creation
|
|
71
|
+
/// requires one by type — [`ViewerConfigInitial`]), so the saved
|
|
72
|
+
/// config carries it unconditionally.
|
|
73
|
+
pub table: String,
|
|
74
|
+
|
|
75
|
+
/// Selected theme NAME (e.g. `"Pro Dark"`) — not a CSS value. Valid
|
|
76
|
+
/// names are the Perspective themes loaded on the page, which
|
|
77
|
+
/// `resetThemes()` re-scans. `None` selects the first available.
|
|
50
78
|
pub theme: Option<String>,
|
|
79
|
+
|
|
80
|
+
/// Panel title, shown in its tab. `None` renders the default title.
|
|
51
81
|
pub title: Option<String>,
|
|
52
82
|
|
|
53
83
|
#[serde(flatten)]
|
|
@@ -93,41 +123,73 @@ impl Deref for PluginConfig {
|
|
|
93
123
|
|
|
94
124
|
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, TS)]
|
|
95
125
|
pub struct ViewerConfigUpdate {
|
|
126
|
+
/// The `@perspective-dev/viewer` version a saved config was written
|
|
127
|
+
/// by, used to migrate older tokens. Omit it — the viewer stamps
|
|
128
|
+
/// the current version on save.
|
|
96
129
|
#[serde(default)]
|
|
97
130
|
#[ts(as = "Option<_>")]
|
|
98
131
|
#[ts(optional)]
|
|
99
132
|
pub version: VersionUpdate,
|
|
100
133
|
|
|
134
|
+
/// Name of the visualization plugin to switch to, from the set
|
|
135
|
+
/// registered on the page (the `list_plugins` agent tool, or the
|
|
136
|
+
/// plugin picker). Changing it changes what the view fields MEAN:
|
|
137
|
+
/// `columns` is positional and every plugin reads the positions
|
|
138
|
+
/// differently, and `group_by`/`split_by` draw different things per
|
|
139
|
+
/// plugin — so read the new plugin's roles before writing `columns`
|
|
140
|
+
/// for it.
|
|
101
141
|
#[serde(default)]
|
|
102
142
|
#[ts(as = "Option<_>")]
|
|
103
143
|
#[ts(optional)]
|
|
104
144
|
pub plugin: PluginUpdate,
|
|
105
145
|
|
|
146
|
+
/// Panel title, shown in its tab. `null` restores the default
|
|
147
|
+
/// title; omitting the field leaves the current one.
|
|
106
148
|
#[serde(default)]
|
|
107
149
|
#[ts(as = "Option<_>")]
|
|
108
150
|
#[ts(optional)]
|
|
109
151
|
pub title: TitleUpdate,
|
|
110
152
|
|
|
153
|
+
/// Name of the `Table` to render, as hosted on this panel's
|
|
154
|
+
/// `Client`. Rebinding an existing panel to another table keeps the
|
|
155
|
+
/// rest of the config, so column names that do not exist in the new
|
|
156
|
+
/// table will fail validation.
|
|
111
157
|
#[serde(default)]
|
|
112
158
|
#[ts(as = "Option<_>")]
|
|
113
159
|
#[ts(optional)]
|
|
114
160
|
pub table: TableUpdate,
|
|
115
161
|
|
|
162
|
+
/// Theme NAME (e.g. `"Pro Dark"`) — not a CSS value. Valid names are
|
|
163
|
+
/// the Perspective themes loaded on the page, re-scanned by
|
|
164
|
+
/// `resetThemes()`. `null` selects the default.
|
|
116
165
|
#[serde(default)]
|
|
117
166
|
#[ts(as = "Option<_>")]
|
|
118
167
|
#[ts(optional)]
|
|
119
168
|
pub theme: ThemeUpdate,
|
|
120
169
|
|
|
170
|
+
/// Whether the settings sidebar is OPEN. Purely cosmetic chrome —
|
|
171
|
+
/// it does not affect what the viewer renders, and it is
|
|
172
|
+
/// element-level rather than per-panel.
|
|
121
173
|
#[serde(default)]
|
|
122
174
|
#[ts(as = "Option<_>")]
|
|
123
175
|
#[ts(optional)]
|
|
124
176
|
pub settings: SettingsUpdate,
|
|
125
177
|
|
|
178
|
+
/// Plugin-wide settings (as opposed to the per-column
|
|
179
|
+
/// [`Self::columns_config`]). The viewer passes these through
|
|
180
|
+
/// opaquely — their valid keys are defined by the ACTIVE plugin and
|
|
181
|
+
/// vary by plugin and by state, so query them with the
|
|
182
|
+
/// `get_style_schema` agent tool rather than guessing.
|
|
126
183
|
#[serde(default)]
|
|
127
184
|
#[ts(as = "Option<_>")]
|
|
128
185
|
#[ts(optional)]
|
|
129
186
|
pub plugin_config: PluginConfigUpdate,
|
|
130
187
|
|
|
188
|
+
/// Per-column styling — formatting, colors, and other per-column
|
|
189
|
+
/// controls — keyed by column name. Opaque to the viewer and
|
|
190
|
+
/// plugin-defined like [`Self::plugin_config`]; `get_style_schema`
|
|
191
|
+
/// reports the valid keys for a given column under the active
|
|
192
|
+
/// plugin.
|
|
131
193
|
#[serde(default)]
|
|
132
194
|
#[ts(as = "Option<_>")]
|
|
133
195
|
#[ts(optional)]
|
|
@@ -150,6 +212,119 @@ impl ViewerConfigUpdate {
|
|
|
150
212
|
}
|
|
151
213
|
}
|
|
152
214
|
|
|
215
|
+
/// The initial configuration of a NEW panel (`addPanel`, `restore`'s
|
|
216
|
+
/// panel-creating upsert, `restoreWorkspace` `panels` entries). Unlike
|
|
217
|
+
/// [`ViewerConfigUpdate`] — a patch against existing state — creation has
|
|
218
|
+
/// no prior state: `table` is REQUIRED (a placed panel without a table
|
|
219
|
+
/// binding would be permanently blank), absent fields mean "default"
|
|
220
|
+
/// rather than "leave unchanged" (so no [`OptionalUpdate`] tri-state), and
|
|
221
|
+
/// there is no `settings` field (element-level, not per-panel).
|
|
222
|
+
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, TS)]
|
|
223
|
+
pub struct ViewerConfigInitial {
|
|
224
|
+
/// Name of the `Table` the new panel renders, as hosted on the
|
|
225
|
+
/// `Client`. REQUIRED: a placed panel with no table binding would be
|
|
226
|
+
/// permanently blank.
|
|
227
|
+
pub table: String,
|
|
228
|
+
|
|
229
|
+
/// The `@perspective-dev/viewer` version a saved config was written
|
|
230
|
+
/// by. Omit it when creating a panel.
|
|
231
|
+
#[ts(optional)]
|
|
232
|
+
pub version: Option<String>,
|
|
233
|
+
|
|
234
|
+
/// Name of the visualization plugin, from the set registered on the
|
|
235
|
+
/// page (the `list_plugins` agent tool, or the plugin picker).
|
|
236
|
+
/// Decides what the view fields MEAN: `columns` is positional and
|
|
237
|
+
/// every plugin reads the positions differently, and
|
|
238
|
+
/// `group_by`/`split_by` draw different things per plugin. Absent
|
|
239
|
+
/// uses the default plugin.
|
|
240
|
+
#[ts(optional)]
|
|
241
|
+
pub plugin: Option<String>,
|
|
242
|
+
|
|
243
|
+
/// Panel title, shown in its tab. Absent renders the default title.
|
|
244
|
+
#[ts(optional)]
|
|
245
|
+
pub title: Option<String>,
|
|
246
|
+
|
|
247
|
+
/// Theme NAME (e.g. `"Pro Dark"`) — not a CSS value. Valid names are
|
|
248
|
+
/// the Perspective themes loaded on the page. Absent uses the
|
|
249
|
+
/// default.
|
|
250
|
+
#[ts(optional)]
|
|
251
|
+
pub theme: Option<String>,
|
|
252
|
+
|
|
253
|
+
/// Plugin-wide settings (as opposed to the per-column
|
|
254
|
+
/// [`Self::columns_config`]). Opaque to the viewer and defined by
|
|
255
|
+
/// the ACTIVE plugin; query the valid keys with `get_style_schema`
|
|
256
|
+
/// rather than guessing.
|
|
257
|
+
#[ts(optional)]
|
|
258
|
+
pub plugin_config: Option<serde_json::Map<String, Value>>,
|
|
259
|
+
|
|
260
|
+
/// Per-column styling — formatting, colors, and other per-column
|
|
261
|
+
/// controls — keyed by column name. Plugin-defined like
|
|
262
|
+
/// [`Self::plugin_config`]; see `get_style_schema`.
|
|
263
|
+
#[ts(optional)]
|
|
264
|
+
pub columns_config: Option<ColumnConfigMap>,
|
|
265
|
+
|
|
266
|
+
#[serde(flatten)]
|
|
267
|
+
pub view_config: ViewConfigUpdate,
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
impl ViewerConfigInitial {
|
|
271
|
+
/// Decode a `JsValue` by auto-detecting format from JavaScript type.
|
|
272
|
+
pub fn decode(config: &JsValue) -> ApiResult<Self> {
|
|
273
|
+
Ok(config.into_serde_ext()?)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/// A default-config panel bound to `table` (the "New panel" menus).
|
|
277
|
+
/// Exhaustive on purpose — same drift alarm as the `From` impl below.
|
|
278
|
+
pub fn new(table: impl Into<String>) -> Self {
|
|
279
|
+
Self {
|
|
280
|
+
table: table.into(),
|
|
281
|
+
version: None,
|
|
282
|
+
plugin: None,
|
|
283
|
+
title: None,
|
|
284
|
+
theme: None,
|
|
285
|
+
plugin_config: None,
|
|
286
|
+
columns_config: None,
|
|
287
|
+
view_config: ViewConfigUpdate::default(),
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
fn up<T: Clone>(value: Option<T>) -> OptionalUpdate<T> {
|
|
293
|
+
match value {
|
|
294
|
+
Some(value) => OptionalUpdate::Update(value),
|
|
295
|
+
None => OptionalUpdate::Missing,
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Constructed EXHAUSTIVELY on purpose: a field added to
|
|
300
|
+
// `ViewerConfigUpdate` breaks this impl at compile time, forcing the
|
|
301
|
+
// "should creation carry it?" decision (the drift alarm).
|
|
302
|
+
impl From<ViewerConfigInitial> for ViewerConfigUpdate {
|
|
303
|
+
fn from(value: ViewerConfigInitial) -> Self {
|
|
304
|
+
ViewerConfigUpdate {
|
|
305
|
+
version: up(value.version),
|
|
306
|
+
plugin: up(value.plugin),
|
|
307
|
+
title: up(value.title),
|
|
308
|
+
table: OptionalUpdate::Update(value.table),
|
|
309
|
+
theme: up(value.theme),
|
|
310
|
+
settings: OptionalUpdate::Missing,
|
|
311
|
+
plugin_config: up(value.plugin_config),
|
|
312
|
+
columns_config: up(value.columns_config),
|
|
313
|
+
view_config: value.view_config,
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// There is deliberately NO `TryFrom<ViewerConfigUpdate>` here. Requiring a
|
|
319
|
+
// `table` is a property of the CREATION ENTRY POINTS — `addPanel`'s
|
|
320
|
+
// argument type, `WorkspaceConfigUpdate::panels`, and the agent's
|
|
321
|
+
// `add_panel` decode — each of which already has a `ViewerConfigInitial`
|
|
322
|
+
// in hand. An update→initial conversion exists only to let a route holding
|
|
323
|
+
// a PATCH pretend it is creating from scratch, which is how `restore`'s
|
|
324
|
+
// upsert acquired the gate and started rejecting the table-less
|
|
325
|
+
// restore-then-`load` contract. Its absence is what keeps that from
|
|
326
|
+
// recurring: `create_panel` takes an update, so no caller needs one.
|
|
327
|
+
|
|
153
328
|
impl std::fmt::Display for ViewerConfigUpdate {
|
|
154
329
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
155
330
|
write!(
|
|
@@ -185,6 +360,50 @@ pub type VersionUpdate = OptionalUpdate<String>;
|
|
|
185
360
|
pub type ColumnConfigUpdate = OptionalUpdate<ColumnConfigMap>;
|
|
186
361
|
pub type PluginConfigUpdate = OptionalUpdate<serde_json::Map<String, Value>>;
|
|
187
362
|
|
|
363
|
+
#[cfg(test)]
|
|
364
|
+
mod tests {
|
|
365
|
+
use super::*;
|
|
366
|
+
|
|
367
|
+
#[test]
|
|
368
|
+
fn initial_requires_a_table() {
|
|
369
|
+
let json = serde_json::json!({ "group_by": ["State"] });
|
|
370
|
+
let err = serde_json::from_value::<ViewerConfigInitial>(json).unwrap_err();
|
|
371
|
+
assert!(format!("{err}").contains("table"));
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/// The creation → patch direction is the only one that exists (see the
|
|
375
|
+
/// note above the `From` impl); a `table` becomes a concrete update and
|
|
376
|
+
/// the element-level `settings` is not carried.
|
|
377
|
+
#[test]
|
|
378
|
+
fn initial_widens_to_an_update() {
|
|
379
|
+
let json = serde_json::json!({
|
|
380
|
+
"table": "superstore",
|
|
381
|
+
"plugin": "Datagrid",
|
|
382
|
+
"group_by": ["State"],
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
let initial: ViewerConfigInitial = serde_json::from_value(json).unwrap();
|
|
386
|
+
let update = ViewerConfigUpdate::from(initial);
|
|
387
|
+
assert!(matches!(&update.table, OptionalUpdate::Update(x) if x == "superstore"));
|
|
388
|
+
assert!(matches!(&update.settings, OptionalUpdate::Missing));
|
|
389
|
+
assert!(matches!(&update.plugin, OptionalUpdate::Update(x) if x == "Datagrid"));
|
|
390
|
+
assert_eq!(
|
|
391
|
+
update.view_config.group_by.as_deref(),
|
|
392
|
+
Some(&["State".to_owned()][..])
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/// A table-less patch is a legitimate creation input now that
|
|
397
|
+
/// `create_panel` takes an update — the deferred panel a `load()`
|
|
398
|
+
/// binds. Nothing in this module may reject it.
|
|
399
|
+
#[test]
|
|
400
|
+
fn a_table_less_update_is_representable() {
|
|
401
|
+
let json = serde_json::json!({ "group_by": ["State"] });
|
|
402
|
+
let update: ViewerConfigUpdate = serde_json::from_value(json).unwrap();
|
|
403
|
+
assert!(matches!(&update.table, OptionalUpdate::Missing));
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
188
407
|
/// Handles `{}` when included as a field with `#[serde(default)]`.
|
|
189
408
|
impl<T: Clone> Default for OptionalUpdate<T> {
|
|
190
409
|
fn default() -> Self {
|