@perspective-dev/viewer 5.3.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.
Files changed (100) hide show
  1. package/dist/cdn/perspective-viewer.js +8 -8
  2. package/dist/cdn/perspective-viewer.js.map +3 -3
  3. package/dist/css/blueprint.css +1 -0
  4. package/dist/css/botanical.css +1 -1
  5. package/dist/css/dracula.css +1 -1
  6. package/dist/css/eggplant.css +1 -0
  7. package/dist/css/gruvbox-dark.css +1 -1
  8. package/dist/css/gruvbox.css +1 -1
  9. package/dist/css/icons.css +1 -1
  10. package/dist/css/intl/de.css +1 -1
  11. package/dist/css/intl/es.css +1 -1
  12. package/dist/css/intl/fr.css +1 -1
  13. package/dist/css/intl/ja.css +1 -1
  14. package/dist/css/intl/pt.css +1 -1
  15. package/dist/css/intl/zh.css +1 -1
  16. package/dist/css/intl.css +1 -1
  17. package/dist/css/ledger.css +1 -0
  18. package/dist/css/monokai.css +1 -1
  19. package/dist/css/nord.css +1 -0
  20. package/dist/css/phosphor.css +1 -1
  21. package/dist/css/pro-dark.css +1 -1
  22. package/dist/css/pro.css +1 -1
  23. package/dist/css/solarized-dark.css +1 -1
  24. package/dist/css/solarized.css +1 -1
  25. package/dist/css/themes.css +1 -1
  26. package/dist/css/vaporwave.css +1 -1
  27. package/dist/css/velvet.css +1 -0
  28. package/dist/docs/perspective-docs.json +1 -1
  29. package/dist/esm/column-format.d.ts +16 -3
  30. package/dist/esm/perspective-viewer.inline.js +9 -9
  31. package/dist/esm/perspective-viewer.inline.js.map +3 -3
  32. package/dist/esm/perspective-viewer.js +8 -8
  33. package/dist/esm/perspective-viewer.js.map +3 -3
  34. package/dist/wasm/perspective-viewer.d.ts +16 -16
  35. package/dist/wasm/perspective-viewer.js +107 -103
  36. package/dist/wasm/perspective-viewer.wasm +0 -0
  37. package/dist/wasm/perspective-viewer.wasm.d.ts +16 -16
  38. package/package.json +1 -1
  39. package/src/css/column-settings-panel.css +19 -0
  40. package/src/css/column-style.css +82 -0
  41. package/src/css/containers/tabs.css +13 -3
  42. package/src/css/dom/select.css +19 -8
  43. package/src/css/plugin-selector.css +1 -0
  44. package/src/css/plugin-settings-panel.css +72 -0
  45. package/src/css/viewer.css +0 -5
  46. package/src/rust/agent/tools.rs +5 -2
  47. package/src/rust/components/column_settings_sidebar/style_tab.rs +296 -237
  48. package/src/rust/components/containers/control_group.rs +91 -0
  49. package/src/rust/components/containers/mod.rs +1 -0
  50. package/src/rust/components/containers/select.rs +8 -2
  51. package/src/rust/components/datetime_column_style/simple.rs +15 -4
  52. package/src/rust/components/datetime_column_style.rs +50 -10
  53. package/src/rust/components/plugin_tab.rs +125 -87
  54. package/src/rust/components/settings_panel.rs +1 -0
  55. package/src/rust/components/status_bar.rs +0 -1
  56. package/src/rust/components/style_controls/number_string_format/digits_section.rs +22 -10
  57. package/src/rust/components/style_controls/number_string_format/misc_section.rs +6 -2
  58. package/src/rust/components/style_controls/number_string_format/style_section.rs +1 -0
  59. package/src/rust/components/style_controls/number_string_format/types.rs +24 -0
  60. package/src/rust/components/style_controls/number_string_format.rs +37 -14
  61. package/src/rust/config/column_config_schema.rs +358 -27
  62. package/src/rust/config/datetime_column_style.rs +4 -21
  63. package/src/rust/config/number_string_format.rs +211 -18
  64. package/src/rust/presentation.rs +17 -0
  65. package/src/rust/queries/plugin_column_styles.rs +2 -2
  66. package/src/rust/renderer/dispatch.rs +34 -8
  67. package/src/rust/renderer/plugin_config.rs +55 -8
  68. package/src/themes/blueprint.css +190 -0
  69. package/src/themes/botanical.css +4 -17
  70. package/src/themes/defaults.css +2 -120
  71. package/src/themes/dracula.css +2 -14
  72. package/src/themes/eggplant.css +189 -0
  73. package/src/themes/gruvbox-dark.css +3 -15
  74. package/src/themes/gruvbox.css +1 -1
  75. package/src/themes/icons.css +1 -2
  76. package/src/themes/intl/de.css +23 -1
  77. package/src/themes/intl/es.css +23 -1
  78. package/src/themes/intl/fr.css +23 -1
  79. package/src/themes/intl/ja.css +23 -1
  80. package/src/themes/intl/pt.css +23 -1
  81. package/src/themes/intl/zh.css +23 -1
  82. package/src/themes/intl.css +26 -4
  83. package/src/themes/ledger.css +195 -0
  84. package/src/themes/monokai.css +2 -14
  85. package/src/themes/nord.css +186 -0
  86. package/src/themes/phosphor.css +2 -14
  87. package/src/themes/pro-dark.css +2 -16
  88. package/src/themes/pro.css +118 -0
  89. package/src/themes/solarized-dark.css +2 -16
  90. package/src/themes/themes.css +10 -5
  91. package/src/themes/vaporwave.css +2 -14
  92. package/src/themes/velvet.css +192 -0
  93. package/src/ts/column-format.ts +52 -2
  94. package/src/svg/dropdown-selector-light.svg +0 -3
  95. /package/dist/wasm/snippets/{perspective-viewer-39c3c3b28e3cbd48 → perspective-viewer-87bd21b215d0631a}/inline0.js +0 -0
  96. /package/dist/wasm/snippets/{perspective-viewer-39c3c3b28e3cbd48 → perspective-viewer-87bd21b215d0631a}/inline1.js +0 -0
  97. /package/dist/wasm/snippets/{perspective-viewer-39c3c3b28e3cbd48 → perspective-viewer-87bd21b215d0631a}/inline2.js +0 -0
  98. /package/dist/wasm/snippets/{perspective-viewer-39c3c3b28e3cbd48 → perspective-viewer-87bd21b215d0631a}/inline3.js +0 -0
  99. /package/dist/wasm/snippets/{perspective-viewer-39c3c3b28e3cbd48 → perspective-viewer-87bd21b215d0631a}/inline4.js +0 -0
  100. /package/dist/wasm/snippets/{perspective-viewer-39c3c3b28e3cbd48 → perspective-viewer-87bd21b215d0631a}/inline5.js +0 -0
@@ -15,6 +15,8 @@ mod misc_section;
15
15
  mod style_section;
16
16
  mod types;
17
17
 
18
+ use std::rc::Rc;
19
+
18
20
  use perspective_client::config::ColumnType;
19
21
  pub use types::*;
20
22
  use yew::{Callback, Component, Properties, html};
@@ -27,6 +29,10 @@ pub struct CustomNumberFormatProps {
27
29
  pub on_change: Callback<ColumnConfigFieldUpdate>,
28
30
  pub view_type: ColumnType,
29
31
  pub column_name: String,
32
+
33
+ /// Resolved default values for this column under the active plugin.
34
+ pub defaults: Rc<NumberFormatDefaults>,
35
+
30
36
  #[prop_or_default]
31
37
  pub keys: Vec<String>,
32
38
  }
@@ -147,25 +153,39 @@ impl Component for CustomNumberFormat {
147
153
  self.config.minimum_integer_digits = val;
148
154
  },
149
155
  CustomNumberFormatMsg::FracChange(val) => {
156
+ let defaults = &ctx.props().defaults;
150
157
  self.config.rounding_increment = None;
151
158
  self.config.maximum_fraction_digits = val.map(|(_, val)| {
152
- let min = self.config.minimum_fraction_digits.unwrap_or(2.);
159
+ let min = self
160
+ .config
161
+ .minimum_fraction_digits
162
+ .unwrap_or(defaults.fraction.0);
153
163
  val.max(min)
154
164
  });
155
165
 
156
166
  self.config.minimum_fraction_digits = val.map(|(val, _)| {
157
- let max = self.config.maximum_fraction_digits.unwrap_or(2.);
167
+ let max = self
168
+ .config
169
+ .maximum_fraction_digits
170
+ .unwrap_or(defaults.fraction.1);
158
171
  val.min(max)
159
172
  });
160
173
  },
161
174
  CustomNumberFormatMsg::SigChange(val) => {
175
+ let defaults = &ctx.props().defaults;
162
176
  self.config.maximum_significant_digits = val.map(|(_, val)| {
163
- let min = self.config.minimum_significant_digits.unwrap_or(1.);
177
+ let min = self
178
+ .config
179
+ .minimum_significant_digits
180
+ .unwrap_or(defaults.significant.0);
164
181
  val.max(min)
165
182
  });
166
183
 
167
184
  self.config.minimum_significant_digits = val.map(|(val, _)| {
168
- let max = self.config.maximum_significant_digits.unwrap_or(21.);
185
+ let max = self
186
+ .config
187
+ .maximum_significant_digits
188
+ .unwrap_or(defaults.significant.1);
169
189
  val.min(max)
170
190
  });
171
191
  },
@@ -191,8 +211,7 @@ impl Component for CustomNumberFormat {
191
211
  },
192
212
  };
193
213
 
194
- let is_float = ctx.props().view_type == ColumnType::Float;
195
- let filtered_config = self.config.clone().filter_default(is_float);
214
+ let filtered_config = self.config.clone().filter_default(&ctx.props().defaults);
196
215
  let mut value = serde_json::Map::new();
197
216
  if filtered_config != CustomNumberFormatConfig::default() {
198
217
  value.insert(
@@ -216,20 +235,24 @@ impl Component for CustomNumberFormat {
216
235
 
217
236
  impl CustomNumberFormat {
218
237
  fn initialize(ctx: &yew::prelude::Context<Self>) -> Self {
219
- let config = ctx.props().restored_config.clone();
238
+ let defaults = &ctx.props().defaults;
239
+ let mut config = ctx.props().restored_config.clone();
240
+ if config._style.is_none() && defaults.style != NumberFormatStyle::Decimal {
241
+ config._style = Some(defaults.style.clone());
242
+ }
243
+
244
+ if config._notation.is_none() && defaults.notation != Notation::Standard {
245
+ config._notation = Some(defaults.notation.clone());
246
+ }
247
+
220
248
  Self {
221
249
  style: config
222
250
  ._style
223
251
  .as_ref()
224
- .map(|style| match style {
225
- NumberFormatStyle::Decimal => NumberStyle::Decimal,
226
- NumberFormatStyle::Currency(_) => NumberStyle::Currency,
227
- NumberFormatStyle::Percent => NumberStyle::Percent,
228
- NumberFormatStyle::Unit(_) => NumberStyle::Unit,
229
- })
252
+ .map(NumberStyle::from)
230
253
  .unwrap_or_default(),
254
+ notation: config._notation.as_ref().map(NotationName::from),
231
255
  config,
232
- notation: None,
233
256
  }
234
257
  }
235
258
  }
@@ -15,7 +15,10 @@ use std::collections::HashSet;
15
15
  use serde::{Deserialize, Serialize};
16
16
  use serde_json::Value;
17
17
 
18
- use super::{CssKind, KeyValueOpts, NumberSeriesStyleDefaultConfig};
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
@@ -41,6 +44,21 @@ impl ColumnConfigSchema {
41
44
  }
42
45
  out
43
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
+ }
44
62
  }
45
63
 
46
64
  /// Discriminated union of widget kinds the viewer can render. Composite
@@ -101,7 +119,12 @@ pub enum ControlSpec {
101
119
  #[serde(default, skip_serializing_if = "std::ops::Not::not")]
102
120
  discrete: bool,
103
121
  },
104
- DatetimeFormat,
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
+ },
105
128
  StringFormat,
106
129
  NumberSeriesStyle {
107
130
  default: NumberSeriesStyleDefaultConfig,
@@ -109,8 +132,19 @@ pub enum ControlSpec {
109
132
  Symbols {
110
133
  default: KeyValueOpts,
111
134
  },
112
- 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
+ },
113
141
  AggregateDepth,
142
+
143
+ Group {
144
+ key: String,
145
+ #[serde(default)]
146
+ fields: Vec<ControlSpec>,
147
+ },
114
148
  }
115
149
 
116
150
  #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
@@ -165,37 +199,90 @@ pub fn discrete_pair(stops: Vec<GradientStopSpec>) -> Vec<GradientStopSpec> {
165
199
  }
166
200
 
167
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
+
168
248
  /// Canonicalize every CSS-valued default at schema ingest, dropping
169
249
  /// (and logging) fields whose default fails its kind's reader.
170
250
  pub fn canonicalize_defaults(mut self) -> Self {
171
- self.fields.retain_mut(|spec| {
172
- let (kind, key, default) = match spec {
173
- ControlSpec::Color { key, default } => (CssKind::Color, key, default),
174
- ControlSpec::Palette { key, default, .. } => (CssKind::Palette, key, default),
175
- ControlSpec::GradientStops { key, default, .. } => {
176
- (CssKind::Gradient, key, default)
177
- },
178
- _ => return true,
179
- };
180
-
181
- match kind.canonicalize(default) {
182
- Ok(canonical) => {
183
- *default = canonical;
184
- true
185
- },
186
- Err(error) => {
187
- tracing::error!("Dropping `{key}` — invalid schema default: {error}");
188
- false
189
- },
190
- }
191
- });
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
+ };
192
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);
193
280
  self
194
281
  }
195
282
 
196
283
  /// The CSS kind of the control owning `key`, if it is CSS-valued.
197
284
  pub fn css_kind_of(&self, key: &str) -> Option<CssKind> {
198
- self.fields.iter().find_map(|spec| match spec {
285
+ self.leaf_fields().into_iter().find_map(|spec| match spec {
199
286
  ControlSpec::Color { key: k, .. } if k == key => Some(CssKind::Color),
200
287
  ControlSpec::Palette { key: k, .. } if k == key => Some(CssKind::Palette),
201
288
  ControlSpec::GradientStops { key: k, .. } if k == key => Some(CssKind::Gradient),
@@ -212,11 +299,11 @@ impl ControlSpec {
212
299
  /// `columns_config` blob passed to `plugin.restore()`.
213
300
  pub fn serialized_keys(&self) -> Vec<&str> {
214
301
  match self {
215
- ControlSpec::DatetimeFormat => vec!["date_format"],
302
+ ControlSpec::DatetimeFormat { .. } => vec!["date_format"],
216
303
  ControlSpec::StringFormat => vec!["format"],
217
304
  ControlSpec::NumberSeriesStyle { .. } => vec!["chart_type", "stack"],
218
305
  ControlSpec::Symbols { .. } => vec!["symbols"],
219
- ControlSpec::NumberFormat => vec!["number_format"],
306
+ ControlSpec::NumberFormat { .. } => vec!["number_format"],
220
307
  ControlSpec::AggregateDepth => vec!["aggregate_depth"],
221
308
  ControlSpec::Enum { key, .. }
222
309
  | ControlSpec::Bool { key, .. }
@@ -225,6 +312,9 @@ impl ControlSpec {
225
312
  | ControlSpec::Color { key, .. }
226
313
  | ControlSpec::Palette { key, .. }
227
314
  | ControlSpec::GradientStops { key, .. } => vec![key.as_str()],
315
+ ControlSpec::Group { fields, .. } => {
316
+ fields.iter().flat_map(|f| f.serialized_keys()).collect()
317
+ },
228
318
  }
229
319
  }
230
320
  }
@@ -252,3 +342,244 @@ pub fn filter_to_schema(
252
342
  .map(|(k, v)| (k.clone(), v.clone()))
253
343
  .collect()
254
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
+ }
@@ -41,10 +41,6 @@ impl Default for DatetimeFormatType {
41
41
  }
42
42
 
43
43
  impl DatetimeFormatType {
44
- fn is_simple(&self) -> bool {
45
- self == &Self::Simple(SimpleDatetimeStyleConfig::default())
46
- }
47
-
48
44
  pub fn time_zone(&self) -> &Option<String> {
49
45
  match self {
50
46
  DatetimeFormatType::Custom(x) => &x.time_zone,
@@ -62,12 +58,13 @@ impl DatetimeFormatType {
62
58
 
63
59
  /// A model for the JSON serialized style configuration for a column of type
64
60
  /// `datetime`.
65
- #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, TS)]
61
+ #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize, TS)]
66
62
  // #[derive(WasmDescribe!, FromWasmAbi!)]
67
63
  pub struct DatetimeColumnStyleConfig {
68
64
  #[serde(default)]
69
- #[serde(skip_serializing_if = "DatetimeFormatType::is_simple")]
70
- pub date_format: DatetimeFormatType,
65
+ #[serde(skip_serializing_if = "Option::is_none")]
66
+ #[ts(optional, as = "Option<_>")]
67
+ pub date_format: Option<DatetimeFormatType>,
71
68
 
72
69
  #[serde(default)]
73
70
  #[serde(skip_serializing_if = "DatetimeColorMode::is_none")]
@@ -78,17 +75,3 @@ pub struct DatetimeColumnStyleConfig {
78
75
  #[ts(skip)]
79
76
  pub color: Option<String>,
80
77
  }
81
-
82
- impl Default for DatetimeColumnStyleConfig {
83
- fn default() -> Self {
84
- Self {
85
- date_format: DatetimeFormatType::Simple(SimpleDatetimeStyleConfig {
86
- time_zone: Default::default(),
87
- date_style: SimpleDatetimeFormat::Short,
88
- time_style: SimpleDatetimeFormat::Medium,
89
- }),
90
- datetime_color_mode: Default::default(),
91
- color: Default::default(),
92
- }
93
- }
94
- }