@perspective-dev/viewer 5.2.0 → 5.3.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.
Files changed (133) hide show
  1. package/dist/cdn/perspective-viewer.js +2 -2
  2. package/dist/cdn/perspective-viewer.js.map +2 -2
  3. package/dist/css/botanical.css +1 -1
  4. package/dist/css/dracula.css +1 -1
  5. package/dist/css/gruvbox-dark.css +1 -1
  6. package/dist/css/gruvbox.css +1 -1
  7. package/dist/css/icons.css +1 -1
  8. package/dist/css/monokai.css +1 -1
  9. package/dist/css/phosphor.css +1 -1
  10. package/dist/css/pro-dark.css +1 -1
  11. package/dist/css/pro.css +1 -1
  12. package/dist/css/solarized-dark.css +1 -1
  13. package/dist/css/solarized.css +1 -1
  14. package/dist/css/themes.css +1 -1
  15. package/dist/css/vaporwave.css +1 -1
  16. package/dist/docs/perspective-docs.json +1 -1
  17. package/dist/esm/perspective-viewer.d.ts +1 -0
  18. package/dist/esm/perspective-viewer.inline.js +9 -9
  19. package/dist/esm/perspective-viewer.inline.js.map +2 -2
  20. package/dist/esm/perspective-viewer.js +4 -4
  21. package/dist/esm/perspective-viewer.js.map +2 -2
  22. package/dist/esm/plugin.d.ts +10 -0
  23. package/dist/esm/ts-rs/PanelViewerConfig.d.ts +1 -1
  24. package/dist/esm/ts-rs/SaveWorkspaceOptions.d.ts +10 -0
  25. package/dist/esm/ts-rs/ViewerConfig.d.ts +1 -1
  26. package/dist/esm/ts-rs/WorkspaceConfig.d.ts +9 -1
  27. package/dist/esm/ts-rs/WorkspaceConfigUpdate.d.ts +13 -3
  28. package/dist/wasm/perspective-viewer.d.ts +43 -36
  29. package/dist/wasm/perspective-viewer.js +194 -136
  30. package/dist/wasm/perspective-viewer.wasm +0 -0
  31. package/dist/wasm/perspective-viewer.wasm.d.ts +19 -17
  32. package/package.json +1 -1
  33. package/src/css/column-settings-panel.css +9 -5
  34. package/src/css/column-style.css +209 -31
  35. package/src/css/dom/select.css +2 -1
  36. package/src/css/form/code-editor.css +4 -4
  37. package/src/rust/agent/tools.rs +48 -17
  38. package/src/rust/components/column_selector/active_column.rs +2 -2
  39. package/src/rust/components/column_selector/add_expression_button.rs +3 -3
  40. package/src/rust/components/column_selector/aggregate_selector.rs +2 -3
  41. package/src/rust/components/column_selector/config_selector.rs +8 -0
  42. package/src/rust/components/column_selector/expr_edit_button.rs +4 -10
  43. package/src/rust/components/column_selector/inactive_column.rs +2 -2
  44. package/src/rust/components/column_selector.rs +48 -4
  45. package/src/rust/components/column_settings_sidebar/style_tab/primitive_field.rs +293 -126
  46. package/src/rust/components/column_settings_sidebar/style_tab.rs +262 -225
  47. package/src/rust/components/column_settings_sidebar.rs +88 -69
  48. package/src/rust/components/editable_header.rs +85 -144
  49. package/src/rust/components/expression_editor.rs +9 -16
  50. package/src/rust/components/form/code_editor.rs +16 -6
  51. package/src/rust/components/form/debug.rs +1 -0
  52. package/src/rust/components/form/mod.rs +3 -1
  53. package/src/rust/components/form/multi_stop_gradient_selector.rs +476 -0
  54. package/src/rust/components/form/named_value_picker.rs +86 -0
  55. package/src/rust/components/form/palette_selector.rs +440 -0
  56. package/src/rust/components/main_panel/msg.rs +2 -2
  57. package/src/rust/components/main_panel/reconcile.rs +4 -4
  58. package/src/rust/components/plugin_tab.rs +4 -28
  59. package/src/rust/components/settings_panel.rs +25 -20
  60. package/src/rust/components/viewer/msg.rs +46 -7
  61. package/src/rust/components/viewer/panels.rs +11 -9
  62. package/src/rust/components/viewer/render.rs +8 -3
  63. package/src/rust/components/viewer/settings.rs +163 -23
  64. package/src/rust/components/viewer/snapshots.rs +46 -21
  65. package/src/rust/components/viewer/wiring.rs +14 -25
  66. package/src/rust/components/viewer.rs +12 -8
  67. package/src/rust/config/column_config_schema.rs +100 -41
  68. package/src/rust/config/css_scale.rs +882 -0
  69. package/src/rust/config/mod.rs +2 -0
  70. package/src/rust/config/options.rs +10 -0
  71. package/src/rust/config/viewer_config.rs +49 -55
  72. package/src/rust/config/workspace_config.rs +132 -5
  73. package/src/rust/custom_elements/viewer.rs +214 -155
  74. package/src/rust/custom_events.rs +14 -1
  75. package/src/rust/js/regular_layout.rs +3 -3
  76. package/src/rust/lib.rs +2 -0
  77. package/src/rust/presentation/column_locator.rs +19 -5
  78. package/src/rust/presentation.rs +192 -36
  79. package/src/rust/queries/column_locator.rs +74 -44
  80. package/src/rust/queries/get_viewer_config.rs +0 -3
  81. package/src/rust/queries/mod.rs +2 -0
  82. package/src/rust/queries/palette_set.rs +82 -0
  83. package/src/rust/queries/plugin_column_styles.rs +6 -2
  84. package/src/rust/renderer/dispatch.rs +16 -4
  85. package/src/rust/renderer/plugin_config.rs +309 -47
  86. package/src/rust/renderer.rs +15 -42
  87. package/src/rust/session/column_defaults_update.rs +15 -8
  88. package/src/rust/session/metadata.rs +5 -1
  89. package/src/rust/session/props.rs +1 -0
  90. package/src/rust/session.rs +9 -6
  91. package/src/rust/tasks/create_panel.rs +44 -57
  92. package/src/rust/tasks/edit_expression.rs +19 -23
  93. package/src/rust/tasks/edit_window.rs +8 -10
  94. package/src/rust/tasks/eject.rs +4 -1
  95. package/src/rust/tasks/pipeline.rs +2 -2
  96. package/src/rust/tasks/presize_panels.rs +44 -5
  97. package/src/rust/tasks/reset_all.rs +4 -4
  98. package/src/rust/tasks/restore_and_render.rs +17 -44
  99. package/src/rust/tasks/restore_panel.rs +11 -13
  100. package/src/rust/tasks/set_edit_mode.rs +3 -1
  101. package/src/rust/tasks/sync_update_panels.rs +11 -1
  102. package/src/rust/tasks/update_theme.rs +23 -18
  103. package/src/rust/utils/css_vars.rs +26 -0
  104. package/src/rust/utils/mod.rs +2 -0
  105. package/src/rust/utils/spawn.rs +1 -1
  106. package/src/rust/workspace.rs +435 -272
  107. package/src/svg/lock.svg +7 -0
  108. package/src/themes/botanical.css +10 -0
  109. package/src/themes/defaults.css +12 -0
  110. package/src/themes/dracula.css +17 -0
  111. package/src/themes/gruvbox-dark.css +20 -0
  112. package/src/themes/gruvbox.css +10 -0
  113. package/src/themes/icons.css +1 -0
  114. package/src/themes/monokai.css +22 -0
  115. package/src/themes/phosphor.css +5 -0
  116. package/src/themes/pro-dark.css +10 -0
  117. package/src/themes/solarized-dark.css +7 -0
  118. package/src/themes/solarized.css +7 -0
  119. package/src/themes/vaporwave.css +20 -0
  120. package/src/ts/perspective-viewer.ts +1 -0
  121. package/src/ts/plugin.ts +14 -0
  122. package/src/ts/ts-rs/PanelViewerConfig.ts +1 -1
  123. package/src/ts/ts-rs/SaveWorkspaceOptions.ts +11 -0
  124. package/src/ts/ts-rs/ViewerConfig.ts +1 -1
  125. package/src/ts/ts-rs/WorkspaceConfig.ts +8 -2
  126. package/src/ts/ts-rs/WorkspaceConfigUpdate.ts +11 -3
  127. package/src/rust/components/form/color_range_selector.rs +0 -137
  128. /package/dist/wasm/snippets/{perspective-viewer-02e461f3d7062b26 → perspective-viewer-39c3c3b28e3cbd48}/inline0.js +0 -0
  129. /package/dist/wasm/snippets/{perspective-viewer-02e461f3d7062b26 → perspective-viewer-39c3c3b28e3cbd48}/inline1.js +0 -0
  130. /package/dist/wasm/snippets/{perspective-viewer-02e461f3d7062b26 → perspective-viewer-39c3c3b28e3cbd48}/inline2.js +0 -0
  131. /package/dist/wasm/snippets/{perspective-viewer-02e461f3d7062b26 → perspective-viewer-39c3c3b28e3cbd48}/inline3.js +0 -0
  132. /package/dist/wasm/snippets/{perspective-viewer-02e461f3d7062b26 → perspective-viewer-39c3c3b28e3cbd48}/inline4.js +0 -0
  133. /package/dist/wasm/snippets/{perspective-viewer-02e461f3d7062b26 → perspective-viewer-39c3c3b28e3cbd48}/inline5.js +0 -0
@@ -42,6 +42,7 @@ use crate::tasks::{
42
42
  delete_expr, delete_window, save_expr, save_window, update_expr, update_window,
43
43
  };
44
44
  use crate::utils::PtrEqRc;
45
+ use crate::workspace::Workspace;
45
46
 
46
47
  #[derive(Clone, Derivative, Properties)]
47
48
  #[derivative(Debug)]
@@ -91,12 +92,13 @@ pub struct ColumnSettingsPanelProps {
91
92
 
92
93
  #[derivative(Debug = "ignore")]
93
94
  pub session: Session,
95
+
96
+ #[derivative(Debug = "ignore")]
97
+ pub workspace: Workspace,
94
98
  }
95
99
 
96
- impl ColumnSettingsPanelProps {
97
- /// Everything EXCEPT the trap-door `auto_width`: the props whose change
98
- /// invalidates the drafts that `initialize` rebuilds.
99
- fn identity_eq(&self, other: &Self) -> bool {
100
+ impl PartialEq for ColumnSettingsPanelProps {
101
+ fn eq(&self, other: &Self) -> bool {
100
102
  self.selected_column == other.selected_column
101
103
  && self.selected_tab == other.selected_tab
102
104
  && self.plugin_name == other.plugin_name
@@ -104,14 +106,46 @@ impl ColumnSettingsPanelProps {
104
106
  && self.view_config == other.view_config
105
107
  && self.column_stats == other.column_stats
106
108
  && self.selected_theme == other.selected_theme
109
+ && self.auto_width == other.auto_width
110
+ && self.is_pinned == other.is_pinned
107
111
  }
108
112
  }
109
113
 
110
- impl PartialEq for ColumnSettingsPanelProps {
111
- fn eq(&self, other: &Self) -> bool {
112
- self.identity_eq(other)
113
- && self.auto_width == other.auto_width
114
- && self.is_pinned == other.is_pinned
114
+ #[derive(Clone, PartialEq)]
115
+ struct Initials {
116
+ column_name: String,
117
+ expr: Rc<String>,
118
+ header: Option<String>,
119
+ window: Option<WindowSpec>,
120
+ }
121
+
122
+ impl Initials {
123
+ fn of(props: &ColumnSettingsPanelProps) -> Self {
124
+ let column_name = props
125
+ .metadata
126
+ .locator_name_or_default(&props.selected_column);
127
+
128
+ let expr = props
129
+ .metadata
130
+ .get_expression_by_alias(&column_name)
131
+ .or_else(|| props.view_config.expressions.get(&column_name).cloned())
132
+ .unwrap_or_default();
133
+
134
+ let expr = Rc::new(expr);
135
+ let header = (*expr != column_name).then_some(column_name.clone());
136
+
137
+ let window = props
138
+ .selected_column
139
+ .name()
140
+ .and_then(|name| props.view_config.windows.get(name))
141
+ .cloned();
142
+
143
+ Self {
144
+ column_name,
145
+ expr,
146
+ header,
147
+ window,
148
+ }
115
149
  }
116
150
  }
117
151
 
@@ -172,22 +206,23 @@ impl Component for ColumnSettingsPanel {
172
206
  initial_window_value: None,
173
207
  window_value: None,
174
208
  tabs: vec![],
175
- on_input: Callback::default(),
176
- on_save: Callback::default(),
177
- on_validate: Callback::default(),
209
+ on_input: ctx.link().callback(ColumnSettingsPanelMsg::SetExprValue),
210
+ on_save: ctx
211
+ .link()
212
+ .callback(ColumnSettingsPanelMsg::OnSaveAttributes),
213
+ on_validate: ctx.link().callback(ColumnSettingsPanelMsg::SetExprValid),
178
214
  };
179
215
 
180
- this.initialize(ctx);
216
+ this.reset_to(ctx, Initials::of(ctx.props()));
181
217
  this
182
218
  }
183
219
 
184
220
  fn changed(&mut self, ctx: &yew::prelude::Context<Self>, old_props: &Self::Properties) -> bool {
185
- // Only reached when props are unequal. Re-`initialize` (which wipes
186
- // in-progress expression/window drafts) only on IDENTITY changes -
187
- // a trap-door `auto_width` change re-renders the `Sidebar` sizer
188
- // alone.
189
- if !ctx.props().identity_eq(old_props) {
190
- self.initialize(ctx);
221
+ let next = Initials::of(ctx.props());
222
+ if ctx.props().selected_column != old_props.selected_column || next != self.initials() {
223
+ self.reset_to(ctx, next);
224
+ } else {
225
+ self.refresh_derived(ctx);
191
226
  }
192
227
 
193
228
  true
@@ -353,9 +388,9 @@ impl Component for ColumnSettingsPanel {
353
388
  };
354
389
 
355
390
  let header_props = props!(EditableHeaderProps {
391
+ value: self.header_value.clone(),
356
392
  initial_value: self.initial_header_value.clone(),
357
393
  placeholder: header_placeholder,
358
- reset_count: self.reset_count,
359
394
  editable: (ctx.props().selected_column.is_expr()
360
395
  && matches!(
361
396
  ctx.props().selected_tab,
@@ -374,7 +409,6 @@ impl Component for ColumnSettingsPanel {
374
409
  ]
375
410
  }),
376
411
  metadata: ctx.props().metadata.clone(),
377
- session: &ctx.props().session
378
412
  });
379
413
 
380
414
  let expr_editor = props!(ExpressionEditorProps {
@@ -382,6 +416,7 @@ impl Component for ColumnSettingsPanel {
382
416
  on_save: self.on_save.clone(),
383
417
  on_validate: self.on_validate.clone(),
384
418
  alias: ctx.props().selected_column.name().cloned(),
419
+ initial_expr: self.initial_expr_value.clone(),
385
420
  disabled: !ctx.props().selected_column.is_expr(),
386
421
  reset_count: self.reset_count,
387
422
  metadata: ctx.props().metadata.clone(),
@@ -457,6 +492,7 @@ impl Component for ColumnSettingsPanel {
457
492
  presentation: ctx.props().presentation.clone(),
458
493
  renderer: ctx.props().renderer.clone(),
459
494
  session: ctx.props().session.clone(),
495
+ workspace: ctx.props().workspace.clone(),
460
496
  };
461
497
 
462
498
  let tab_children = self.tabs.iter().map(|tab| match tab {
@@ -506,38 +542,22 @@ impl ColumnSettingsPanel {
506
542
  self.save_enabled = changed && valid;
507
543
  }
508
544
 
509
- fn initialize(&mut self, ctx: &yew::prelude::Context<Self>) {
510
- let column_name = ctx
511
- .props()
512
- .metadata
513
- .locator_name_or_default(&ctx.props().selected_column);
514
-
515
- let initial_expr_value = ctx
516
- .props()
517
- .metadata
518
- .get_expression_by_alias(&column_name)
519
- .unwrap_or_default();
520
-
521
- let initial_expr_value = Rc::new(initial_expr_value);
522
- let initial_header_value =
523
- (*initial_expr_value != column_name).then_some(column_name.clone());
545
+ fn initials(&self) -> Initials {
546
+ Initials {
547
+ column_name: self.column_name.clone(),
548
+ expr: self.initial_expr_value.clone(),
549
+ header: self.initial_header_value.clone(),
550
+ window: self.initial_window_value.clone(),
551
+ }
552
+ }
524
553
 
525
- let maybe_ty = ctx
554
+ fn refresh_derived(&mut self, ctx: &yew::prelude::Context<Self>) {
555
+ self.maybe_ty = ctx
526
556
  .props()
527
557
  .metadata
528
558
  .locator_view_type(&ctx.props().selected_column);
529
559
 
530
- // Specs are unnamed (the `windows` map key names them, and the
531
- // editable header owns naming in the UI), so drafts and saved specs
532
- // compare directly.
533
- let initial_window_value = ctx
534
- .props()
535
- .selected_column
536
- .name()
537
- .and_then(|name| ctx.props().view_config.windows.get(name))
538
- .cloned();
539
-
540
- let tabs = {
560
+ self.tabs = {
541
561
  let mut tabs = vec![];
542
562
  let is_new_expr = ctx.props().selected_column.is_new_expr();
543
563
  let show_styles = !is_new_expr
@@ -571,28 +591,27 @@ impl ColumnSettingsPanel {
571
591
 
572
592
  tabs
573
593
  };
594
+ }
574
595
 
575
- let on_input = ctx.link().callback(ColumnSettingsPanelMsg::SetExprValue);
576
- let on_save = ctx
577
- .link()
578
- .callback(ColumnSettingsPanelMsg::OnSaveAttributes);
579
-
580
- let on_validate = ctx.link().callback(ColumnSettingsPanelMsg::SetExprValid);
581
- *self = Self {
596
+ fn reset_to(&mut self, ctx: &yew::prelude::Context<Self>, initials: Initials) {
597
+ let Initials {
582
598
  column_name,
583
- expr_value: initial_expr_value.clone(),
584
- initial_expr_value,
585
- header_value: initial_header_value.clone(),
586
- initial_header_value,
587
- maybe_ty,
588
- window_value: initial_window_value.clone(),
589
- initial_window_value,
590
- tabs,
591
- header_valid: true,
592
- on_input,
593
- on_save,
594
- on_validate,
595
- ..*self
596
- }
599
+ expr,
600
+ header,
601
+ window,
602
+ } = initials;
603
+
604
+ self.column_name = column_name;
605
+ self.expr_value = expr.clone();
606
+ self.initial_expr_value = expr;
607
+ self.header_value = header.clone();
608
+ self.initial_header_value = header;
609
+ self.window_value = window.clone();
610
+ self.initial_window_value = window;
611
+ self.header_valid = true;
612
+ self.save_enabled = false;
613
+ self.reset_enabled = false;
614
+ self.reset_count = self.reset_count.wrapping_add(1);
615
+ self.refresh_derived(ctx);
597
616
  }
598
617
  }
@@ -14,32 +14,30 @@ use std::rc::Rc;
14
14
 
15
15
  use itertools::Itertools;
16
16
  use web_sys::{FocusEvent, HtmlInputElement, KeyboardEvent};
17
- use yew::{Callback, Component, Html, NodeRef, Properties, TargetCast, classes, html};
17
+ use yew::prelude::*;
18
18
 
19
19
  use super::type_icon::TypeIconType;
20
20
  use crate::components::type_icon::TypeIcon;
21
- use crate::session::{Session, SessionMetadataRc};
21
+ use crate::session::SessionMetadataRc;
22
22
 
23
23
  #[derive(Clone, PartialEq, Properties)]
24
24
  pub struct EditableHeaderProps {
25
25
  pub icon_type: Option<TypeIconType>,
26
26
  pub on_change: Callback<(Option<String>, bool)>,
27
27
  pub editable: bool,
28
+
29
+ /// The owner's draft.
30
+ pub value: Option<String>,
31
+
32
+ /// The saved name the draft is measured against.
28
33
  pub initial_value: Option<String>,
29
34
  pub placeholder: Rc<String>,
30
35
 
31
- // TODO remove this pattern
32
- #[prop_or_default]
33
- pub reset_count: u8,
34
-
35
36
  #[prop_or_default]
36
37
  pub update_on_input: bool,
37
38
 
38
39
  /// Session metadata snapshot — threaded from `SessionProps`.
39
40
  pub metadata: SessionMetadataRc,
40
-
41
- // State
42
- pub session: Session,
43
41
  }
44
42
 
45
43
  impl EditableHeaderProps {
@@ -55,152 +53,95 @@ impl EditableHeaderProps {
55
53
  Some((idx, _)) => split[..idx].to_owned(),
56
54
  }
57
55
  }
58
- }
59
-
60
- pub enum EditableHeaderMsg {
61
- SetNewValue(String),
62
- OnClick(()),
63
- }
64
56
 
65
- #[derive(Debug)]
66
- pub struct EditableHeader {
67
- noderef: NodeRef,
68
- edited: bool,
69
- valid: bool,
70
- value: Option<String>,
71
- placeholder: String,
72
- }
57
+ fn is_valid(&self, value: &Option<String>, placeholder: &str) -> bool {
58
+ let Some(value) = value else {
59
+ return true;
60
+ };
73
61
 
74
- impl Component for EditableHeader {
75
- type Message = EditableHeaderMsg;
76
- type Properties = EditableHeaderProps;
77
-
78
- fn create(ctx: &yew::prelude::Context<Self>) -> Self {
79
- Self {
80
- value: ctx.props().initial_value.clone(),
81
- placeholder: ctx.props().split_placeholder(),
82
- valid: true,
83
- noderef: NodeRef::default(),
84
- edited: false,
62
+ if value == placeholder || Some(value) == self.initial_value.as_ref() {
63
+ return true;
85
64
  }
86
- }
87
65
 
88
- fn changed(&mut self, ctx: &yew::prelude::Context<Self>, old_props: &Self::Properties) -> bool {
89
- if ctx.props().reset_count != old_props.reset_count {
90
- self.value.clone_from(&ctx.props().initial_value);
91
- }
92
- if ctx.props().initial_value != old_props.initial_value {
93
- self.edited = false;
94
- self.value.clone_from(&ctx.props().initial_value);
95
- }
96
- if !ctx.props().editable {
97
- self.edited = false;
98
- }
99
- self.placeholder = ctx.props().split_placeholder();
100
- ctx.props() != old_props
101
- }
66
+ let metadata = &self.metadata;
67
+ let Some(table_columns) = metadata.get_table_columns() else {
68
+ return true;
69
+ };
102
70
 
103
- fn update(&mut self, ctx: &yew::prelude::Context<Self>, msg: Self::Message) -> bool {
104
- match msg {
105
- EditableHeaderMsg::SetNewValue(new_value) => {
106
- let maybe_value = (!new_value.is_empty()).then_some(new_value.clone());
107
- self.edited = ctx.props().initial_value != maybe_value;
108
-
109
- self.valid = (|| -> Option<bool> {
110
- if maybe_value
111
- .as_ref()
112
- .map(|v| v == &self.placeholder)
113
- .unwrap_or(true)
114
- {
115
- return Some(true);
116
- }
117
- if !self.edited {
118
- return Some(true);
119
- }
120
- let metadata = &ctx.props().metadata;
121
- let expressions = metadata.get_expression_columns();
122
- let windows = metadata.get_window_columns();
123
- let found = metadata
124
- .get_table_columns()?
125
- .iter()
126
- .chain(expressions)
127
- .chain(windows)
128
- .contains(&new_value);
129
- Some(!found)
130
- })()
131
- .unwrap_or(true);
132
-
133
- self.value.clone_from(&maybe_value);
134
- ctx.props().on_change.emit((maybe_value, self.valid));
135
- true
136
- },
137
- EditableHeaderMsg::OnClick(()) => {
138
- self.noderef
139
- .cast::<HtmlInputElement>()
140
- .unwrap()
141
- .focus()
142
- .unwrap();
143
- false
144
- },
145
- }
71
+ !table_columns
72
+ .iter()
73
+ .chain(metadata.get_expression_columns())
74
+ .chain(metadata.get_window_columns())
75
+ .contains(value)
146
76
  }
77
+ }
147
78
 
148
- fn view(&self, ctx: &yew::prelude::Context<Self>) -> Html {
149
- let mut classes = classes!("sidebar_header_contents");
150
- if ctx.props().editable {
151
- classes.push("editable");
152
- }
153
-
154
- if !self.valid {
155
- classes.push("invalid");
156
- }
157
-
158
- if self.edited {
159
- classes.push("edited");
160
- }
79
+ #[function_component(EditableHeader)]
80
+ pub fn editable_header(props: &EditableHeaderProps) -> Html {
81
+ let noderef = use_node_ref();
82
+ let placeholder = props.split_placeholder();
83
+ let edited = props.value != props.initial_value;
84
+ let valid = props.is_valid(&props.value, &placeholder);
85
+ let mut classes = classes!("sidebar_header_contents");
86
+ if props.editable {
87
+ classes.push("editable");
88
+ }
161
89
 
162
- let onkeyup = ctx.link().callback(|e: KeyboardEvent| {
163
- let value = e.target_unchecked_into::<HtmlInputElement>().value();
164
- EditableHeaderMsg::SetNewValue(value)
165
- });
90
+ if !valid {
91
+ classes.push("invalid");
92
+ }
166
93
 
167
- let onblur = ctx.link().callback(|e: FocusEvent| {
168
- let value = e.target_unchecked_into::<HtmlInputElement>().value();
169
- EditableHeaderMsg::SetNewValue(value)
170
- });
94
+ if edited {
95
+ classes.push("edited");
96
+ }
171
97
 
172
- let update_on_input = ctx.props().update_on_input;
173
- let oninput = ctx.link().batch_callback(move |e: yew::InputEvent| {
98
+ let on_value = {
99
+ let props = props.clone();
100
+ let placeholder = placeholder.clone();
101
+ Callback::from(move |value: String| {
102
+ let value = (!value.is_empty()).then_some(value);
103
+ let valid = props.is_valid(&value, &placeholder);
104
+ props.on_change.emit((value, valid));
105
+ })
106
+ };
107
+
108
+ let onkeyup =
109
+ on_value.reform(|e: KeyboardEvent| e.target_unchecked_into::<HtmlInputElement>().value());
110
+ let onblur =
111
+ on_value.reform(|e: FocusEvent| e.target_unchecked_into::<HtmlInputElement>().value());
112
+ let oninput = {
113
+ let update_on_input = props.update_on_input;
114
+ let on_value = on_value.clone();
115
+ Callback::from(move |e: InputEvent| {
174
116
  if update_on_input {
175
- let value = e.target_unchecked_into::<HtmlInputElement>().value();
176
- vec![EditableHeaderMsg::SetNewValue(value)]
177
- } else {
178
- vec![]
117
+ on_value.emit(e.target_unchecked_into::<HtmlInputElement>().value());
179
118
  }
180
- });
181
-
182
- html! {
183
- <div class={classes} onclick={ctx.link().callback(|_| EditableHeaderMsg::OnClick(()))}>
184
- if let Some(icon) = ctx.props().icon_type { <TypeIcon ty={icon} /> }
185
- <input
186
- ref={self.noderef.clone()}
187
- type="search"
188
- class="sidebar_header_title"
189
- disabled={!ctx.props().editable}
190
- {onblur}
191
- {onkeyup}
192
- {oninput}
193
- value={self.value.clone()}
194
- placeholder={self.placeholder.clone()}
195
- />
196
- </div>
197
- }
119
+ })
120
+ };
121
+
122
+ let onclick = {
123
+ let noderef = noderef.clone();
124
+ Callback::from(move |_: MouseEvent| {
125
+ if let Some(input) = noderef.cast::<HtmlInputElement>() {
126
+ let _ = input.focus();
127
+ }
128
+ })
129
+ };
130
+
131
+ html! {
132
+ <div class={classes} {onclick}>
133
+ if let Some(icon) = props.icon_type { <TypeIcon ty={icon} /> }
134
+ <input
135
+ ref={noderef}
136
+ type="search"
137
+ class="sidebar_header_title"
138
+ disabled={!props.editable}
139
+ {onblur}
140
+ {onkeyup}
141
+ {oninput}
142
+ value={props.value.clone()}
143
+ {placeholder}
144
+ />
145
+ </div>
198
146
  }
199
147
  }
200
-
201
- #[derive(Default, Debug, PartialEq, Copy, Clone)]
202
- pub enum ValueState {
203
- #[default]
204
- Unedited,
205
- Edited,
206
- }
@@ -16,7 +16,7 @@ use perspective_client::{ExprValidationError, clone};
16
16
  use yew::prelude::*;
17
17
 
18
18
  use super::form::code_editor::*;
19
- use crate::session::{Session, SessionMetadata, SessionMetadataRc};
19
+ use crate::session::{Session, SessionMetadataRc};
20
20
  use crate::tasks::{ExprValidation, validate_expression};
21
21
 
22
22
  #[derive(Properties, PartialEq, Clone)]
@@ -27,6 +27,9 @@ pub struct ExpressionEditorProps {
27
27
  pub alias: Option<String>,
28
28
  pub disabled: bool,
29
29
 
30
+ /// The saved expression text this editor edits against.
31
+ pub initial_expr: Rc<String>,
32
+
30
33
  #[prop_or_default]
31
34
  pub reset_count: u8,
32
35
 
@@ -69,7 +72,7 @@ impl Component for ExpressionEditor {
69
72
 
70
73
  fn create(ctx: &Context<Self>) -> Self {
71
74
  let oninput = ctx.link().callback(ExpressionEditorMsg::SetExpr);
72
- let expr = initial_expr(&ctx.props().metadata, &ctx.props().alias);
75
+ let expr = ctx.props().initial_expr.clone();
73
76
  ctx.link()
74
77
  .send_message(Self::Message::SetExpr(expr.clone()));
75
78
 
@@ -156,24 +159,14 @@ impl Component for ExpressionEditor {
156
159
  fn changed(&mut self, ctx: &Context<Self>, old_props: &Self::Properties) -> bool {
157
160
  if ctx.props().alias != old_props.alias
158
161
  || ctx.props().reset_count != old_props.reset_count
159
- || (ctx.props().alias.is_some() && ctx.props().metadata != old_props.metadata)
162
+ || ctx.props().initial_expr != old_props.initial_expr
160
163
  {
161
- ctx.link()
162
- .send_message(ExpressionEditorMsg::SetExpr(initial_expr(
163
- &ctx.props().metadata,
164
- &ctx.props().alias,
165
- )));
164
+ ctx.link().send_message(ExpressionEditorMsg::SetExpr(
165
+ ctx.props().initial_expr.clone(),
166
+ ));
166
167
  false
167
168
  } else {
168
169
  true
169
170
  }
170
171
  }
171
172
  }
172
-
173
- fn initial_expr(metadata: &SessionMetadata, alias: &Option<String>) -> Rc<String> {
174
- alias
175
- .as_ref()
176
- .and_then(|alias| metadata.get_expression_by_alias(alias))
177
- .unwrap_or_default()
178
- .into()
179
- }
@@ -45,6 +45,9 @@ pub struct CodeEditorProps {
45
45
  #[prop_or_default]
46
46
  pub error: Option<ExprValidationError>,
47
47
 
48
+ #[prop_or_default]
49
+ pub contained: bool,
50
+
48
51
  /// Selected theme name, threaded for PortalModal consumers.
49
52
  #[prop_or_default]
50
53
  pub theme: String,
@@ -165,20 +168,27 @@ pub fn code_editor(props: &CodeEditorProps) -> Html {
165
168
  ))
166
169
  .collect::<Html>();
167
170
 
168
- let class = if props.wordwrap {
169
- "wordwrap scrollable"
170
- } else {
171
- "scrollable"
171
+ let mut outer_class = classes!("scrollable");
172
+ let mut inner_class = classes!("editor-inner", "scrollable");
173
+
174
+ if props.wordwrap {
175
+ outer_class.push("wordwrap");
176
+ inner_class.push("wordwrap");
172
177
  };
178
+
179
+ if props.contained {
180
+ outer_class.push("contained")
181
+ }
182
+
173
183
  clone!(props.disabled);
174
184
  html! {
175
185
  <>
176
- <div id="editor" {class}>
186
+ <div id="editor" class={outer_class}>
177
187
  <div id="line_numbers" ref={lineno_ref}>{ line_numbers }</div>
178
188
  <MirroredTextarea
179
189
  id="textarea_editable"
180
190
  mirror_id="content"
181
- class={classes!("editor-inner", class)}
191
+ class={inner_class}
182
192
  mirror={terms}
183
193
  {textarea_ref}
184
194
  mirror_ref={content_ref}
@@ -178,6 +178,7 @@ pub fn debug_panel(props: &DebugPanelProps) -> Html {
178
178
  <CodeEditor
179
179
  expr={&*expr}
180
180
  disabled=false
181
+ contained=true
181
182
  {oninput}
182
183
  {onsave}
183
184
  select_all={select_all.subscriber()}
@@ -17,14 +17,16 @@
17
17
  //! parent module directly.
18
18
 
19
19
  pub mod code_editor;
20
- pub mod color_range_selector;
21
20
  pub mod color_selector;
22
21
  pub mod debug;
23
22
  pub mod mirrored_textarea;
23
+ pub mod multi_stop_gradient_selector;
24
+ pub mod named_value_picker;
24
25
  pub mod number_field;
25
26
  pub mod number_input;
26
27
  pub mod number_range_field;
27
28
  pub mod optional_field;
29
+ pub mod palette_selector;
28
30
  pub mod select_enum_field;
29
31
  pub mod select_value_field;
30
32