@perspective-dev/viewer 5.4.0 → 5.5.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 (142) hide show
  1. package/dist/cdn/perspective-viewer.js +4 -4
  2. package/dist/cdn/perspective-viewer.js.map +2 -2
  3. package/dist/css/blueprint.css +1 -1
  4. package/dist/css/botanical.css +1 -1
  5. package/dist/css/dracula.css +1 -1
  6. package/dist/css/eggplant.css +1 -1
  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/ar.css +1 -1
  11. package/dist/css/intl/de.css +1 -1
  12. package/dist/css/intl/es.css +1 -1
  13. package/dist/css/intl/fr.css +1 -1
  14. package/dist/css/intl/ja.css +1 -1
  15. package/dist/css/intl/pt.css +1 -1
  16. package/dist/css/intl/zh.css +1 -1
  17. package/dist/css/intl.css +1 -1
  18. package/dist/css/ledger.css +1 -1
  19. package/dist/css/monokai.css +1 -1
  20. package/dist/css/nord.css +1 -1
  21. package/dist/css/phosphor.css +1 -1
  22. package/dist/css/pro-dark.css +1 -1
  23. package/dist/css/pro.css +1 -1
  24. package/dist/css/solarized-dark.css +1 -1
  25. package/dist/css/solarized.css +1 -1
  26. package/dist/css/themes.css +1 -1
  27. package/dist/css/vaporwave.css +1 -1
  28. package/dist/css/velvet.css +1 -1
  29. package/dist/docs/perspective-docs.json +1 -1
  30. package/dist/esm/perspective-viewer.d.ts +2 -0
  31. package/dist/esm/perspective-viewer.inline.js +9 -9
  32. package/dist/esm/perspective-viewer.inline.js.map +3 -3
  33. package/dist/esm/perspective-viewer.js +4 -4
  34. package/dist/esm/perspective-viewer.js.map +2 -2
  35. package/dist/esm/plugin.d.ts +9 -0
  36. package/dist/esm/ts-rs/AddPanelOptions.d.ts +9 -0
  37. package/dist/esm/ts-rs/ExportMethod.d.ts +1 -1
  38. package/dist/esm/ts-rs/GetViewMode.d.ts +4 -0
  39. package/dist/esm/ts-rs/GetViewOptions.d.ts +8 -0
  40. package/dist/esm/ts-rs/RestoreOptions.d.ts +6 -0
  41. package/dist/esm/ts-rs/RestoreWorkspaceOptions.d.ts +9 -0
  42. package/dist/esm/ts-rs/WorkspaceConfigUpdate.d.ts +30 -22
  43. package/dist/wasm/perspective-viewer.d.ts +102 -47
  44. package/dist/wasm/perspective-viewer.js +233 -142
  45. package/dist/wasm/perspective-viewer.wasm +0 -0
  46. package/dist/wasm/perspective-viewer.wasm.d.ts +18 -18
  47. package/package.json +7 -7
  48. package/src/css/column-selector.css +11 -3
  49. package/src/css/column-style.css +162 -0
  50. package/src/css/config-selector.css +1 -1
  51. package/src/css/form/debug.css +9 -0
  52. package/src/css/status-bar.css +11 -0
  53. package/src/css/ui/containers/tabs.css +2 -1
  54. package/src/rust/agent/tools.rs +80 -7
  55. package/src/rust/components/column_selector/active_column.rs +9 -0
  56. package/src/rust/components/column_selector.rs +11 -0
  57. package/src/rust/components/column_settings_sidebar/style_tab/alignment_field.rs +90 -0
  58. package/src/rust/components/column_settings_sidebar/style_tab/font_field.rs +267 -0
  59. package/src/rust/components/column_settings_sidebar/style_tab/primitive_field.rs +1 -1
  60. package/src/rust/components/column_settings_sidebar/style_tab.rs +68 -9
  61. package/src/rust/components/debug_panel.rs +107 -50
  62. package/src/rust/components/export_dropdown.rs +6 -0
  63. package/src/rust/components/main_panel/msg.rs +4 -0
  64. package/src/rust/components/main_panel.rs +14 -0
  65. package/src/rust/components/mod.rs +0 -1
  66. package/src/rust/components/plugin_tab.rs +57 -3
  67. package/src/rust/components/settings_panel.rs +6 -0
  68. package/src/rust/components/status_bar.rs +12 -3
  69. package/src/rust/components/status_indicator.rs +4 -0
  70. package/src/rust/components/viewer/filters.rs +2 -2
  71. package/src/rust/components/viewer/msg.rs +4 -0
  72. package/src/rust/components/viewer/panels.rs +6 -3
  73. package/src/rust/components/viewer/render.rs +1 -0
  74. package/src/rust/components/viewer/wiring.rs +14 -2
  75. package/src/rust/components/viewer.rs +2 -2
  76. package/src/rust/config/column_config_schema.rs +312 -8
  77. package/src/rust/config/datetime_column_style.rs +0 -11
  78. package/src/rust/config/export_method.rs +24 -0
  79. package/src/rust/config/mod.rs +0 -2
  80. package/src/rust/config/options.rs +50 -0
  81. package/src/rust/config/workspace_config.rs +69 -24
  82. package/src/rust/custom_elements/viewer.rs +214 -113
  83. package/src/rust/js/plugin.rs +3 -1
  84. package/src/rust/lib.rs +7 -3
  85. package/src/rust/queries/plugin_column_styles.rs +6 -5
  86. package/src/rust/renderer/plugin_config.rs +200 -41
  87. package/src/rust/renderer/plugin_store.rs +2 -1
  88. package/src/rust/renderer/props.rs +5 -0
  89. package/src/rust/renderer.rs +9 -0
  90. package/src/rust/session/metadata.rs +5 -10
  91. package/src/rust/session/props.rs +4 -0
  92. package/src/rust/session.rs +273 -195
  93. package/src/rust/tasks/copy_export.rs +67 -0
  94. package/src/rust/tasks/create_panel.rs +16 -13
  95. package/src/rust/tasks/pipeline.rs +26 -8
  96. package/src/rust/tasks/restore_panel.rs +129 -30
  97. package/src/rust/tasks/send_column_config.rs +3 -0
  98. package/src/rust/tasks/sync_update_panels.rs +148 -68
  99. package/src/rust/tasks/table_lifecycle.rs +7 -1
  100. package/src/rust/ui/containers/tab_list.rs +1 -1
  101. package/src/rust/ui/form/intl_label.rs +4 -23
  102. package/src/rust/ui/portal.rs +1 -0
  103. package/src/rust/utils/css_vars.rs +13 -0
  104. package/src/rust/{config/datetime_column_style/color_mode.rs → utils/in_flight.rs} +47 -35
  105. package/src/rust/utils/local_fonts.rs +395 -0
  106. package/src/rust/utils/mod.rs +4 -0
  107. package/src/rust/utils/pending_effects.rs +6 -30
  108. package/src/rust/workspace.rs +12 -0
  109. package/src/svg/status_pending.svg +3 -0
  110. package/src/themes/icons.css +1 -0
  111. package/src/themes/intl/ar.css +20 -7
  112. package/src/themes/intl/de.css +20 -7
  113. package/src/themes/intl/es.css +20 -7
  114. package/src/themes/intl/fr.css +20 -7
  115. package/src/themes/intl/ja.css +20 -7
  116. package/src/themes/intl/pt.css +20 -7
  117. package/src/themes/intl/zh.css +20 -7
  118. package/src/themes/intl.css +20 -7
  119. package/src/themes/monokai.css +1 -1
  120. package/src/themes/vaporwave.css +1 -1
  121. package/src/ts/perspective-viewer.ts +2 -0
  122. package/src/ts/plugin.ts +9 -0
  123. package/src/ts/ts-rs/AddPanelOptions.ts +10 -0
  124. package/src/ts/ts-rs/ExportMethod.ts +1 -1
  125. package/src/ts/ts-rs/{DatetimeColorMode.ts → GetViewMode.ts} +4 -1
  126. package/src/ts/ts-rs/GetViewOptions.ts +7 -0
  127. package/src/ts/ts-rs/RestoreOptions.ts +7 -1
  128. package/src/ts/ts-rs/RestoreWorkspaceOptions.ts +10 -0
  129. package/src/ts/ts-rs/WorkspaceConfigUpdate.ts +31 -20
  130. package/dist/esm/ts-rs/DatetimeColorMode.d.ts +0 -1
  131. package/dist/esm/ts-rs/FormatMode.d.ts +0 -1
  132. package/dist/esm/ts-rs/StringColorMode.d.ts +0 -1
  133. package/src/rust/components/string_column_style.rs +0 -120
  134. package/src/rust/config/string_column_style.rs +0 -135
  135. package/src/ts/ts-rs/FormatMode.ts +0 -3
  136. package/src/ts/ts-rs/StringColorMode.ts +0 -3
  137. /package/dist/wasm/snippets/{perspective-viewer-adc6f3f79141112b → perspective-viewer-6813b568219c6763}/inline0.js +0 -0
  138. /package/dist/wasm/snippets/{perspective-viewer-adc6f3f79141112b → perspective-viewer-6813b568219c6763}/inline1.js +0 -0
  139. /package/dist/wasm/snippets/{perspective-viewer-adc6f3f79141112b → perspective-viewer-6813b568219c6763}/inline2.js +0 -0
  140. /package/dist/wasm/snippets/{perspective-viewer-adc6f3f79141112b → perspective-viewer-6813b568219c6763}/inline3.js +0 -0
  141. /package/dist/wasm/snippets/{perspective-viewer-adc6f3f79141112b → perspective-viewer-6813b568219c6763}/inline4.js +0 -0
  142. /package/dist/wasm/snippets/{perspective-viewer-adc6f3f79141112b → perspective-viewer-6813b568219c6763}/inline5.js +0 -0
package/src/rust/lib.rs CHANGED
@@ -93,25 +93,29 @@ export type * from "../../src/ts/ts-rs/WorkspaceConfigUpdate.d.ts";
93
93
  export type * from "../../src/ts/ts-rs/ExportMethod.d.ts";
94
94
  export type * from "../../src/ts/ts-rs/PanelOptions.d.ts";
95
95
  export type * from "../../src/ts/ts-rs/RestoreOptions.d.ts";
96
+ export type * from "../../src/ts/ts-rs/RestoreWorkspaceOptions.d.ts";
97
+ export type * from "../../src/ts/ts-rs/AddPanelOptions.d.ts";
96
98
  export type * from "../../src/ts/ts-rs/SaveWorkspaceOptions.d.ts";
97
99
  export type * from "../../src/ts/ts-rs/ClientOptions.d.ts";
98
100
  export type * from "../../src/ts/ts-rs/ExportOptions.d.ts";
99
101
  export type * from "../../src/ts/ts-rs/GetTableOptions.d.ts";
100
102
  export type * from "../../src/ts/ts-rs/GetClientOptions.d.ts";
103
+ export type * from "../../src/ts/ts-rs/GetViewOptions.d.ts";
104
+ export type * from "../../src/ts/ts-rs/GetViewMode.d.ts";
101
105
  export type * from "../../src/ts/ts-rs/CustomNumberFormatConfig.d.ts";
102
106
  export type * from "../../src/ts/ts-rs/NumberFormatStyle.d.ts";
103
107
  export type * from "../../src/ts/ts-rs/Notation.d.ts";
104
108
  export type * from "../../src/ts/ts-rs/DatetimeFormatType.d.ts";
105
- export type * from "../../src/ts/ts-rs/StringColorMode.d.ts";
106
- export type * from "../../src/ts/ts-rs/DatetimeColorMode.d.ts";
107
- export type * from "../../src/ts/ts-rs/FormatMode.d.ts";
108
109
 
109
110
  import type {GetTableOptions} from "../../src/ts/ts-rs/GetTableOptions.d.ts";
110
111
  import type {PanelOptions} from "../../src/ts/ts-rs/PanelOptions.d.ts";
111
112
  import type {RestoreOptions} from "../../src/ts/ts-rs/RestoreOptions.d.ts";
113
+ import type {RestoreWorkspaceOptions} from "../../src/ts/ts-rs/RestoreWorkspaceOptions.d.ts";
114
+ import type {AddPanelOptions} from "../../src/ts/ts-rs/AddPanelOptions.d.ts";
112
115
  import type {SaveWorkspaceOptions} from "../../src/ts/ts-rs/SaveWorkspaceOptions.d.ts";
113
116
  import type {ExportOptions} from "../../src/ts/ts-rs/ExportOptions.d.ts";
114
117
  import type {GetClientOptions} from "../../src/ts/ts-rs/GetClientOptions.d.ts";
118
+ import type {GetViewOptions} from "../../src/ts/ts-rs/GetViewOptions.d.ts";
115
119
  import type {ClientOptions} from "../../src/ts/ts-rs/ClientOptions.d.ts";
116
120
  import type {ViewerConfig} from "../../src/ts/ts-rs/ViewerConfig.d.ts";
117
121
  import type {ViewerConfigUpdate} from "../../src/ts/ts-rs/ViewerConfigUpdate.d.ts";
@@ -29,18 +29,19 @@ struct ColumnStats {
29
29
  abs_max: Option<f64>,
30
30
  }
31
31
 
32
- /// Queries the active plugin for its plugin-scoped
33
- /// [`ColumnConfigSchema`]. Mirrors [`get_column_config_schema`] but
34
- /// drops the column-specific args — the schema describes the shape
35
- /// of the plugin's own config bucket on the renderer.
32
+ /// Queries the active plugin for its plugin-scoped [`ColumnConfigSchema`] as
33
+ /// it applies to `current_value`.
36
34
  pub fn get_plugin_config_schema(
37
35
  renderer: &Renderer,
38
36
  view_config: &ViewConfig,
37
+ current_value: Option<&serde_json::Map<String, serde_json::Value>>,
39
38
  ) -> ApiResult<ColumnConfigSchema> {
40
39
  let plugin = renderer.ensure_plugin_selected()?;
41
40
  let view_config_js =
42
41
  wasm_bindgen::JsValue::from_serde_ext(view_config).unwrap_or(wasm_bindgen::JsValue::NULL);
43
- let raw = plugin._plugin_config_schema(&view_config_js)?;
42
+ let current_js = wasm_bindgen::JsValue::from_serde_ext(&current_value)
43
+ .unwrap_or(wasm_bindgen::JsValue::NULL);
44
+ let raw = plugin._plugin_config_schema(&view_config_js, &current_js)?;
44
45
  serde_wasm_bindgen::from_value::<ColumnConfigSchema>(raw)
45
46
  .map(|schema| schema.canonicalize())
46
47
  .map_err(|e| e.into())
@@ -247,10 +247,16 @@ impl Renderer {
247
247
  {
248
248
  let active = schema.active_keys();
249
249
  cfg.retain(|k, _| active.contains(k));
250
- let errors = normalize_css_values(&schema, &mut cfg);
250
+ let errors = normalize_values(&schema, &mut cfg);
251
251
  if let Some((key, error)) = errors.first() {
252
+ let ty = session
253
+ .metadata()
254
+ .get_column_view_type(&col)
255
+ .map(|t| format!(" for a {t} column"))
256
+ .unwrap_or_default();
257
+
252
258
  return Err(ApiError::from(JsValue::from_str(&format!(
253
- "Invalid `columns_config[\"{col}\"].{key}`: {error}"
259
+ "Invalid `columns_config[\"{col}\"].{key}`{ty}: {error}"
254
260
  ))));
255
261
  }
256
262
 
@@ -305,7 +311,7 @@ impl Renderer {
305
311
  &column_name,
306
312
  current_value.as_ref(),
307
313
  ) {
308
- for (key, error) in normalize_css_values(&schema, &mut update.value) {
314
+ for (key, error) in normalize_values(&schema, &mut update.value) {
309
315
  tracing::error!("Dropping `{column_name}`.`{key}`: {error}");
310
316
  }
311
317
 
@@ -329,16 +335,25 @@ impl Renderer {
329
335
  entry.clone()
330
336
  };
331
337
 
332
- let active = self
338
+ // The schema AFTER the update decides which keys survive and, for a
339
+ // key whose control kind follows another key (`fg_color` reads as a
340
+ // color, gradient or palette depending on `fg_mode`), whether the
341
+ // value still parses under its new kind. A value that no longer
342
+ // does is dropped here, so `save()` never emits a config that
343
+ // `restore()` would reject.
344
+ let after = self
333
345
  .query_column_config_schema(view_config, session, &column_name, Some(&next))
334
- .ok()
335
- .map(|schema| schema.active_keys());
346
+ .ok();
336
347
 
337
348
  let mut st = self.borrow_mut();
338
349
  let bucket = st.plugin_states.entry(n).or_default();
339
350
  if let Some(entry) = bucket.columns.get_mut(&column_name) {
340
- if let Some(active) = &active {
351
+ if let Some(schema) = &after {
352
+ let active = schema.active_keys();
341
353
  entry.retain(|k, _| active.contains(k));
354
+ for (key, error) in normalize_values(schema, entry) {
355
+ tracing::warn!("Dropping `{column_name}`.`{key}` after a mode change: {error}");
356
+ }
342
357
  }
343
358
 
344
359
  if entry.is_empty() {
@@ -371,15 +386,18 @@ impl Renderer {
371
386
  }
372
387
  }
373
388
 
374
- /// Synchronously query the active plugin's
375
- /// [`ColumnConfigSchema`] used to gate plugin-config strip logic.
389
+ /// Synchronously query the active plugin's [`ColumnConfigSchema`] as it
390
+ /// applies to `current_value`, the plugin-config state the schema should
391
+ /// describe.
376
392
  fn query_plugin_config_schema(
377
393
  &self,
378
394
  view_config: &ViewConfig,
395
+ current_value: Option<&serde_json::Map<String, serde_json::Value>>,
379
396
  ) -> ApiResult<ColumnConfigSchema> {
380
397
  let plugin = self.ensure_plugin_selected()?;
381
398
  let view_config_js = JsValue::from_serde_ext(view_config).unwrap_or(JsValue::NULL);
382
- let raw = plugin._plugin_config_schema(&view_config_js)?;
399
+ let current_js = JsValue::from_serde_ext(&current_value).unwrap_or(JsValue::NULL);
400
+ let raw = plugin._plugin_config_schema(&view_config_js, &current_js)?;
383
401
  serde_wasm_bindgen::from_value::<ColumnConfigSchema>(raw)
384
402
  .map(|schema| schema.canonicalize())
385
403
  .map_err(|e| e.into())
@@ -434,12 +452,9 @@ impl Renderer {
434
452
  .map_err(|e| e.into())
435
453
  }
436
454
 
437
- /// Wholesale update the active plugin's plugin-level config map.
438
- /// Entries whose value equals the schema-declared default are
439
- /// treated as "reset this key" — the corresponding bucket entry
440
- /// is cleared rather than the default being stored literally.
441
- /// Keys absent from the incoming map are left alone (merge
442
- /// semantics for the non-default subset).
455
+ /// Merge an update into the active plugin's plugin-level config map,
456
+ /// clearing entries equal to the schema default and dropping keys the
457
+ /// resulting schema no longer advertises.
443
458
  pub fn update_plugin_config(
444
459
  &self,
445
460
  view_config: &ViewConfig,
@@ -449,28 +464,42 @@ impl Renderer {
449
464
  return Ok(false);
450
465
  };
451
466
 
452
- let schema = self.query_plugin_config_schema(view_config).ok();
453
- let mut st = self.borrow_mut();
454
- let bucket = st.plugin_states.entry(n).or_default();
455
467
  match update {
456
468
  OptionalUpdate::SetDefault => {
469
+ let mut st = self.borrow_mut();
470
+ let bucket = st.plugin_states.entry(n).or_default();
457
471
  let changed = !bucket.plugin.is_empty();
458
472
  bucket.plugin.clear();
459
473
  Ok(changed)
460
474
  },
461
475
  OptionalUpdate::Missing => Ok(false),
462
476
  OptionalUpdate::Update(mut map) => {
463
- let mut changed = false;
477
+ let mut merged = self.get_plugin_config();
478
+ for (k, v) in &map {
479
+ merged.insert(k.clone(), v.clone());
480
+ }
481
+
482
+ let schema = self
483
+ .query_plugin_config_schema(view_config, Some(&merged))
484
+ .ok();
485
+
486
+ let mut active = None;
464
487
  if let Some(s) = &schema {
465
- let active = s.active_keys();
466
- map.retain(|k, _| active.contains(k));
467
- let errors = normalize_css_values(s, &mut map);
488
+ let keys = s.active_keys();
489
+ map.retain(|k, _| keys.contains(k));
490
+ active = Some(keys);
491
+ let errors = normalize_values(s, &mut map);
468
492
  if let Some((key, error)) = errors.first() {
469
493
  return Err(ApiError::from(JsValue::from_str(&format!(
470
494
  "Invalid `plugin_config.{key}`: {error}"
471
495
  ))));
472
496
  }
497
+ }
473
498
 
499
+ let mut st = self.borrow_mut();
500
+ let bucket = st.plugin_states.entry(n).or_default();
501
+ let mut changed = false;
502
+ if let Some(s) = &schema {
474
503
  let leaves = s.leaf_fields();
475
504
  map.retain(|key, value| {
476
505
  let is_default = leaves
@@ -494,12 +523,19 @@ impl Renderer {
494
523
  }
495
524
  }
496
525
 
526
+ if let Some(active) = active {
527
+ let before = bucket.plugin.len();
528
+ bucket.plugin.retain(|k, _| active.contains(k));
529
+ changed |= bucket.plugin.len() != before;
530
+ }
531
+
497
532
  Ok(changed)
498
533
  },
499
534
  }
500
535
  }
501
536
 
502
- /// Apply a single schema-field update from the plugin-settings UI
537
+ /// Apply a single schema-field update from the plugin-settings UI,
538
+ /// dropping keys the resulting schema no longer advertises.
503
539
  pub fn update_plugin_config_field(
504
540
  &self,
505
541
  view_config: &ViewConfig,
@@ -509,12 +545,28 @@ impl Renderer {
509
545
  return false;
510
546
  };
511
547
 
512
- if let Ok(schema) = self.query_plugin_config_schema(view_config) {
513
- for (key, error) in normalize_css_values(&schema, &mut update.value) {
548
+ let mut next = self.get_plugin_config();
549
+ for k in &update.keys {
550
+ match update.value.get(k) {
551
+ Some(v) => {
552
+ next.insert(k.clone(), v.clone());
553
+ },
554
+ None => {
555
+ next.remove(k);
556
+ },
557
+ }
558
+ }
559
+
560
+ let schema = self
561
+ .query_plugin_config_schema(view_config, Some(&next))
562
+ .ok();
563
+
564
+ if let Some(schema) = &schema {
565
+ for (key, error) in normalize_values(schema, &mut update.value) {
514
566
  tracing::error!("Dropping `plugin_config`.`{key}`: {error}");
515
567
  }
516
568
 
517
- strip_default_values(&schema, &mut update.value);
569
+ strip_default_values(schema, &mut update.value);
518
570
  }
519
571
 
520
572
  let mut st = self.borrow_mut();
@@ -532,28 +584,48 @@ impl Renderer {
532
584
  }
533
585
  }
534
586
 
587
+ if let Some(schema) = &schema {
588
+ let active = schema.active_keys();
589
+ let before = bucket.plugin.len();
590
+ bucket.plugin.retain(|k, _| active.contains(k));
591
+ changed |= bucket.plugin.len() != before;
592
+ }
593
+
535
594
  changed
536
595
  }
537
596
  }
538
597
 
539
- fn normalize_css_values(
598
+ /// Canonicalize every CSS-valued key of `map` and check every `Enum`-valued
599
+ /// key against its variants, removing and reporting the keys that fail.
600
+ /// The schema is the one queried for `map`'s own values, so an `Enum`'s
601
+ /// variants are exactly the values the column's type accepts.
602
+ fn normalize_values(
540
603
  schema: &ColumnConfigSchema,
541
604
  map: &mut serde_json::Map<String, serde_json::Value>,
542
605
  ) -> Vec<(String, String)> {
543
606
  let mut errors = vec![];
544
607
  for (key, value) in map.iter_mut() {
545
- let Some(kind) = schema.css_kind_of(key) else {
546
- continue;
547
- };
548
-
549
- let canonical = match value.as_str() {
550
- Some(src) => kind.canonicalize(src),
551
- None => Err(format!("expected a CSS string, got `{value}`")),
552
- };
608
+ if let Some(kind) = schema.css_kind_of(key) {
609
+ let canonical = match value.as_str() {
610
+ Some(src) => kind.canonicalize(src),
611
+ None => Err(format!("expected a CSS string, got `{value}`")),
612
+ };
553
613
 
554
- match canonical {
555
- Ok(canonical) => *value = Value::String(canonical),
556
- Err(error) => errors.push((key.clone(), error)),
614
+ match canonical {
615
+ Ok(canonical) => *value = Value::String(canonical),
616
+ Err(error) => errors.push((key.clone(), error)),
617
+ }
618
+ } else if let Some(variants) = schema.enum_variants_of(key) {
619
+ let accepted = variants.iter().any(|v| value.as_str() == Some(&v.value));
620
+ if !accepted {
621
+ let expected = variants
622
+ .iter()
623
+ .map(|v| format!("`{}`", v.value))
624
+ .collect::<Vec<_>>()
625
+ .join(", ");
626
+
627
+ errors.push((key.clone(), format!("{value} is not one of {expected}")));
628
+ }
557
629
  }
558
630
  }
559
631
 
@@ -580,6 +652,21 @@ fn matches_declared_default(spec: &ControlSpec, key: &str, value: &Value) -> boo
580
652
  match spec {
581
653
  ControlSpec::Enum {
582
654
  key: k, default, ..
655
+ }
656
+ | ControlSpec::Font {
657
+ key: k, default, ..
658
+ } if k == key => value.as_str() == Some(default.as_str()),
659
+ ControlSpec::Font {
660
+ size: Some(size), ..
661
+ } if size.key == key => value.as_f64() == Some(size.default),
662
+ ControlSpec::Font { bold, italic, .. } => [bold, italic]
663
+ .into_iter()
664
+ .flatten()
665
+ .any(|toggle| toggle.key == key && value.as_bool() == Some(toggle.default)),
666
+ ControlSpec::Alignment {
667
+ key: k,
668
+ default: Some(default),
669
+ ..
583
670
  } if k == key => value.as_str() == Some(default.as_str()),
584
671
  ControlSpec::Bool {
585
672
  key: k, default, ..
@@ -703,6 +790,35 @@ mod tests {
703
790
  ));
704
791
  }
705
792
 
793
+ #[test]
794
+ fn alignment_strips_only_a_declared_default() {
795
+ let schema = ColumnConfigSchema {
796
+ fields: vec![
797
+ ControlSpec::Alignment {
798
+ key: "align".to_owned(),
799
+ default: None,
800
+ corners: false,
801
+ },
802
+ ControlSpec::Alignment {
803
+ key: "legend_anchor".to_owned(),
804
+ default: Some(Alignment::TopRight),
805
+ corners: true,
806
+ },
807
+ ],
808
+ };
809
+
810
+ let mut map = json!({ "align": "center", "legend_anchor": "top-right" })
811
+ .as_object()
812
+ .unwrap()
813
+ .clone();
814
+
815
+ strip_default_values(&schema, &mut map);
816
+ assert_eq!(
817
+ map,
818
+ json!({ "align": "center" }).as_object().unwrap().clone()
819
+ );
820
+ }
821
+
706
822
  #[test]
707
823
  fn strip_default_values_sees_through_groups() {
708
824
  let leaves = vec![
@@ -751,7 +867,50 @@ mod tests {
751
867
  }
752
868
 
753
869
  #[test]
754
- fn normalize_css_values_canonicalizes_and_reports() {
870
+ fn normalize_values_rejects_enum_values_outside_the_variants() {
871
+ let schema = ColumnConfigSchema {
872
+ fields: vec![ControlSpec::Group {
873
+ key: "color".to_owned(),
874
+ fields: vec![ControlSpec::Enum {
875
+ key: "fg_mode".to_owned(),
876
+ default: "color".to_owned(),
877
+ variants: vec![
878
+ EnumVariant {
879
+ value: "disabled".to_owned(),
880
+ label: None,
881
+ },
882
+ EnumVariant {
883
+ value: "color".to_owned(),
884
+ label: None,
885
+ },
886
+ ],
887
+ }],
888
+ }],
889
+ };
890
+
891
+ let mut map = json!({ "fg_mode": "color", "other": "series" })
892
+ .as_object()
893
+ .unwrap()
894
+ .clone();
895
+
896
+ assert!(normalize_values(&schema, &mut map).is_empty());
897
+ assert_eq!(map.len(), 2);
898
+
899
+ let mut map = json!({ "fg_mode": "series" }).as_object().unwrap().clone();
900
+ let errors = normalize_values(&schema, &mut map);
901
+ assert_eq!(errors.len(), 1);
902
+ assert_eq!(errors[0].0, "fg_mode");
903
+ assert_eq!(errors[0].1, "\"series\" is not one of `disabled`, `color`");
904
+ assert!(map.is_empty());
905
+
906
+ let mut map = json!({ "fg_mode": 3 }).as_object().unwrap().clone();
907
+ let errors = normalize_values(&schema, &mut map);
908
+ assert_eq!(errors.len(), 1);
909
+ assert!(map.is_empty());
910
+ }
911
+
912
+ #[test]
913
+ fn normalize_values_canonicalizes_and_reports() {
755
914
  let schema = ColumnConfigSchema {
756
915
  fields: vec![
757
916
  ControlSpec::Color {
@@ -786,7 +945,7 @@ mod tests {
786
945
  .unwrap()
787
946
  .clone();
788
947
 
789
- let errors = normalize_css_values(&schema, &mut map);
948
+ let errors = normalize_values(&schema, &mut map);
790
949
  assert_eq!(errors.len(), 1);
791
950
  assert_eq!(errors[0].0, "gradient");
792
951
  assert_eq!(
@@ -26,7 +26,8 @@ pub struct PluginStore {
26
26
 
27
27
  impl PluginStore {
28
28
  fn init_lazy(&mut self) {
29
- self.plugins = Some(PLUGIN_REGISTRY.create_plugins());
29
+ let plugins = PLUGIN_REGISTRY.create_plugins();
30
+ self.plugins = Some(plugins);
30
31
  self.plugin_configs = Some(PLUGIN_REGISTRY.plugin_configs());
31
32
  self.plugin_records = Some(PLUGIN_REGISTRY.available_plugin_names_by_category());
32
33
  }
@@ -14,6 +14,7 @@ use std::rc::Rc;
14
14
 
15
15
  use crate::config::PluginStaticConfig;
16
16
  use crate::renderer::limits::RenderLimits;
17
+ use crate::renderer::plugin_config::ColumnConfigMap;
17
18
  use crate::utils::PtrEqRc;
18
19
 
19
20
  /// Value-semantic snapshot of the renderer state read by components.
@@ -50,6 +51,10 @@ pub struct RendererProps {
50
51
  /// the interior-mutable handle, no manual PubSub subscription, no
51
52
  /// race window between `plugins_idx` swap and the schema query.
52
53
  pub plugin_config: PtrEqRc<serde_json::Map<String, serde_json::Value>>,
54
+
55
+ /// Snapshot of the active plugin's `columns_config` bucket, re-read by
56
+ /// `Renderer::to_props` on `columns_config_changed` and `plugin_changed`.
57
+ pub columns_config: PtrEqRc<ColumnConfigMap>,
53
58
  }
54
59
 
55
60
  impl RendererProps {
@@ -112,6 +112,11 @@ pub struct RendererData {
112
112
  /// plugin's bucket.
113
113
  pub plugin_config_changed: PubSub<serde_json::Map<String, serde_json::Value>>,
114
114
 
115
+ /// Fires after the active plugin's per-column bucket changes by any path
116
+ /// (edit, restore or reset), unlike `column_style_changed` which covers
117
+ /// edits only.
118
+ pub columns_config_changed: PubSub<ColumnConfigMap>,
119
+
115
120
  /// `true` while the active plugin's "rendering N of M" warning is
116
121
  /// dismissable.
117
122
  pub render_warning: Cell<bool>,
@@ -263,6 +268,7 @@ impl Renderer {
263
268
  selection_changed: Default::default(),
264
269
  column_style_changed: Default::default(),
265
270
  plugin_config_changed: Default::default(),
271
+ columns_config_changed: Default::default(),
266
272
  render_warning: Cell::new(true),
267
273
  presize_pending: Cell::new(0),
268
274
  presized_box: Cell::new(None),
@@ -695,6 +701,7 @@ impl Renderer {
695
701
  .into();
696
702
 
697
703
  let plugin_config = PtrEqRc::new(self.get_plugin_config());
704
+ let columns_config = PtrEqRc::new(self.all_columns_configs());
698
705
  RendererProps {
699
706
  plugin_name,
700
707
  config,
@@ -702,6 +709,7 @@ impl Renderer {
702
709
  available_plugins,
703
710
  is_chart,
704
711
  plugin_config,
712
+ columns_config,
705
713
  }
706
714
  } else {
707
715
  RendererProps {
@@ -711,6 +719,7 @@ impl Renderer {
711
719
  available_plugins: PtrEqRc::new(vec![]),
712
720
  is_chart: false,
713
721
  plugin_config: PtrEqRc::default(),
722
+ columns_config: PtrEqRc::default(),
714
723
  }
715
724
  }
716
725
  }
@@ -92,7 +92,7 @@ impl SessionMetadata {
92
92
  &mut self,
93
93
  view_schema: &HashMap<String, ColumnType>,
94
94
  ) -> ApiResult<()> {
95
- self.as_mut().unwrap().view_schema = Some(view_schema.clone());
95
+ self.as_mut().ok_or("No `Table` set")?.view_schema = Some(view_schema.clone());
96
96
  Ok(())
97
97
  }
98
98
 
@@ -115,7 +115,7 @@ impl SessionMetadata {
115
115
  Some((name.clone(), dtype))
116
116
  })
117
117
  .collect();
118
- self.as_mut().unwrap().window_schema = window_schema;
118
+ self.as_mut().ok_or("No `Table` set")?.window_schema = window_schema;
119
119
  Ok(())
120
120
  }
121
121
 
@@ -151,16 +151,11 @@ impl SessionMetadata {
151
151
  )));
152
152
  }
153
153
 
154
- let mut edited = self
155
- .as_mut()
156
- .unwrap()
157
- .expr_meta
158
- .take()
159
- .map(|x| x.edited)
160
- .unwrap_or_default();
154
+ let state = self.as_mut().ok_or("No `Table` set")?;
155
+ let mut edited = state.expr_meta.take().map(|x| x.edited).unwrap_or_default();
161
156
 
162
157
  edited.retain(|k, _| valid_recs.expression_alias.contains_key(k));
163
- self.as_mut().unwrap().expr_meta = Some(SessionViewExpressionMetadata {
158
+ state.expr_meta = Some(SessionViewExpressionMetadata {
164
159
  expressions: valid_recs.clone(),
165
160
  edited,
166
161
  });
@@ -22,11 +22,15 @@ use crate::session::replace_expression_update::ViewConfigExt as ReplaceExprExt;
22
22
  use crate::session::{ColumnStats, TableErrorState};
23
23
  use crate::utils::*;
24
24
 
25
+ /// The panel's table binding as the status indicator and chrome see it:
26
+ /// bound, a `load()` in flight, or a named table awaiting its host (see
27
+ /// `Session::pending_table`).
25
28
  #[derive(Clone, Debug, PartialEq, Default)]
26
29
  pub enum TableLoadState {
27
30
  #[default]
28
31
  Loaded,
29
32
  Loading,
33
+ Pending,
30
34
  }
31
35
 
32
36
  /// Value-semantic snapshot of the session state read by the root component.