@perspective-dev/viewer 5.5.0 → 5.6.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 (201) hide show
  1. package/README.md +82 -3
  2. package/dist/cdn/perspective-viewer.js +9 -9
  3. package/dist/cdn/perspective-viewer.js.map +3 -3
  4. package/dist/css/blueprint.css +1 -1
  5. package/dist/css/botanical.css +1 -1
  6. package/dist/css/dracula.css +1 -1
  7. package/dist/css/eggplant.css +1 -1
  8. package/dist/css/gruvbox-dark.css +1 -1
  9. package/dist/css/gruvbox.css +1 -1
  10. package/dist/css/icons.css +1 -1
  11. package/dist/css/intl/ar.css +1 -1
  12. package/dist/css/intl/de.css +1 -1
  13. package/dist/css/intl/es.css +1 -1
  14. package/dist/css/intl/fr.css +1 -1
  15. package/dist/css/intl/ja.css +1 -1
  16. package/dist/css/intl/pt.css +1 -1
  17. package/dist/css/intl/zh.css +1 -1
  18. package/dist/css/intl.css +1 -1
  19. package/dist/css/ledger.css +1 -1
  20. package/dist/css/monokai.css +1 -1
  21. package/dist/css/nord.css +1 -1
  22. package/dist/css/phosphor.css +1 -1
  23. package/dist/css/pro-dark.css +1 -1
  24. package/dist/css/pro.css +1 -1
  25. package/dist/css/solarized-dark.css +1 -1
  26. package/dist/css/solarized.css +1 -1
  27. package/dist/css/themes.css +1 -1
  28. package/dist/css/vaporwave.css +1 -1
  29. package/dist/css/velvet.css +1 -1
  30. package/dist/docs/perspective-docs.json +1 -1
  31. package/dist/esm/column-format.js +2 -0
  32. package/dist/esm/column-format.js.map +7 -0
  33. package/dist/esm/extensions.d.ts +3 -13
  34. package/dist/esm/perspective-viewer.d.ts +1 -1
  35. package/dist/esm/perspective-viewer.js +9 -9
  36. package/dist/esm/perspective-viewer.js.map +3 -3
  37. package/dist/esm/plugin.d.ts +4 -1
  38. package/dist/esm/select-detail.d.ts +15 -0
  39. package/dist/esm/select-detail.js +2 -0
  40. package/dist/esm/select-detail.js.map +7 -0
  41. package/dist/esm/ts-rs/DerivedTableConfig.d.ts +35 -0
  42. package/dist/esm/ts-rs/ExportMethod.d.ts +1 -1
  43. package/dist/esm/ts-rs/PanelViewerConfig.d.ts +7 -6
  44. package/dist/esm/ts-rs/RestoreOptions.d.ts +3 -7
  45. package/dist/esm/ts-rs/TableRef.d.ts +9 -0
  46. package/dist/esm/ts-rs/ViewerConfig.d.ts +7 -6
  47. package/dist/esm/ts-rs/ViewerConfigUpdate.d.ts +7 -5
  48. package/dist/esm/ts-rs/WorkspaceConfig.d.ts +9 -1
  49. package/dist/esm/ts-rs/WorkspaceConfigUpdate.d.ts +9 -0
  50. package/dist/wasm/perspective-viewer.d.ts +126 -35
  51. package/dist/wasm/perspective-viewer.js +320 -185
  52. package/dist/wasm/perspective-viewer.wasm +0 -0
  53. package/dist/wasm/perspective-viewer.wasm.d.ts +21 -17
  54. package/package.json +33 -20
  55. package/src/css/column-selector.css +205 -50
  56. package/src/css/ui/containers/context-menu.css +12 -1
  57. package/src/css/ui/containers/dropdown-menu-items.css +29 -0
  58. package/src/css/ui/containers/scroll-panel.css +4 -1
  59. package/src/rust/agent/client/protocol.rs +0 -89
  60. package/src/rust/agent/markdown/mod.rs +0 -238
  61. package/src/rust/agent/preamble.md +1 -0
  62. package/src/rust/agent/tools.rs +2 -16
  63. package/src/rust/components/column_selector/active_column.rs +15 -5
  64. package/src/rust/components/column_selector/add_expression_button.rs +9 -26
  65. package/src/rust/components/column_selector/config_selector.rs +34 -6
  66. package/src/rust/components/column_selector/derived_control.rs +229 -0
  67. package/src/rust/components/column_selector/expr_edit_button.rs +3 -2
  68. package/src/rust/components/column_selector/filter_column.rs +0 -2
  69. package/src/rust/components/column_selector/inactive_column.rs +4 -5
  70. package/src/rust/components/column_selector/pivot_column.rs +21 -1
  71. package/src/rust/components/column_selector/sort_column.rs +4 -3
  72. package/src/rust/components/column_selector.rs +21 -1
  73. package/src/rust/components/column_settings_sidebar/save_settings.rs +5 -3
  74. package/src/rust/components/column_settings_sidebar/style_tab.rs +1 -0
  75. package/src/rust/components/column_settings_sidebar.rs +25 -19
  76. package/src/rust/components/debug_panel.rs +38 -29
  77. package/src/rust/components/dragdrop_list.rs +8 -9
  78. package/src/rust/components/export_dropdown.rs +6 -0
  79. package/src/rust/components/main_panel/msg.rs +6 -4
  80. package/src/rust/components/main_panel/reconcile.rs +6 -1
  81. package/src/rust/components/main_panel/render.rs +70 -51
  82. package/src/rust/components/main_panel/update.rs +39 -1
  83. package/src/rust/components/main_panel.rs +97 -59
  84. package/src/rust/components/new_panel_menu.rs +69 -1
  85. package/src/rust/components/panel_menu.rs +63 -12
  86. package/src/rust/components/plugin_tab.rs +3 -2
  87. package/src/rust/components/render_warning.rs +2 -1
  88. package/src/rust/components/settings_panel.rs +4 -4
  89. package/src/rust/components/status_bar.rs +80 -21
  90. package/src/rust/components/status_indicator.rs +8 -1
  91. package/src/rust/components/viewer/filters.rs +15 -3
  92. package/src/rust/components/viewer/msg.rs +8 -0
  93. package/src/rust/components/viewer/panels.rs +44 -4
  94. package/src/rust/components/viewer/render.rs +1 -0
  95. package/src/rust/components/viewer/settings.rs +0 -22
  96. package/src/rust/components/viewer/snapshots.rs +2 -2
  97. package/src/rust/components/viewer/wiring.rs +18 -1
  98. package/src/rust/components/viewer.rs +47 -27
  99. package/src/rust/components/window_editor.rs +5 -4
  100. package/src/rust/config/column_config_schema.rs +38 -377
  101. package/src/rust/config/export_method.rs +24 -0
  102. package/src/rust/config/number_string_format.rs +0 -94
  103. package/src/rust/config/options.rs +3 -7
  104. package/src/rust/config/plugin_static_config.rs +0 -42
  105. package/src/rust/config/viewer_config.rs +47 -17
  106. package/src/rust/config/workspace_config.rs +158 -128
  107. package/src/rust/custom_elements/viewer.rs +189 -129
  108. package/src/rust/custom_events.rs +33 -18
  109. package/src/rust/derived.rs +266 -0
  110. package/src/rust/exprtk/tokenize.rs +0 -54
  111. package/src/rust/js/plugin.rs +1 -1
  112. package/src/rust/lib.rs +3 -0
  113. package/src/rust/presentation.rs +23 -4
  114. package/src/rust/queries/column_locator.rs +55 -48
  115. package/src/rust/queries/get_viewer_config.rs +1 -1
  116. package/src/rust/queries/plugin_column_styles.rs +22 -23
  117. package/src/rust/renderer/dispatch.rs +2 -0
  118. package/src/rust/renderer/plugin_config.rs +652 -487
  119. package/src/rust/renderer/props.rs +4 -0
  120. package/src/rust/renderer/registry.rs +2 -6
  121. package/src/rust/renderer/state.rs +205 -0
  122. package/src/rust/renderer.rs +201 -59
  123. package/src/rust/session/metadata.rs +35 -19
  124. package/src/rust/session/op_queue.rs +430 -0
  125. package/src/rust/session/panel_state.rs +327 -0
  126. package/src/rust/session/view_subscription.rs +42 -3
  127. package/src/rust/session.rs +931 -419
  128. package/src/rust/tasks/apply_global_filters.rs +49 -35
  129. package/src/rust/tasks/auto_pause.rs +22 -12
  130. package/src/rust/tasks/copy_export.rs +72 -0
  131. package/src/rust/tasks/create_panel.rs +46 -21
  132. package/src/rust/tasks/derive.rs +312 -0
  133. package/src/rust/tasks/mod.rs +6 -2
  134. package/src/rust/tasks/pipeline.rs +152 -89
  135. package/src/rust/tasks/reset_all.rs +31 -20
  136. package/src/rust/tasks/restore_derived.rs +200 -0
  137. package/src/rust/tasks/restore_panel.rs +153 -141
  138. package/src/rust/tasks/send_column_config.rs +56 -29
  139. package/src/rust/tasks/send_plugin_config.rs +52 -23
  140. package/src/rust/tasks/set_edit_mode.rs +33 -25
  141. package/src/rust/tasks/sync_update_panels.rs +40 -13
  142. package/src/rust/tasks/table_lifecycle.rs +70 -34
  143. package/src/rust/tasks/transactional_restore.rs +387 -0
  144. package/src/rust/tasks/update_theme.rs +45 -6
  145. package/src/rust/ui/containers/scroll_panel.rs +17 -10
  146. package/src/rust/ui/containers/split_panel.rs +0 -41
  147. package/src/rust/ui/context_menu.rs +157 -18
  148. package/src/rust/ui/font_loader.rs +23 -35
  149. package/src/rust/ui/form/mirrored_textarea.rs +0 -12
  150. package/src/rust/ui/form/multi_stop_gradient_selector.rs +0 -89
  151. package/src/rust/ui/form/palette_selector.rs +0 -51
  152. package/src/rust/ui/portal.rs +4 -10
  153. package/src/rust/utils/browser/mod.rs +2 -3
  154. package/src/{ts/perspective-viewer.inline.ts → rust/utils/browser/mouse.rs} +11 -10
  155. package/src/rust/utils/browser/request_animation_frame.rs +27 -8
  156. package/src/rust/utils/css.rs +0 -360
  157. package/src/rust/utils/{tests/mod.rs → in_flight.rs} +62 -2
  158. package/src/rust/utils/local_fonts.rs +0 -36
  159. package/src/rust/utils/mod.rs +4 -3
  160. package/src/rust/utils/modal_position.rs +15 -21
  161. package/src/rust/utils/pending_effects.rs +6 -30
  162. package/src/rust/utils/{browser/tests/mod.rs → plugin_host_state.rs} +31 -1
  163. package/src/rust/utils/pubsub.rs +0 -1
  164. package/src/rust/workspace.rs +187 -140
  165. package/src/svg/derived.svg +6 -0
  166. package/src/themes/icons.css +1 -0
  167. package/src/themes/intl/ar.css +10 -0
  168. package/src/themes/intl/de.css +10 -0
  169. package/src/themes/intl/es.css +10 -0
  170. package/src/themes/intl/fr.css +10 -0
  171. package/src/themes/intl/ja.css +10 -0
  172. package/src/themes/intl/pt.css +10 -0
  173. package/src/themes/intl/zh.css +10 -0
  174. package/src/themes/intl.css +10 -0
  175. package/src/themes/monokai.css +1 -1
  176. package/src/themes/vaporwave.css +1 -1
  177. package/src/ts/extensions.ts +3 -36
  178. package/src/ts/perspective-viewer.ts +1 -1
  179. package/src/ts/plugin.ts +4 -1
  180. package/src/{rust/utils/tests/request_animation_frame.rs → ts/select-detail.ts} +32 -26
  181. package/src/ts/ts-rs/DerivedTableConfig.ts +22 -0
  182. package/src/ts/ts-rs/ExportMethod.ts +1 -1
  183. package/src/ts/ts-rs/PanelViewerConfig.ts +7 -6
  184. package/src/ts/ts-rs/RestoreOptions.ts +3 -7
  185. package/src/ts/ts-rs/TableRef.ts +9 -0
  186. package/src/ts/ts-rs/ViewerConfig.ts +7 -6
  187. package/src/ts/ts-rs/ViewerConfigUpdate.ts +7 -5
  188. package/src/ts/ts-rs/WorkspaceConfig.ts +7 -1
  189. package/src/ts/ts-rs/WorkspaceConfigUpdate.ts +7 -0
  190. package/dist/esm/perspective-viewer.inline.d.ts +0 -3
  191. package/dist/esm/perspective-viewer.inline.js +0 -21
  192. package/dist/esm/perspective-viewer.inline.js.map +0 -7
  193. package/src/rust/tasks/restore_and_render.rs +0 -107
  194. package/src/rust/utils/browser/tests/debounce.rs +0 -163
  195. package/src/rust/utils/tests/pubsub.rs +0 -95
  196. /package/dist/wasm/snippets/{perspective-viewer-ea71a3990cad35d7 → perspective-viewer-d0d525977a1acba2}/inline0.js +0 -0
  197. /package/dist/wasm/snippets/{perspective-viewer-ea71a3990cad35d7 → perspective-viewer-d0d525977a1acba2}/inline1.js +0 -0
  198. /package/dist/wasm/snippets/{perspective-viewer-ea71a3990cad35d7 → perspective-viewer-d0d525977a1acba2}/inline2.js +0 -0
  199. /package/dist/wasm/snippets/{perspective-viewer-ea71a3990cad35d7 → perspective-viewer-d0d525977a1acba2}/inline3.js +0 -0
  200. /package/dist/wasm/snippets/{perspective-viewer-ea71a3990cad35d7 → perspective-viewer-d0d525977a1acba2}/inline4.js +0 -0
  201. /package/dist/wasm/snippets/{perspective-viewer-ea71a3990cad35d7 → perspective-viewer-d0d525977a1acba2}/inline5.js +0 -0
@@ -0,0 +1,229 @@
1
+ // ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
2
+ // ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
3
+ // ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
4
+ // ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
5
+ // ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
6
+ // ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
7
+ // ┃ Copyright (c) 2017, the Perspective Authors. ┃
8
+ // ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
9
+ // ┃ This file is part of the Perspective library, distributed under the terms ┃
10
+ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11
+ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
+
13
+ use perspective_client::config::ViewConfig;
14
+ use perspective_js::utils::ApiFuture;
15
+ use yew::prelude::*;
16
+
17
+ // use crate::derived::suggest_name;
18
+ use crate::presentation::Presentation;
19
+ // use crate::queries::fetch_hosted_tables;
20
+ use crate::renderer::Renderer;
21
+ use crate::session::{Session, SessionMetadataRc};
22
+ use crate::tasks::release_derived;
23
+ // use crate::tasks::{can_derive, create_derived, release_derived};
24
+ use crate::utils::{AddListener, PtrEqRc, Subscription, left_click, read_intl_string};
25
+ use crate::workspace::Workspace;
26
+
27
+ #[derive(Clone, Properties)]
28
+ pub struct DerivedControlProps {
29
+ pub session: Session,
30
+ pub renderer: Renderer,
31
+ pub presentation: Presentation,
32
+ pub workspace: Workspace,
33
+ pub metadata: SessionMetadataRc,
34
+ pub view_config: PtrEqRc<ViewConfig>,
35
+
36
+ /// The `Table` this panel is bound to, threaded as a value prop so the
37
+ /// chain re-renders when the binding changes.
38
+ pub table: Option<String>,
39
+ }
40
+
41
+ impl PartialEq for DerivedControlProps {
42
+ fn eq(&self, other: &Self) -> bool {
43
+ self.table == other.table
44
+ && self.metadata == other.metadata
45
+ && self.view_config == other.view_config
46
+ }
47
+ }
48
+
49
+ pub enum DerivedControlMsg {
50
+ // Create,
51
+ Release(bool),
52
+
53
+ /// Some panel's table binding changed — re-render so the consumer
54
+ /// count ([`Workspace::derived_consumers`]) gating refreshes.
55
+ Refresh,
56
+ }
57
+
58
+ pub struct DerivedControl {
59
+ /// Keeps the [`Workspace::bindings_changed`] listener attached for this
60
+ /// component's lifetime.
61
+ _bindings_sub: Subscription,
62
+ }
63
+
64
+ impl Component for DerivedControl {
65
+ type Message = DerivedControlMsg;
66
+ type Properties = DerivedControlProps;
67
+
68
+ fn create(ctx: &Context<Self>) -> Self {
69
+ let link = ctx.link().clone();
70
+ let _bindings_sub = ctx
71
+ .props()
72
+ .workspace
73
+ .bindings_changed()
74
+ .add_listener(move |_| link.send_message(DerivedControlMsg::Refresh));
75
+
76
+ Self { _bindings_sub }
77
+ }
78
+
79
+ fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
80
+ match msg {
81
+ // DerivedControlMsg::Create => {
82
+ // let props = ctx.props().clone();
83
+ // ApiFuture::spawn(async move {
84
+ // let name = suggest_name(
85
+ // &fetch_hosted_tables(&props.workspace)
86
+ // .await
87
+ // .into_iter()
88
+ // .flat_map(|(_, tables)| tables)
89
+ // .collect(),
90
+ // &props.view_config,
91
+ // &props.table,
92
+ // );
93
+
94
+ // create_derived(
95
+ // &props.session,
96
+ // &props.renderer,
97
+ // &props.presentation,
98
+ // &props.workspace,
99
+ // name,
100
+ // )
101
+ // .await
102
+ // });
103
+
104
+ // false
105
+ // },
106
+ DerivedControlMsg::Release(delete) => {
107
+ let props = ctx.props().clone();
108
+ let effect = props.workspace.effects().guard();
109
+ ApiFuture::spawn(async move {
110
+ let _effect = effect;
111
+ release_derived(
112
+ &props.session,
113
+ &props.renderer,
114
+ &props.presentation,
115
+ &props.workspace,
116
+ delete,
117
+ )
118
+ .await
119
+ });
120
+
121
+ false
122
+ },
123
+ DerivedControlMsg::Refresh => true,
124
+ }
125
+ }
126
+
127
+ fn view(&self, ctx: &Context<Self>) -> Html {
128
+ let props = ctx.props();
129
+ let workspace = &props.workspace;
130
+ let bound = props.table.clone().unwrap_or_default();
131
+ let is_derived = workspace.is_derived(&bound);
132
+ // let is_pivoted =
133
+ // !props.view_config.group_by.is_empty() ||
134
+ // !props.view_config.split_by.is_empty();
135
+
136
+ // let _can_create = is_pivoted && can_derive(&props.session);
137
+ // if !is_derived && !can_create {
138
+ // return html! {};
139
+ // }
140
+
141
+ if !is_derived {
142
+ return html! {};
143
+ }
144
+
145
+ let is_snapshot = !props.metadata.get_features().is_some_and(|x| x.on_update);
146
+ let mut chain = workspace.derived_ancestors(&bound);
147
+ chain.reverse();
148
+ if is_derived {
149
+ chain.push(bound.clone());
150
+ chain.remove(0);
151
+ }
152
+
153
+ let sole_consumer = workspace.derived_consumers(&bound) == 1;
154
+ let release = left_click(
155
+ ctx.link()
156
+ .callback(|event: MouseEvent| DerivedControlMsg::Release(event.shift_key())),
157
+ );
158
+
159
+ // let create = ctx.link().callback(|_| DerivedControlMsg::Create);
160
+ // let last = chain.len().saturating_sub(1);
161
+ let viewer = props.presentation.viewer_elem();
162
+ let intl = |slug: &str, fallback: &str| {
163
+ read_intl_string(viewer, slug).unwrap_or_else(|| fallback.to_owned())
164
+ };
165
+
166
+ // let _create_title = intl("derive-table-title", "Derive a new table
167
+ // from this view");
168
+ let snapshot_title = intl(
169
+ "derived-snapshot-title",
170
+ "This data source does not stream - this table's data is fixed at the point it was \
171
+ derived.",
172
+ );
173
+
174
+ let release_title = if sole_consumer {
175
+ intl(
176
+ "derived-release-delete-title",
177
+ "Back to the parent table - shift-click to delete this one",
178
+ )
179
+ } else {
180
+ intl("derived-release-title", "Back to the parent table")
181
+ };
182
+
183
+ let last = chain.len().saturating_sub(1);
184
+ let derived_tables = chain.iter().enumerate().map(|(idx, name)| {
185
+ let is_own = is_derived && idx == last;
186
+ // let consumers = workspace.derived_consumers(name);
187
+ let mut close_class = classes!("derived_release");
188
+ if sole_consumer {
189
+ close_class.push("shift-alt-icon");
190
+ }
191
+
192
+ let mut level_class = classes!("derived_level");
193
+ if is_own {
194
+ level_class.push("is-own");
195
+ }
196
+
197
+ html! {
198
+ <div class={level_class}>
199
+ <div class="derived_label_wrapper">
200
+ <span class="derived_icon" />
201
+ <span class="derived_label">{ name }</span>
202
+ </div>
203
+ // if consumers > 1 { <span class="derived_consumers">{ consumers }</span> }
204
+ if is_snapshot {
205
+ <span class="derived_snapshot" title={snapshot_title.clone()} />
206
+ }
207
+ if is_own {
208
+ <span
209
+ class={close_class}
210
+ title={release_title.clone()}
211
+ onmousedown={release.clone()}
212
+ />
213
+ } else {
214
+ <span class="derived_release disabled" />
215
+ }
216
+ </div>
217
+ }
218
+ });
219
+
220
+ html! {
221
+ <div id="derived_stack">
222
+ // if can_create {
223
+ // <div class="derived_create" title={create_title} onmousedown={create} />
224
+ // }
225
+ { for derived_tables }
226
+ </div>
227
+ }
228
+ }
229
+ }
@@ -13,6 +13,7 @@
13
13
  use yew::prelude::*;
14
14
 
15
15
  use super::ColumnSettingsTarget;
16
+ use crate::utils::left_click;
16
17
 
17
18
  #[derive(PartialEq, Clone, Properties)]
18
19
  pub struct ExprEditButtonProps {
@@ -41,10 +42,10 @@ pub struct ExprEditButtonProps {
41
42
  /// when pressed, it opens up the expression editor side-panel.
42
43
  #[function_component]
43
44
  pub fn ExprEditButton(p: &ExprEditButtonProps) -> Html {
44
- let onmousedown = yew::use_callback(p.clone(), |_, p| {
45
+ let onmousedown = left_click(yew::use_callback(p.clone(), |_, p| {
45
46
  p.on_open_expr_panel
46
47
  .emit(ColumnSettingsTarget::Column(p.name.clone()))
47
- });
48
+ }));
48
49
 
49
50
  let class = if p.is_disabled {
50
51
  "expression-edit-button disabled"
@@ -410,8 +410,6 @@ impl FilterColumnProps {
410
410
  self.metadata.get_column_table_type(filter.column())
411
411
  }
412
412
 
413
- // Get the string value, suitable for the `value` field of a `FilterColumns`'s
414
- // `<input>`.
415
413
  fn get_filter_input(&self) -> Option<String> {
416
414
  let filter_type = self.get_current_filter_type()?;
417
415
  match (&filter_type, &self.filter.term()) {
@@ -132,9 +132,10 @@ impl Component for InactiveColumn {
132
132
  .get_column_table_type(&ctx.props().name)
133
133
  .unwrap_or(ColumnType::String);
134
134
 
135
- let add_column = ctx
136
- .link()
137
- .callback(|event: MouseEvent| InactiveColumnMsg::ActivateColumn(event.shift_key()));
135
+ let add_column =
136
+ left_click(ctx.link().callback(|event: MouseEvent| {
137
+ InactiveColumnMsg::ActivateColumn(event.shift_key())
138
+ }));
138
139
 
139
140
  let ondragend = ctx.props().ondragend.reform(|_| {});
140
141
  let ondragstart = ctx.link().callback({
@@ -210,8 +211,6 @@ impl InactiveColumnProps {
210
211
  let mut columns = self.view_config.columns.clone();
211
212
  let max_cols = self.renderer.metadata().config_column_names.len();
212
213
 
213
- // Don't treat `None` at the end of the column list as columns, we'll refill
214
- // these later
215
214
  if let Some(last_filled) = columns.iter().rposition(|x| !x.is_none()) {
216
215
  columns.truncate(last_filled + 1);
217
216
 
@@ -14,9 +14,10 @@ use perspective_client::config::ColumnType;
14
14
  use web_sys::*;
15
15
  use yew::prelude::*;
16
16
 
17
+ use super::expr_edit_button::*;
17
18
  use crate::components::dragdrop_list::*;
18
19
  use crate::components::type_icon::TypeIcon;
19
- use crate::presentation::Presentation;
20
+ use crate::presentation::{ColumnSettingsTarget, Presentation};
20
21
  use crate::session::*;
21
22
  use crate::utils::*;
22
23
 
@@ -35,6 +36,14 @@ pub struct PivotColumnProps {
35
36
  #[prop_or_default]
36
37
  pub metadata: Option<SessionMetadataRc>,
37
38
 
39
+ /// Opens this column's settings, when the host offers them.
40
+ #[prop_or_default]
41
+ pub on_open_expr_panel: Option<Callback<ColumnSettingsTarget>>,
42
+
43
+ /// Is this column's settings panel open?
44
+ #[prop_or_default]
45
+ pub is_editing: bool,
46
+
38
47
  // State
39
48
  #[prop_or_default]
40
49
  pub opt_session: Option<Session>,
@@ -46,6 +55,8 @@ impl PartialEq for PivotColumnProps {
46
55
  self.column == other.column
47
56
  && self.action == other.action
48
57
  && self.metadata == other.metadata
58
+ && self.is_editing == other.is_editing
59
+ && self.on_open_expr_panel.is_some() == other.on_open_expr_panel.is_some()
49
60
  }
50
61
  }
51
62
 
@@ -103,6 +114,15 @@ impl Component for PivotColumn {
103
114
  <span class="drag-handle icon" />
104
115
  <TypeIcon ty={col_type} />
105
116
  <span class="column_name">{ ctx.props().column.clone() }</span>
117
+ if let Some(on_open_expr_panel) = &ctx.props().on_open_expr_panel {
118
+ <ExprEditButton
119
+ name={ctx.props().column.clone()}
120
+ {on_open_expr_panel}
121
+ is_expression=false
122
+ is_disabled=false
123
+ is_editing={ctx.props().is_editing}
124
+ />
125
+ }
106
126
  </div>
107
127
  </div>
108
128
  }
@@ -96,9 +96,10 @@ impl Component for SortColumn {
96
96
  }
97
97
 
98
98
  fn view(&self, ctx: &Context<Self>) -> Html {
99
- let onclick = ctx
100
- .link()
101
- .callback(|event: MouseEvent| SortColumnMsg::SortDirClick(event.shift_key()));
99
+ let onclick = left_click(
100
+ ctx.link()
101
+ .callback(|event: MouseEvent| SortColumnMsg::SortDirClick(event.shift_key())),
102
+ );
102
103
 
103
104
  let dragstart = Callback::from({
104
105
  let event_name = ctx.props().sort.0.to_owned();
@@ -15,6 +15,7 @@ mod add_expression_button;
15
15
  mod aggregate_selector;
16
16
  mod column_selector_column_row;
17
17
  mod config_selector;
18
+ mod derived_control;
18
19
  mod empty_column;
19
20
  mod expr_edit_button;
20
21
  mod filter_column;
@@ -37,6 +38,7 @@ use yew::prelude::*;
37
38
  use self::active_column::*;
38
39
  use self::add_expression_button::AddExpressionButton;
39
40
  use self::config_selector::ConfigSelector;
41
+ use self::derived_control::DerivedControl;
40
42
  use self::inactive_column::*;
41
43
  use crate::components::column_dropdown::{ColumnDropDownElement, ColumnDropDownPortal};
42
44
  use crate::config::PluginStaticConfig;
@@ -50,6 +52,7 @@ use crate::session::*;
50
52
  use crate::tasks::apply_and_render;
51
53
  use crate::ui::{Orientation, ScrollPanel, ScrollPanelItem, SplitPanel};
52
54
  use crate::utils::*;
55
+ use crate::workspace::Workspace;
53
56
 
54
57
  #[derive(Properties)]
55
58
  pub struct ColumnSelectorProps {
@@ -83,6 +86,7 @@ pub struct ColumnSelectorProps {
83
86
  // State
84
87
  pub session: Session,
85
88
  pub renderer: Renderer,
89
+ pub workspace: Workspace,
86
90
  pub presentation: Presentation,
87
91
 
88
92
  /// Fires when this component is resized via the UI.
@@ -391,10 +395,26 @@ impl Component for ColumnSelector {
391
395
  .unwrap_or_default(),
392
396
  );
393
397
 
398
+ let derived_control = html_nested! {
399
+ <ScrollPanelItem key="derived_control" size_hint=0_f64>
400
+ <DerivedControl
401
+ session={ctx.props().session.clone()}
402
+ renderer={ctx.props().renderer.clone()}
403
+ presentation={ctx.props().presentation.clone()}
404
+ workspace={ctx.props().workspace.clone()}
405
+ metadata={ctx.props().metadata.clone()}
406
+ view_config={ctx.props().view_config.clone()}
407
+ table={ctx.props().session.get_table().map(|x| x.get_name().to_owned())}
408
+ />
409
+ </ScrollPanelItem>
410
+ };
411
+
394
412
  let config_selector = html_nested! {
395
413
  <ScrollPanelItem key="config_selector" {size_hint}>
396
414
  <ConfigSelector
397
415
  onselect={onselect.clone()}
416
+ on_open_expr_panel={ctx.props().on_open_expr_panel.clone()}
417
+ selected_column={ctx.props().selected_column.clone()}
398
418
  ondragenter={ctx.link().callback(|()| Redraw)}
399
419
  view_config={ctx.props().view_config.clone()}
400
420
  drag_column={ctx.props().drag_column.clone()}
@@ -570,7 +590,7 @@ impl Component for ColumnSelector {
570
590
  drop={ondrop}
571
591
  on_resize={&ctx.props().on_resize}
572
592
  on_dimensions_reset={&self.on_reset}
573
- children={std::iter::once(config_selector).chain(active_columns).collect::<Vec<_>>()}
593
+ children={std::iter::once(derived_control).chain(std::iter::once(config_selector)).chain(active_columns).collect::<Vec<_>>()}
574
594
  />
575
595
  </div>
576
596
  }];
@@ -12,6 +12,8 @@
12
12
 
13
13
  use yew::{Callback, Html, Properties, function_component, html};
14
14
 
15
+ use crate::utils::left_click;
16
+
15
17
  #[derive(Properties, PartialEq, Clone)]
16
18
  pub struct SaveSettingsProps {
17
19
  pub save_enabled: bool,
@@ -26,9 +28,9 @@ pub struct SaveSettingsProps {
26
28
 
27
29
  #[function_component(SaveSettings)]
28
30
  pub fn save_settings(props: &SaveSettingsProps) -> Html {
29
- let reset = props.on_reset.reform(|_| ());
30
- let save = props.on_save.reform(|_| ());
31
- let delete = props.on_delete.reform(|_| ());
31
+ let reset = left_click(props.on_reset.reform(|_| ()));
32
+ let save = left_click(props.on_save.reform(|_| ()));
33
+ let delete = left_click(props.on_delete.reform(|_| ()));
32
34
  html! {
33
35
  <div id="save-settings-wrapper">
34
36
  if props.show_danger_zone {
@@ -471,6 +471,7 @@ fn render_leaf(spec: ControlSpec, keys: &[String], ctx: &FieldRenderCtx) -> Opti
471
471
  max,
472
472
  step,
473
473
  include,
474
+ ..
474
475
  } => {
475
476
  let current = raw_config
476
477
  .as_ref()
@@ -34,6 +34,7 @@ use crate::components::expression_editor::ExpressionEditorProps;
34
34
  use crate::components::type_icon::TypeIconType;
35
35
  use crate::components::window_editor::WindowEditorProps;
36
36
  use crate::presentation::{ColumnLocator, ColumnSettingsTab, Presentation};
37
+ use crate::queries::{has_column_config, is_pivot_only_column};
37
38
  use crate::renderer::Renderer;
38
39
  use crate::session::{Session, SessionMetadataRc};
39
40
  use crate::tasks::{
@@ -218,7 +219,9 @@ impl Component for ColumnSettingsPanel {
218
219
 
219
220
  fn changed(&mut self, ctx: &yew::prelude::Context<Self>, old_props: &Self::Properties) -> bool {
220
221
  let next = Initials::of(ctx.props());
221
- if ctx.props().selected_column != old_props.selected_column || next != self.initials() {
222
+ if ctx.props().selected_column != old_props.selected_column
223
+ || next != Initials::of(old_props)
224
+ {
222
225
  self.reset_to(ctx, next);
223
226
  } else {
224
227
  self.refresh_derived(ctx);
@@ -541,33 +544,36 @@ impl ColumnSettingsPanel {
541
544
  self.save_enabled = changed && valid;
542
545
  }
543
546
 
544
- fn initials(&self) -> Initials {
545
- Initials {
546
- column_name: self.column_name.clone(),
547
- expr: self.initial_expr_value.clone(),
548
- header: self.initial_header_value.clone(),
549
- window: self.initial_window_value.clone(),
550
- }
551
- }
552
-
553
547
  fn refresh_derived(&mut self, ctx: &yew::prelude::Context<Self>) {
554
- self.maybe_ty = ctx
548
+ let view_ty = ctx
555
549
  .props()
556
550
  .metadata
557
551
  .locator_view_type(&ctx.props().selected_column);
558
552
 
553
+ self.maybe_ty = match ctx.props().selected_column.name() {
554
+ Some(name) if is_pivot_only_column(name, &ctx.props().view_config) => {
555
+ ctx.props().metadata.get_column_table_type(name).or(view_ty)
556
+ },
557
+ _ => view_ty,
558
+ };
559
+
559
560
  self.tabs = {
560
561
  let mut tabs = vec![];
561
562
  let is_new_expr = ctx.props().selected_column.is_new_expr();
563
+ let selected_name = ctx
564
+ .props()
565
+ .selected_column
566
+ .name()
567
+ .map(|x| x.to_string())
568
+ .unwrap_or_default();
569
+
562
570
  let show_styles = !is_new_expr
563
- && ctx.props().renderer.can_render_column_styles()
564
- && ctx.props().view_config.columns.contains(&Some(
565
- ctx.props()
566
- .selected_column
567
- .name()
568
- .map(|x| x.to_string())
569
- .unwrap_or_default(),
570
- ));
571
+ && has_column_config(
572
+ &selected_name,
573
+ &ctx.props().view_config,
574
+ &ctx.props().metadata,
575
+ &ctx.props().renderer,
576
+ );
571
577
 
572
578
  if !is_new_expr && show_styles {
573
579
  tabs.push(ColumnSettingsTab::Style);
@@ -22,13 +22,12 @@ use crate::components::code_editor::CodeEditor;
22
22
  use crate::config::*;
23
23
  use crate::js::{MimeType, copy_to_clipboard, paste_from_clipboard};
24
24
  use crate::presentation::*;
25
- use crate::queries::fetch_hosted_tables;
26
25
  use crate::renderer::*;
27
26
  use crate::session::*;
28
27
  use crate::utils::*;
29
28
  use crate::workspace::Workspace;
30
29
 
31
- #[derive(Clone, PartialEq, Properties)]
30
+ #[derive(Clone, Properties)]
32
31
  pub struct DebugPanelProps {
33
32
  pub presentation: Presentation,
34
33
  pub renderer: Renderer,
@@ -47,9 +46,16 @@ pub struct DebugPanelProps {
47
46
  pub on_auto_width: Callback<f64>,
48
47
  }
49
48
 
49
+ impl PartialEq for DebugPanelProps {
50
+ fn eq(&self, rhs: &Self) -> bool {
51
+ self.initial_width == rhs.initial_width
52
+ }
53
+ }
54
+
50
55
  /// Whether the editor holds text the panel has not successfully applied, read
51
- /// synchronously by the config-change listeners so a failed apply's own
52
- /// `view_config_changed` never discards it.
56
+ /// synchronously by the config-change listeners and again when a
57
+ /// [`DebugPanelProps::set_text`] read lands, so neither a failed apply's own
58
+ /// `view_config_changed` nor an in-flight config read discards it.
53
59
  type Dirty = Rc<Cell<bool>>;
54
60
 
55
61
  #[function_component(DebugPanel)]
@@ -72,10 +78,12 @@ pub fn debug_panel(props: &DebugPanelProps) -> Html {
72
78
  }
73
79
  });
74
80
 
75
- use_effect_with((expr.setter(), props.clone()), {
81
+ use_effect_with((), {
76
82
  clone!(error, modified, dirty);
77
- move |(text, state)| {
78
- state.set_text(text.clone());
83
+ let state = props.clone();
84
+ let text = expr.setter();
85
+ move |()| {
86
+ state.set_text(text.clone(), dirty.clone());
79
87
  error.set(None);
80
88
  let sub1 = state
81
89
  .renderer
@@ -154,7 +162,7 @@ pub fn debug_panel(props: &DebugPanelProps) -> Html {
154
162
  clone!(modified, dirty);
155
163
  move |_, (text, error, props)| {
156
164
  dirty.set(false);
157
- props.set_text(text.clone());
165
+ props.set_text(text.clone(), dirty.clone());
158
166
  error.set(None);
159
167
  modified.set(false);
160
168
  }
@@ -214,7 +222,9 @@ pub fn debug_panel(props: &DebugPanelProps) -> Html {
214
222
  }
215
223
 
216
224
  impl DebugPanelProps {
217
- fn set_text(&self, setter: UseStateSetter<Rc<String>>) {
225
+ /// Asynchronously read this panel's config into the editor, unless the
226
+ /// editor has unapplied text by the time the read lands.
227
+ fn set_text(&self, setter: UseStateSetter<Rc<String>>, dirty: Dirty) {
218
228
  let props = self.clone();
219
229
  ApiFuture::spawn(async move {
220
230
  let config = crate::queries::get_viewer_config(
@@ -222,12 +232,16 @@ impl DebugPanelProps {
222
232
  &props.renderer,
223
233
  &props.presentation,
224
234
  )
225
- .await?;
235
+ .await?
236
+ .map_table(|name| props.workspace.inline_table_ref(&name));
226
237
  let json = JsValue::from_serde_ext(&config)?;
227
238
  let js_string =
228
239
  js_sys::JSON::stringify_with_replacer_and_space(&json, &JsValue::NULL, &2.into())?;
229
240
 
230
- setter.set(Rc::new(js_string.as_string().unwrap()));
241
+ if !dirty.get() {
242
+ setter.set(Rc::new(js_string.as_string().unwrap()));
243
+ }
244
+
231
245
  Ok(())
232
246
  });
233
247
  }
@@ -246,7 +260,7 @@ impl DebugPanelProps {
246
260
  }
247
261
 
248
262
  error.set(None);
249
- props.set_text(text.clone());
263
+ props.set_text(text.clone(), dirty.clone());
250
264
  modified.set(false);
251
265
  }
252
266
  }
@@ -283,21 +297,6 @@ impl DebugPanelProps {
283
297
  },
284
298
  };
285
299
 
286
- if let OptionalUpdate::Update(name) = &config.table {
287
- let hosted = fetch_hosted_tables(&props.workspace).await;
288
- if !hosted
289
- .iter()
290
- .any(|(_, names)| names.iter().any(|n| n == name))
291
- {
292
- fail(
293
- format!("Unknown table \"{name}\""),
294
- locate_key(&source, "table"),
295
- );
296
-
297
- return Ok(());
298
- }
299
- }
300
-
301
300
  let active = props.workspace.active_renderer().as_ref() == Some(&props.renderer);
302
301
  let result = crate::tasks::restore_panel(
303
302
  &props.session,
@@ -308,6 +307,7 @@ impl DebugPanelProps {
308
307
  config,
309
308
  crate::tasks::RestoreErrors::Suppress,
310
309
  MissingTable::Error,
310
+ None,
311
311
  )
312
312
  .await;
313
313
 
@@ -316,9 +316,18 @@ impl DebugPanelProps {
316
316
  dirty.set(false);
317
317
  error.set(None);
318
318
  modified.set(false);
319
- props.set_text(text);
319
+ props.set_text(text, dirty.clone());
320
+ },
321
+ Err(e) => {
322
+ let message = format!("{e}");
323
+ let position = if message.starts_with("Unknown table") {
324
+ locate_key(&source, "table")
325
+ } else {
326
+ (0, 0)
327
+ };
328
+
329
+ fail(message, position)
320
330
  },
321
- Err(e) => fail(format!("{e}"), (0, 0)),
322
331
  }
323
332
 
324
333
  Ok(())