@perspective-dev/viewer 5.0.0 → 5.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (213) hide show
  1. package/dist/cdn/perspective-viewer.js +9 -9
  2. package/dist/cdn/perspective-viewer.js.map +4 -4
  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/intl/de.css +1 -1
  9. package/dist/css/intl/es.css +1 -1
  10. package/dist/css/intl/fr.css +1 -1
  11. package/dist/css/intl/ja.css +1 -1
  12. package/dist/css/intl/pt.css +1 -1
  13. package/dist/css/intl/zh.css +1 -1
  14. package/dist/css/intl.css +1 -1
  15. package/dist/css/monokai.css +1 -1
  16. package/dist/css/phosphor.css +1 -1
  17. package/dist/css/pro-dark.css +1 -1
  18. package/dist/css/pro.css +1 -1
  19. package/dist/css/solarized-dark.css +1 -1
  20. package/dist/css/solarized.css +1 -1
  21. package/dist/css/themes.css +1 -1
  22. package/dist/css/vaporwave.css +1 -1
  23. package/dist/docs/perspective-docs.json +1 -0
  24. package/dist/esm/column-format.d.ts +19 -28
  25. package/dist/esm/perspective-viewer.d.ts +2 -0
  26. package/dist/esm/perspective-viewer.inline.js +8 -8
  27. package/dist/esm/perspective-viewer.inline.js.map +4 -4
  28. package/dist/esm/perspective-viewer.js +9 -9
  29. package/dist/esm/perspective-viewer.js.map +4 -4
  30. package/dist/esm/providers.d.ts +79 -0
  31. package/dist/esm/ts-rs/CompactDisplay.d.ts +5 -0
  32. package/dist/esm/ts-rs/CurrencyCode.d.ts +1 -0
  33. package/dist/esm/ts-rs/CurrencyDisplay.d.ts +1 -0
  34. package/dist/esm/ts-rs/CurrencyNumberFormatStyle.d.ts +8 -0
  35. package/dist/esm/ts-rs/CurrencySign.d.ts +1 -0
  36. package/dist/esm/ts-rs/CustomDatetimeFormat.d.ts +1 -0
  37. package/dist/esm/ts-rs/CustomDatetimeStyleConfig.d.ts +54 -0
  38. package/dist/esm/ts-rs/CustomNumberFormatConfig.d.ts +34 -0
  39. package/dist/esm/ts-rs/DatetimeColorMode.d.ts +1 -0
  40. package/dist/esm/ts-rs/DatetimeFormatType.d.ts +6 -0
  41. package/dist/esm/ts-rs/FormatMode.d.ts +1 -0
  42. package/dist/esm/ts-rs/FormatUnit.d.ts +1 -0
  43. package/dist/esm/ts-rs/Notation.d.ts +17 -0
  44. package/dist/esm/ts-rs/NumberFormatStyle.d.ts +18 -0
  45. package/dist/esm/ts-rs/PanelViewerConfig.d.ts +43 -1
  46. package/dist/esm/ts-rs/PluginStaticConfig.d.ts +35 -0
  47. package/dist/esm/ts-rs/RestoreOptions.d.ts +19 -0
  48. package/dist/esm/ts-rs/RoundingMode.d.ts +1 -0
  49. package/dist/esm/ts-rs/RoundingPriority.d.ts +1 -0
  50. package/dist/esm/ts-rs/SignDisplay.d.ts +1 -0
  51. package/dist/esm/ts-rs/SimpleDatetimeFormat.d.ts +1 -0
  52. package/dist/esm/ts-rs/SimpleDatetimeStyleConfig.d.ts +24 -0
  53. package/dist/esm/ts-rs/SplitRollupMode.d.ts +9 -0
  54. package/dist/esm/ts-rs/StringColorMode.d.ts +1 -0
  55. package/dist/esm/ts-rs/TrailingZeroDisplay.d.ts +1 -0
  56. package/dist/esm/ts-rs/Unit.d.ts +1 -0
  57. package/dist/esm/ts-rs/UnitDisplay.d.ts +1 -0
  58. package/dist/esm/ts-rs/UnitNumberFormatStyle.d.ts +6 -0
  59. package/dist/esm/ts-rs/UseGrouping.d.ts +1 -0
  60. package/dist/esm/ts-rs/ViewerConfig.d.ts +43 -1
  61. package/dist/esm/ts-rs/ViewerConfigInitial.d.ts +175 -0
  62. package/dist/esm/ts-rs/ViewerConfigUpdate.d.ts +72 -0
  63. package/dist/esm/ts-rs/WindowSort.d.ts +7 -0
  64. package/dist/esm/ts-rs/WindowSortDir.d.ts +6 -0
  65. package/dist/esm/ts-rs/WindowSpec.d.ts +47 -0
  66. package/dist/esm/ts-rs/Windows.d.ts +10 -0
  67. package/dist/esm/ts-rs/WorkspaceConfigUpdate.d.ts +7 -6
  68. package/dist/wasm/perspective-viewer.d.ts +222 -26
  69. package/dist/wasm/perspective-viewer.js +378 -125
  70. package/dist/wasm/perspective-viewer.wasm +0 -0
  71. package/dist/wasm/perspective-viewer.wasm.d.ts +18 -15
  72. package/package.json +2 -1
  73. package/src/css/_viewer_bundle.css +1 -0
  74. package/src/css/column-selector.css +9 -0
  75. package/src/css/column-settings-panel.css +67 -2
  76. package/src/css/column-style.css +4 -3
  77. package/src/css/config-selector.css +60 -4
  78. package/src/css/containers/context-menu.css +12 -6
  79. package/src/css/containers/tabs.css +2 -2
  80. package/src/css/form/code-editor.css +2 -3
  81. package/src/css/form/mirrored-textarea.css +81 -0
  82. package/src/css/viewer.css +336 -0
  83. package/src/rust/agent/client/mod.rs +19 -0
  84. package/src/rust/agent/client/protocol.rs +320 -0
  85. package/src/rust/agent/client/transport.rs +287 -0
  86. package/src/rust/agent/client/turn.rs +174 -0
  87. package/src/rust/agent/config.rs +159 -0
  88. package/src/rust/agent/docs.rs +293 -0
  89. package/src/rust/agent/markdown/block.rs +509 -0
  90. package/src/rust/agent/markdown/entity.rs +52 -0
  91. package/src/rust/agent/markdown/events.rs +71 -0
  92. package/src/rust/agent/markdown/inline.rs +497 -0
  93. package/src/rust/agent/markdown/mod.rs +466 -0
  94. package/src/rust/agent/mod.rs +23 -0
  95. package/src/rust/agent/preamble.md +16 -0
  96. package/src/rust/agent/runtime.rs +201 -0
  97. package/src/rust/agent/slot.rs +236 -0
  98. package/src/rust/agent/tools.rs +1110 -0
  99. package/src/rust/components/chat_panel.rs +289 -0
  100. package/src/rust/components/column_selector/active_column.rs +21 -27
  101. package/src/rust/components/column_selector/column_selector_column_row.rs +73 -0
  102. package/src/rust/components/column_selector/config_selector.rs +83 -8
  103. package/src/rust/components/column_selector/expr_edit_button.rs +7 -1
  104. package/src/rust/components/column_selector/inactive_column.rs +8 -1
  105. package/src/rust/components/column_selector.rs +27 -3
  106. package/src/rust/components/column_settings_sidebar/style_tab.rs +2 -1
  107. package/src/rust/components/column_settings_sidebar/window_tab.rs +34 -0
  108. package/src/rust/components/column_settings_sidebar.rs +163 -16
  109. package/src/rust/components/containers/dragdrop_list.rs +110 -3
  110. package/src/rust/components/containers/sidebar.rs +99 -22
  111. package/src/rust/components/containers/split_panel.rs +6 -0
  112. package/src/rust/components/editable_header.rs +2 -0
  113. package/src/rust/components/form/code_editor.rs +14 -23
  114. package/src/rust/components/form/debug.rs +8 -5
  115. package/src/rust/components/form/mirrored_textarea.rs +165 -0
  116. package/src/rust/components/form/mod.rs +1 -0
  117. package/src/rust/components/main_panel.rs +5 -0
  118. package/src/rust/components/mod.rs +3 -0
  119. package/src/rust/components/panel_menu.rs +7 -0
  120. package/src/rust/components/panel_tab.rs +5 -0
  121. package/src/rust/components/portal.rs +7 -0
  122. package/src/rust/components/settings_panel.rs +80 -2
  123. package/src/rust/components/viewer/msg.rs +2 -0
  124. package/src/rust/components/viewer/panels.rs +9 -14
  125. package/src/rust/components/viewer/render.rs +14 -0
  126. package/src/rust/components/viewer/settings.rs +33 -1
  127. package/src/rust/components/viewer.rs +2 -0
  128. package/src/rust/components/window_editor.rs +985 -0
  129. package/src/rust/config/datetime_column_style/custom.rs +17 -0
  130. package/src/rust/config/datetime_column_style/simple.rs +10 -0
  131. package/src/rust/config/number_string_format.rs +33 -0
  132. package/src/rust/config/options.rs +18 -0
  133. package/src/rust/config/plugin_static_config.rs +120 -5
  134. package/src/rust/config/viewer_config.rs +220 -1
  135. package/src/rust/config/workspace_config.rs +7 -6
  136. package/src/rust/custom_elements/viewer.rs +209 -18
  137. package/src/rust/custom_events.rs +29 -0
  138. package/src/rust/lib.rs +17 -1
  139. package/src/rust/presentation/column_locator.rs +16 -1
  140. package/src/rust/presentation.rs +8 -0
  141. package/src/rust/queries/columns_iter_set.rs +7 -1
  142. package/src/rust/queries/get_viewer_config.rs +5 -1
  143. package/src/rust/renderer/dispatch.rs +85 -58
  144. package/src/rust/renderer.rs +34 -1
  145. package/src/rust/session/column_defaults_update.rs +18 -0
  146. package/src/rust/session/drag_drop_update.rs +18 -0
  147. package/src/rust/session/metadata.rs +64 -9
  148. package/src/rust/session/replace_expression_update.rs +2 -0
  149. package/src/rust/session.rs +7 -5
  150. package/src/rust/tasks/create_panel.rs +18 -8
  151. package/src/rust/tasks/edit_window.rs +155 -0
  152. package/src/rust/tasks/mod.rs +2 -0
  153. package/src/rust/tasks/restore_panel.rs +34 -17
  154. package/src/rust/tasks/sync_update_panels.rs +7 -23
  155. package/src/rust/utils/browser/dragdrop.rs +12 -0
  156. package/src/rust/utils/browser/tests/debounce.rs +49 -0
  157. package/src/rust/utils/debounce.rs +76 -27
  158. package/src/rust/workspace.rs +112 -20
  159. package/src/svg/pin-icon.svg +5 -0
  160. package/src/themes/icons.css +1 -0
  161. package/src/themes/intl/de.css +23 -0
  162. package/src/themes/intl/es.css +23 -0
  163. package/src/themes/intl/fr.css +23 -0
  164. package/src/themes/intl/ja.css +23 -0
  165. package/src/themes/intl/pt.css +23 -0
  166. package/src/themes/intl/zh.css +23 -0
  167. package/src/themes/intl.css +26 -1
  168. package/src/ts/column-format.ts +67 -46
  169. package/src/ts/perspective-viewer.ts +2 -0
  170. package/src/ts/providers.ts +96 -0
  171. package/src/ts/ts-rs/CompactDisplay.ts +3 -0
  172. package/src/ts/ts-rs/CurrencyCode.ts +3 -0
  173. package/src/ts/ts-rs/CurrencyDisplay.ts +3 -0
  174. package/src/ts/ts-rs/CurrencyNumberFormatStyle.ts +6 -0
  175. package/src/ts/ts-rs/CurrencySign.ts +3 -0
  176. package/src/ts/ts-rs/CustomDatetimeFormat.ts +3 -0
  177. package/src/ts/ts-rs/CustomDatetimeStyleConfig.ts +54 -0
  178. package/src/ts/ts-rs/CustomNumberFormatConfig.ts +25 -0
  179. package/src/ts/ts-rs/DatetimeColorMode.ts +3 -0
  180. package/src/ts/ts-rs/DatetimeFormatType.ts +8 -0
  181. package/src/ts/ts-rs/FormatMode.ts +3 -0
  182. package/src/ts/ts-rs/FormatUnit.ts +3 -0
  183. package/src/ts/ts-rs/Notation.ts +11 -0
  184. package/src/ts/ts-rs/NumberFormatStyle.ts +12 -0
  185. package/src/ts/ts-rs/PanelViewerConfig.ts +48 -1
  186. package/src/ts/ts-rs/PluginStaticConfig.ts +36 -1
  187. package/src/ts/ts-rs/RestoreOptions.ts +20 -0
  188. package/src/ts/ts-rs/RoundingMode.ts +3 -0
  189. package/src/ts/ts-rs/RoundingPriority.ts +3 -0
  190. package/src/ts/ts-rs/SignDisplay.ts +3 -0
  191. package/src/ts/ts-rs/SimpleDatetimeFormat.ts +3 -0
  192. package/src/ts/ts-rs/SimpleDatetimeStyleConfig.ts +25 -0
  193. package/src/ts/ts-rs/SplitRollupMode.ts +11 -0
  194. package/src/ts/ts-rs/StringColorMode.ts +3 -0
  195. package/src/ts/ts-rs/TrailingZeroDisplay.ts +3 -0
  196. package/src/ts/ts-rs/Unit.ts +3 -0
  197. package/src/ts/ts-rs/UnitDisplay.ts +3 -0
  198. package/src/ts/ts-rs/UnitNumberFormatStyle.ts +5 -0
  199. package/src/ts/ts-rs/UseGrouping.ts +3 -0
  200. package/src/ts/ts-rs/ViewerConfig.ts +48 -1
  201. package/src/ts/ts-rs/ViewerConfigInitial.ts +164 -0
  202. package/src/ts/ts-rs/ViewerConfigUpdate.ts +81 -2
  203. package/src/ts/ts-rs/WindowSort.ts +9 -0
  204. package/src/ts/ts-rs/WindowSortDir.ts +8 -0
  205. package/src/ts/ts-rs/WindowSpec.ts +47 -0
  206. package/src/ts/ts-rs/Windows.ts +10 -0
  207. package/src/ts/ts-rs/WorkspaceConfigUpdate.ts +7 -6
  208. /package/dist/wasm/snippets/{perspective-viewer-daeef4ceb066e6c7 → perspective-viewer-02e461f3d7062b26}/inline0.js +0 -0
  209. /package/dist/wasm/snippets/{perspective-viewer-daeef4ceb066e6c7 → perspective-viewer-02e461f3d7062b26}/inline1.js +0 -0
  210. /package/dist/wasm/snippets/{perspective-viewer-daeef4ceb066e6c7 → perspective-viewer-02e461f3d7062b26}/inline2.js +0 -0
  211. /package/dist/wasm/snippets/{perspective-viewer-daeef4ceb066e6c7 → perspective-viewer-02e461f3d7062b26}/inline3.js +0 -0
  212. /package/dist/wasm/snippets/{perspective-viewer-daeef4ceb066e6c7 → perspective-viewer-02e461f3d7062b26}/inline4.js +0 -0
  213. /package/dist/wasm/snippets/{perspective-viewer-daeef4ceb066e6c7 → perspective-viewer-02e461f3d7062b26}/inline5.js +0 -0
@@ -0,0 +1,985 @@
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 std::collections::HashSet;
14
+ use std::rc::Rc;
15
+
16
+ use perspective_client::config::{ColumnType, WindowFrame, WindowSort, WindowSortDir, WindowSpec};
17
+ use wasm_bindgen::JsCast;
18
+ use web_sys::{DragEvent, HtmlInputElement, MouseEvent};
19
+ use yew::prelude::*;
20
+
21
+ use crate::components::column_dropdown::{ColumnDropDownElement, ColumnDropDownPortal};
22
+ use crate::components::column_selector::{ColumnSelectorColumnRow, InPlaceColumn, PivotColumn};
23
+ use crate::components::containers::dragdrop_list::{
24
+ DragContext, DragDropList, DragDropListItemProps,
25
+ };
26
+ use crate::components::containers::select::{Select, SelectItem};
27
+ use crate::presentation::Presentation;
28
+ use crate::session::{Session, SessionMetadataRc};
29
+ use crate::utils::{AddListener, DragEffect, DragTarget, Subscription};
30
+
31
+ /// The declared capabilities of one window aggregate, for a `source` column
32
+ /// type. Which controls an aggregate needs is the data model's to state - the
33
+ /// editor cannot infer it from a name it has never seen.
34
+ fn op_spec(
35
+ metadata: &SessionMetadataRc,
36
+ source: &str,
37
+ op: &str,
38
+ ) -> Option<perspective_client::proto::WindowAggregateArgs> {
39
+ let ty = metadata.get_column_table_type(source)?;
40
+ metadata.get_window_aggregate(ty, op)
41
+ }
42
+
43
+ /// The editor's frame-type labels, as the `frames` a declaration lists.
44
+ fn frame_label(frame: &str) -> &'static str {
45
+ match frame {
46
+ "rows" => "Rows",
47
+ "range" => "Range",
48
+ _ => "Cumulative",
49
+ }
50
+ }
51
+
52
+ fn frame_name(label: &str) -> &'static str {
53
+ match label {
54
+ "Rows" => "rows",
55
+ "Range" => "range",
56
+ _ => "cumulative",
57
+ }
58
+ }
59
+
60
+ fn is_orderable_for_range(ty: ColumnType) -> bool {
61
+ matches!(
62
+ ty,
63
+ ColumnType::Integer | ColumnType::Float | ColumnType::Date | ColumnType::Datetime
64
+ )
65
+ }
66
+
67
+ #[derive(Clone, Properties)]
68
+ pub struct WindowEditorProps {
69
+ pub metadata: SessionMetadataRc,
70
+
71
+ /// The saved spec under edit, or `None` for the "new column" drawer.
72
+ pub initial: Option<WindowSpec>,
73
+
74
+ /// Fires on every form mutation with the draft spec when it validates,
75
+ /// else `None`. The spec's `name` is a placeholder - the editable header
76
+ /// owns naming.
77
+ pub on_change: Callback<Option<WindowSpec>>,
78
+
79
+ /// Incremented by the parent to discard the draft.
80
+ pub reset_count: u8,
81
+
82
+ /// Drag/drop state - the editor's column slots are STAGED drop targets
83
+ /// ([`DragTarget::is_staged`]): drops mutate the draft only, and the
84
+ /// drag origin never self-removes.
85
+ pub presentation: Presentation,
86
+
87
+ /// Threaded for the slots' autocomplete dropdown
88
+ /// ([`ColumnDropDownElement`]).
89
+ pub session: Session,
90
+
91
+ /// Selected theme name, threaded for the autocomplete dropdown's
92
+ /// `PortalModal`.
93
+ #[prop_or_default]
94
+ pub selected_theme: Option<String>,
95
+ }
96
+
97
+ impl PartialEq for WindowEditorProps {
98
+ fn eq(&self, other: &Self) -> bool {
99
+ self.metadata == other.metadata
100
+ && self.initial == other.initial
101
+ && self.reset_count == other.reset_count
102
+ && self.selected_theme == other.selected_theme
103
+ }
104
+ }
105
+
106
+ /// Input mirror of a draft [`WindowSpec`]. Free-text fields hold raw
107
+ /// strings so invalid intermediate input is representable; the NUMBER
108
+ /// fields are typed with default `1` and can never hold an invalid value -
109
+ /// like the Style tab's `NumberField`, a non-numeric or out-of-domain
110
+ /// input resets to the default at the keystroke. `validate` produces the
111
+ /// spec (or the error shown inline), applying the same rules as `View`
112
+ /// construction so a saveable draft cannot be rejected by the engine.
113
+ #[derive(Clone, PartialEq)]
114
+ struct WindowDraft {
115
+ op: String,
116
+ source: String,
117
+ order_by: String,
118
+ order_desc: bool,
119
+ partition_by: Vec<String>,
120
+ frame_type: String,
121
+ frame_rows: u32,
122
+ frame_range: f64,
123
+ offset: u32,
124
+ alpha: f64,
125
+ }
126
+
127
+ impl Default for WindowDraft {
128
+ fn default() -> Self {
129
+ Self {
130
+ op: String::default(),
131
+ source: String::default(),
132
+ order_by: String::default(),
133
+ order_desc: false,
134
+ partition_by: vec![],
135
+ frame_type: String::default(),
136
+ frame_rows: 1,
137
+ frame_range: 1.0,
138
+ offset: 1,
139
+ alpha: 1.0,
140
+ }
141
+ }
142
+ }
143
+
144
+ impl WindowDraft {
145
+ fn from_spec(spec: &WindowSpec) -> Self {
146
+ let (frame_type, frame_rows, frame_range) = match spec.frame {
147
+ Some(WindowFrame::Rows(n)) => ("Rows", n, 1.0),
148
+ Some(WindowFrame::Range(x)) => ("Range", 1, x),
149
+ Some(WindowFrame::Cumulative) | None => ("Cumulative", 1, 1.0),
150
+ };
151
+
152
+ Self {
153
+ op: spec.aggregate.clone(),
154
+ source: spec.column.clone(),
155
+ order_by: spec
156
+ .order_by
157
+ .as_ref()
158
+ .map(|x| x.0.clone())
159
+ .unwrap_or_default(),
160
+ order_desc: spec
161
+ .order_by
162
+ .as_ref()
163
+ .map(|x| x.1 == WindowSortDir::Desc)
164
+ .unwrap_or_default(),
165
+ partition_by: spec.partition_by.clone(),
166
+ frame_type: frame_type.to_string(),
167
+ frame_rows,
168
+ frame_range,
169
+ offset: spec.offset.unwrap_or(1),
170
+ alpha: spec.alpha.unwrap_or(1.0),
171
+ }
172
+ }
173
+
174
+ fn new_default() -> Self {
175
+ Self {
176
+ op: "sum".to_string(),
177
+ frame_type: "Cumulative".to_string(),
178
+ ..Self::default()
179
+ }
180
+ }
181
+
182
+ fn validate(&self, metadata: &SessionMetadataRc) -> Result<WindowSpec, String> {
183
+ let op = self.op.clone();
184
+
185
+ // Every slot takes true `Table` columns ONLY - expression aliases
186
+ // and other window columns would create dependency cycles (and
187
+ // force delete-blocking). The slots reject non-table drops with the
188
+ // invalid-X overlay, so these are backstops for API-authored specs
189
+ // opened in the editor.
190
+ let table_column = |col: &String| {
191
+ metadata
192
+ .get_table_columns()
193
+ .into_iter()
194
+ .flatten()
195
+ .any(|x| x == col)
196
+ };
197
+
198
+ if self.source.is_empty() {
199
+ return Err("Missing Column".to_string());
200
+ }
201
+
202
+ if !table_column(&self.source) {
203
+ // TODO I should not be
204
+ return Err(format!(
205
+ "\"{}\" must be a table column to source a window",
206
+ self.source
207
+ ));
208
+ }
209
+
210
+ let source_ty = metadata
211
+ .get_column_table_type(&self.source)
212
+ .ok_or_else(|| format!("Unknown source column \"{}\"", self.source))?;
213
+
214
+ // Backstop for API-authored specs opened in the editor - the op
215
+ // menu only offers the feature-declared set, so this is
216
+ // unreachable from the UI. The declaration also supplies the
217
+ // controls this op takes, below.
218
+ let declared = metadata
219
+ .get_window_aggregate(source_ty, &op)
220
+ .ok_or_else(|| {
221
+ format!("\"{op}\" is not a supported window aggregate for this column")
222
+ })?;
223
+
224
+ // An EMPTY order slot is valid when the backend has a natural row
225
+ // order to fall back on (primary key order in the engine, `rowid`
226
+ // for SQL virtual servers); UNORDERED stores (`Features`) require
227
+ // an explicit order column.
228
+ let order_ty = if self.order_by.is_empty() {
229
+ if metadata
230
+ .get_features()
231
+ .map(|x| x.unordered)
232
+ .unwrap_or_default()
233
+ {
234
+ return Err("Missing Order By".to_string());
235
+ }
236
+
237
+ None
238
+ } else {
239
+ if !table_column(&self.order_by) {
240
+ // TODO I should not be
241
+ return Err(format!(
242
+ "\"{}\" must be a table column to order by",
243
+ self.order_by
244
+ ));
245
+ }
246
+
247
+ Some(
248
+ metadata
249
+ .get_column_table_type(&self.order_by)
250
+ .ok_or_else(|| format!("Unknown order by column \"{}\"", self.order_by))?,
251
+ )
252
+ };
253
+
254
+ for col in self.partition_by.iter() {
255
+ // TODO I should not be
256
+ if !table_column(col) {
257
+ return Err(format!(
258
+ "\"{}\" must be a table column to partition by",
259
+ col
260
+ ));
261
+ }
262
+ }
263
+
264
+ // The numeric fields are typed and input-clamped to their domains,
265
+ // so no parse or range errors are reachable here.
266
+ let frame = if declared.frames.is_empty() {
267
+ None
268
+ } else {
269
+ let chosen = frame_name(&self.frame_type);
270
+ if !declared.frames.iter().any(|x| x == chosen) {
271
+ return Err(format!("\"{op}\" does not support a {chosen} frame"));
272
+ }
273
+
274
+ match self.frame_type.as_str() {
275
+ "Rows" => Some(WindowFrame::Rows(self.frame_rows)),
276
+ "Range" => {
277
+ // The natural-order fallback has no units, so `range`
278
+ // frames require an explicit order column.
279
+ let Some(order_ty) = order_ty else {
280
+ return Err("Range frames require an order by column".to_string());
281
+ };
282
+
283
+ if !is_orderable_for_range(order_ty) {
284
+ return Err("Range frames require a numeric, date or datetime order by \
285
+ column"
286
+ .to_string());
287
+ }
288
+
289
+ Some(WindowFrame::Range(self.frame_range))
290
+ },
291
+ _ => None,
292
+ }
293
+ };
294
+
295
+ // Emit `None` at the engine default so a spec saved without an
296
+ // explicit `offset` round-trips unchanged (the name-stripped
297
+ // change-detection baseline compares specs structurally).
298
+ let offset = (declared.offset && self.offset != 1).then_some(self.offset);
299
+ let alpha = declared.alpha.then_some(self.alpha);
300
+
301
+ let mut partition_by = self.partition_by.clone();
302
+ partition_by.retain(|col| !col.is_empty());
303
+
304
+ Ok(WindowSpec {
305
+ column: self.source.clone(),
306
+ aggregate: op,
307
+ partition_by,
308
+ order_by: (!self.order_by.is_empty()).then(|| {
309
+ WindowSort(
310
+ self.order_by.clone(),
311
+ if self.order_desc {
312
+ WindowSortDir::Desc
313
+ } else {
314
+ WindowSortDir::Asc
315
+ },
316
+ )
317
+ }),
318
+ frame,
319
+ offset,
320
+ alpha,
321
+ })
322
+ }
323
+ }
324
+
325
+ #[derive(Clone, Debug)]
326
+ pub enum WindowEditorMsg {
327
+ SetOp(String),
328
+ ClearSource,
329
+ ClearOrderBy,
330
+ ToggleOrderDir,
331
+ RemovePartition(usize),
332
+ SetFrameType(String),
333
+ /// Number-field messages carry the input's `value_as_number` (`NaN` for
334
+ /// empty or unparseable text); the handlers clamp to each field's
335
+ /// domain, resetting to the default `1` on invalid input.
336
+ SetFrameRows(f64),
337
+ SetFrameRange(f64),
338
+ SetOffset(f64),
339
+ SetAlpha(f64),
340
+ /// A completed drop concerning this editor: a staged target to fill,
341
+ /// and/or the staged origin slot (`DragEffect::Move` from a slot pill)
342
+ /// to clear.
343
+ Drop(String, DragTarget, Option<DragTarget>),
344
+ New(DragTarget, InPlaceColumn),
345
+ DragEnter(DragTarget, usize),
346
+ DragLeave(DragTarget),
347
+ }
348
+
349
+ /// [`DragContext`] bindings routing one [`DragDropList`] per staged target
350
+ /// into [`WindowEditorMsg`]s - the same composition the config selector
351
+ /// uses per zone, so the slots inherit its drop previews, autocomplete
352
+ /// (`EmptyColumn`) and pill styling.
353
+ struct WindowSourceContext;
354
+ struct WindowOrderByContext;
355
+ struct WindowPartitionByContext;
356
+
357
+ impl DragContext<WindowEditorMsg> for WindowSourceContext {
358
+ fn close(_index: usize) -> WindowEditorMsg {
359
+ WindowEditorMsg::ClearSource
360
+ }
361
+
362
+ fn dragenter(index: usize) -> WindowEditorMsg {
363
+ WindowEditorMsg::DragEnter(DragTarget::WindowSource, index)
364
+ }
365
+
366
+ fn dragleave() -> WindowEditorMsg {
367
+ WindowEditorMsg::DragLeave(DragTarget::WindowSource)
368
+ }
369
+
370
+ fn create(col: InPlaceColumn) -> WindowEditorMsg {
371
+ WindowEditorMsg::New(DragTarget::WindowSource, col)
372
+ }
373
+
374
+ fn is_self_move(effect: DragTarget) -> bool {
375
+ effect == DragTarget::WindowSource
376
+ }
377
+ }
378
+
379
+ impl DragContext<WindowEditorMsg> for WindowOrderByContext {
380
+ fn close(_index: usize) -> WindowEditorMsg {
381
+ WindowEditorMsg::ClearOrderBy
382
+ }
383
+
384
+ fn dragenter(index: usize) -> WindowEditorMsg {
385
+ WindowEditorMsg::DragEnter(DragTarget::WindowOrderBy, index)
386
+ }
387
+
388
+ fn dragleave() -> WindowEditorMsg {
389
+ WindowEditorMsg::DragLeave(DragTarget::WindowOrderBy)
390
+ }
391
+
392
+ fn create(col: InPlaceColumn) -> WindowEditorMsg {
393
+ WindowEditorMsg::New(DragTarget::WindowOrderBy, col)
394
+ }
395
+
396
+ fn is_self_move(effect: DragTarget) -> bool {
397
+ effect == DragTarget::WindowOrderBy
398
+ }
399
+ }
400
+
401
+ impl DragContext<WindowEditorMsg> for WindowPartitionByContext {
402
+ fn close(index: usize) -> WindowEditorMsg {
403
+ WindowEditorMsg::RemovePartition(index)
404
+ }
405
+
406
+ fn dragenter(index: usize) -> WindowEditorMsg {
407
+ WindowEditorMsg::DragEnter(DragTarget::WindowPartitionBy, index)
408
+ }
409
+
410
+ fn dragleave() -> WindowEditorMsg {
411
+ WindowEditorMsg::DragLeave(DragTarget::WindowPartitionBy)
412
+ }
413
+
414
+ fn create(col: InPlaceColumn) -> WindowEditorMsg {
415
+ WindowEditorMsg::New(DragTarget::WindowPartitionBy, col)
416
+ }
417
+
418
+ fn is_self_move(effect: DragTarget) -> bool {
419
+ effect == DragTarget::WindowPartitionBy
420
+ }
421
+ }
422
+
423
+ /// A slot's filled state: the shared `ColumnSelectorColumnRow`, with the
424
+ /// window `op` selector bound into its aggregate slot for the SOURCE slot -
425
+ /// the ONLY op control in the UI. The pill is a drag ORIGIN with
426
+ /// `DragEffect::Move(action)` (the same shape as `PivotColumn`) - the open
427
+ /// [`WindowEditor`] consumes the staged origin from `drop_received` to
428
+ /// complete the move by clearing this slot in its draft.
429
+ #[derive(Clone, Properties)]
430
+ pub struct WindowSlotColumnProps {
431
+ pub column: String,
432
+ pub column_type: Option<ColumnType>,
433
+
434
+ /// The staged slot this pill occupies - its drag origin.
435
+ pub action: DragTarget,
436
+
437
+ pub presentation: Presentation,
438
+
439
+ #[prop_or_default]
440
+ pub aggregate: Option<Html>,
441
+
442
+ /// Trailing affordance in the row (the order slot's staged sort-dir
443
+ /// toggle).
444
+ #[prop_or_default]
445
+ pub trailing: Html,
446
+ }
447
+
448
+ impl PartialEq for WindowSlotColumnProps {
449
+ fn eq(&self, other: &Self) -> bool {
450
+ self.column == other.column
451
+ && self.column_type == other.column_type
452
+ && self.action == other.action
453
+ && self.aggregate == other.aggregate
454
+ && self.trailing == other.trailing
455
+ }
456
+ }
457
+
458
+ impl DragDropListItemProps for WindowSlotColumnProps {
459
+ type Item = String;
460
+
461
+ fn get_item(&self) -> String {
462
+ self.column.clone()
463
+ }
464
+ }
465
+
466
+ pub struct WindowSlotColumn;
467
+
468
+ impl Component for WindowSlotColumn {
469
+ type Message = ();
470
+ type Properties = WindowSlotColumnProps;
471
+
472
+ fn create(_ctx: &Context<Self>) -> Self {
473
+ Self
474
+ }
475
+
476
+ fn view(&self, ctx: &Context<Self>) -> Html {
477
+ let dragstart = Callback::from({
478
+ let column = ctx.props().column.clone();
479
+ let presentation = ctx.props().presentation.clone();
480
+ let action = ctx.props().action;
481
+ move |event: DragEvent| {
482
+ presentation.set_drag_image(&event).unwrap();
483
+ presentation.notify_drag_start(column.clone(), DragEffect::Move(action))
484
+ }
485
+ });
486
+
487
+ let dragend = Callback::from({
488
+ let presentation = ctx.props().presentation.clone();
489
+ move |_: DragEvent| presentation.notify_drag_end()
490
+ });
491
+
492
+ html! {
493
+ <div class="column-selector-column">
494
+ <ColumnSelectorColumnRow
495
+ name={ctx.props().column.clone()}
496
+ col_type={ctx.props().column_type}
497
+ aggregate={ctx.props().aggregate.clone()}
498
+ trailing={ctx.props().trailing.clone()}
499
+ wrapper_class={classes!["column-selector-column-title"]}
500
+ ondragstart={Some(dragstart)}
501
+ ondragend={Some(dragend)}
502
+ />
503
+ </div>
504
+ }
505
+ }
506
+ }
507
+
508
+ pub struct WindowEditor {
509
+ draft: WindowDraft,
510
+ error: Option<String>,
511
+ column_dropdown: ColumnDropDownElement,
512
+ _drop_sub: Subscription,
513
+ }
514
+
515
+ impl WindowEditor {
516
+ fn initialize(ctx: &Context<Self>) -> WindowDraft {
517
+ ctx.props()
518
+ .initial
519
+ .as_ref()
520
+ .map(WindowDraft::from_spec)
521
+ .unwrap_or_else(WindowDraft::new_default)
522
+ }
523
+
524
+ fn emit(&mut self, ctx: &Context<Self>) {
525
+ match self.draft.validate(&ctx.props().metadata) {
526
+ Ok(spec) => {
527
+ self.error = None;
528
+ ctx.props().on_change.emit(Some(spec));
529
+ },
530
+ Err(msg) => {
531
+ self.error = Some(msg);
532
+ ctx.props().on_change.emit(None);
533
+ },
534
+ }
535
+ }
536
+ }
537
+
538
+ impl Component for WindowEditor {
539
+ type Message = WindowEditorMsg;
540
+ type Properties = WindowEditorProps;
541
+
542
+ fn create(ctx: &Context<Self>) -> Self {
543
+ let _drop_sub = {
544
+ let link = ctx.link().clone();
545
+ ctx.props().presentation.drop_received.add_listener(
546
+ move |(column, target, effect, _index): (String, DragTarget, DragEffect, usize)| {
547
+ let staged_origin = match effect {
548
+ DragEffect::Move(origin) if origin.is_staged() => Some(origin),
549
+ _ => None,
550
+ };
551
+
552
+ if target.is_staged() || staged_origin.is_some() {
553
+ link.send_message(WindowEditorMsg::Drop(column, target, staged_origin));
554
+ }
555
+ },
556
+ )
557
+ };
558
+
559
+ let mut this = Self {
560
+ draft: Self::initialize(ctx),
561
+ error: None,
562
+ column_dropdown: ColumnDropDownElement::new(ctx.props().session.clone()),
563
+ _drop_sub,
564
+ };
565
+
566
+ this.emit(ctx);
567
+ this
568
+ }
569
+
570
+ fn changed(&mut self, ctx: &Context<Self>, old_props: &Self::Properties) -> bool {
571
+ if ctx.props().reset_count != old_props.reset_count
572
+ || ctx.props().initial != old_props.initial
573
+ {
574
+ self.draft = Self::initialize(ctx);
575
+ self.emit(ctx);
576
+ true
577
+ } else {
578
+ false
579
+ }
580
+ }
581
+
582
+ fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
583
+ match msg {
584
+ WindowEditorMsg::DragEnter(target, index) => {
585
+ return ctx.props().presentation.notify_drag_enter(target, index);
586
+ },
587
+ WindowEditorMsg::DragLeave(target) => {
588
+ ctx.props().presentation.notify_drag_leave(target);
589
+ return true;
590
+ },
591
+ WindowEditorMsg::New(target, InPlaceColumn::Column(column)) => {
592
+ ctx.link()
593
+ .send_message(WindowEditorMsg::Drop(column, target, None));
594
+ return false;
595
+ },
596
+ WindowEditorMsg::New(_, InPlaceColumn::Expression(_)) => {
597
+ // In-place expression creation is a config-selector affordance;
598
+ // staged window slots take existing columns only.
599
+ return false;
600
+ },
601
+ WindowEditorMsg::Drop(column, target, origin) => {
602
+ // The origin slot clears FIRST so a self-move (origin ==
603
+ // target) re-inserts the same column and nets to no change.
604
+ match origin {
605
+ Some(DragTarget::WindowSource) => self.draft.source = String::default(),
606
+ Some(DragTarget::WindowOrderBy) => self.draft.order_by = String::default(),
607
+ Some(DragTarget::WindowPartitionBy) => {
608
+ self.draft.partition_by.retain(|x| x != &column)
609
+ },
610
+ _ => {},
611
+ }
612
+
613
+ // Slots take true `Table` columns ONLY - a staged drop of
614
+ // an expression or window column is rejected outright (the
615
+ // list already showed the invalid-X overlay during hover).
616
+ let is_table_column = ctx
617
+ .props()
618
+ .metadata
619
+ .get_table_columns()
620
+ .into_iter()
621
+ .flatten()
622
+ .any(|x| *x == column);
623
+
624
+ match target {
625
+ _ if target.is_staged() && !is_table_column => {},
626
+ DragTarget::WindowSource => {
627
+ self.draft.source = column;
628
+
629
+ // The op selector only offers the feature-declared
630
+ // set for the source's type, so an op orphaned by
631
+ // the new source coerces to that set's first entry.
632
+ let available = ctx
633
+ .props()
634
+ .metadata
635
+ .get_column_table_type(&self.draft.source)
636
+ .and_then(|ty| {
637
+ ctx.props()
638
+ .metadata
639
+ .get_features()
640
+ .map(|x| x.get_window_aggregates(ty))
641
+ })
642
+ .unwrap_or_default();
643
+
644
+ if !available.iter().any(|x| x.name == self.draft.op)
645
+ && let Some(first) = available.first()
646
+ {
647
+ self.draft.op = first.name.clone();
648
+ }
649
+ },
650
+ DragTarget::WindowOrderBy => self.draft.order_by = column,
651
+ DragTarget::WindowPartitionBy
652
+ if !column.is_empty() && !self.draft.partition_by.contains(&column) =>
653
+ {
654
+ self.draft.partition_by.push(column);
655
+ },
656
+ // A committing target: the config zone handles its own
657
+ // insert; only the staged origin cleanup applies here.
658
+ _ => {},
659
+ }
660
+ },
661
+ WindowEditorMsg::SetOp(op) => {
662
+ self.draft.op = op;
663
+
664
+ // Keep the frame coherent as the op changes - an op that
665
+ // does not accept the current frame kind coerces to its
666
+ // first declared one (the dropdown omits the rest).
667
+ if let Some(declared) =
668
+ op_spec(&ctx.props().metadata, &self.draft.source, &self.draft.op)
669
+ && !declared.frames.is_empty()
670
+ && !declared
671
+ .frames
672
+ .iter()
673
+ .any(|x| x == frame_name(&self.draft.frame_type))
674
+ && let Some(first) = declared.frames.first()
675
+ {
676
+ self.draft.frame_type = frame_label(first).to_string();
677
+ }
678
+ },
679
+ WindowEditorMsg::ClearSource => self.draft.source = String::default(),
680
+ WindowEditorMsg::ClearOrderBy => self.draft.order_by = String::default(),
681
+ WindowEditorMsg::ToggleOrderDir => self.draft.order_desc = !self.draft.order_desc,
682
+ WindowEditorMsg::RemovePartition(idx) => {
683
+ if idx < self.draft.partition_by.len() {
684
+ self.draft.partition_by.remove(idx);
685
+ }
686
+ },
687
+ WindowEditorMsg::SetFrameType(x) => self.draft.frame_type = x,
688
+ WindowEditorMsg::SetFrameRows(x) => {
689
+ self.draft.frame_rows = if x.is_finite() && x >= 0.0 {
690
+ x as u32
691
+ } else {
692
+ 1
693
+ };
694
+ },
695
+ WindowEditorMsg::SetFrameRange(x) => {
696
+ self.draft.frame_range = if x.is_finite() && x > 0.0 { x } else { 1.0 };
697
+ },
698
+ WindowEditorMsg::SetOffset(x) => {
699
+ self.draft.offset = if x.is_finite() && x >= 0.0 {
700
+ x as u32
701
+ } else {
702
+ 1
703
+ };
704
+ },
705
+ WindowEditorMsg::SetAlpha(x) => {
706
+ self.draft.alpha = if x.is_finite() && x > 0.0 && x <= 1.0 {
707
+ x
708
+ } else {
709
+ 1.0
710
+ };
711
+ },
712
+ }
713
+
714
+ self.emit(ctx);
715
+ true
716
+ }
717
+
718
+ fn view(&self, ctx: &Context<Self>) -> Html {
719
+ let declared = op_spec(&ctx.props().metadata, &self.draft.source, &self.draft.op);
720
+
721
+ // The op selector renders the FEATURE-DECLARED window aggregates
722
+ // for the source column's type, in the server's declared order -
723
+ // capability and type-validity both come from the data model
724
+ // (`GetFeaturesResp::window_aggregates`), not a hardcoded list.
725
+ let ops: Rc<Vec<SelectItem<String>>> = Rc::new(
726
+ ctx.props()
727
+ .metadata
728
+ .get_column_table_type(&self.draft.source)
729
+ .and_then(|ty| {
730
+ ctx.props()
731
+ .metadata
732
+ .get_features()
733
+ .map(|x| x.get_window_aggregates(ty))
734
+ })
735
+ .unwrap_or_default()
736
+ .into_iter()
737
+ .map(|x| SelectItem::Option(x.name.clone()))
738
+ .collect(),
739
+ );
740
+
741
+ // Number inputs report `value_as_number` (`NaN` for empty/garbage),
742
+ // the Style tab `NumberField` idiom - the handlers reset invalid
743
+ // values to the default.
744
+ let on_number_input = |f: fn(f64) -> WindowEditorMsg| {
745
+ ctx.link().callback(move |event: InputEvent| {
746
+ let value = event
747
+ .target()
748
+ .and_then(|t| t.dyn_into::<HtmlInputElement>().ok())
749
+ .map(|x| x.value_as_number())
750
+ .unwrap_or(f64::NAN);
751
+ f(value)
752
+ })
753
+ };
754
+
755
+ // Frame type as a dropdown; like the op selector, invalid choices
756
+ // are omitted rather than disabled - the declared `frames` are the
757
+ // menu, and `SetOp` already coerces the draft into them.
758
+ let frame_types: Rc<Vec<SelectItem<String>>> = Rc::new(
759
+ declared
760
+ .as_ref()
761
+ .map(|x| x.frames.clone())
762
+ .unwrap_or_default()
763
+ .iter()
764
+ .map(|x| SelectItem::Option(frame_label(x).to_string()))
765
+ .collect(),
766
+ );
767
+
768
+ // Slots take true `Table` columns ONLY - expression aliases and
769
+ // other window columns would create dependency cycles (and force
770
+ // delete-blocking). They join every slot's `exclude` set (removing
771
+ // them from the autocomplete dropdowns), the drop handler rejects
772
+ // them, and an invalid hover shows the X overlay below.
773
+ let non_table: HashSet<String> = ctx
774
+ .props()
775
+ .metadata
776
+ .get_expression_columns()
777
+ .chain(ctx.props().metadata.get_window_columns())
778
+ .cloned()
779
+ .collect();
780
+
781
+ let is_invalid_drag = |target: DragTarget| {
782
+ ctx.props()
783
+ .presentation
784
+ .is_dragover(target)
785
+ .map(|(_, col)| non_table.contains(&col))
786
+ .unwrap_or_default()
787
+ };
788
+
789
+ // The op selector renders in the source pill's aggregate-selector
790
+ // space - the ONLY place a window op control exists in the UI.
791
+ let source_exclude: HashSet<String> = std::iter::once(self.draft.source.clone())
792
+ .chain(non_table.iter().cloned())
793
+ .collect();
794
+
795
+ let source_list = html! {
796
+ <DragDropList<WindowEditor, WindowSlotColumn, WindowSourceContext>
797
+ name="window-source"
798
+ parent={ctx.link().clone()}
799
+ presentation={ctx.props().presentation.clone()}
800
+ column_dropdown={self.column_dropdown.clone()}
801
+ exclude={source_exclude}
802
+ is_dragover={ctx.props().presentation.is_dragover(DragTarget::WindowSource)}
803
+ is_invalid={is_invalid_drag(DragTarget::WindowSource)}
804
+ single_slot=true
805
+ >
806
+ { for (!self.draft.source.is_empty()).then(|| yew::html_nested! {
807
+ <WindowSlotColumn
808
+ column={self.draft.source.clone()}
809
+ column_type={ctx.props().metadata.get_column_table_type(&self.draft.source)}
810
+ action={DragTarget::WindowSource}
811
+ presentation={ctx.props().presentation.clone()}
812
+ aggregate={html! {
813
+ <div class="aggregate-selector-wrapper">
814
+ <Select<String>
815
+ wrapper_class="aggregate-selector"
816
+ values={ops.clone()}
817
+ selected={self.draft.op.clone()}
818
+ on_select={ctx.link().callback(WindowEditorMsg::SetOp)}
819
+ />
820
+ </div>
821
+ }}
822
+ />
823
+ }) }
824
+ </DragDropList<WindowEditor, WindowSlotColumn, WindowSourceContext>>
825
+ };
826
+
827
+ let order_exclude: HashSet<String> = std::iter::once(self.draft.order_by.clone())
828
+ .chain(non_table.iter().cloned())
829
+ .collect();
830
+ let order_list = html! {
831
+ <DragDropList<WindowEditor, WindowSlotColumn, WindowOrderByContext>
832
+ name="window-order-by"
833
+ parent={ctx.link().clone()}
834
+ presentation={ctx.props().presentation.clone()}
835
+ column_dropdown={self.column_dropdown.clone()}
836
+ exclude={order_exclude}
837
+ is_dragover={ctx.props().presentation.is_dragover(DragTarget::WindowOrderBy)}
838
+ is_invalid={is_invalid_drag(DragTarget::WindowOrderBy)}
839
+ single_slot=true
840
+ >
841
+ { for (!self.draft.order_by.is_empty()).then(|| {
842
+ // The sort pill's direction affordance, staged: the
843
+ // toggle mutates the draft only, committed by Save.
844
+ let dir = if self.draft.order_desc {
845
+ WindowSortDir::Desc
846
+ } else {
847
+ WindowSortDir::Asc
848
+ };
849
+
850
+ let onmousedown = ctx
851
+ .link()
852
+ .callback(|_: MouseEvent| WindowEditorMsg::ToggleOrderDir);
853
+
854
+ yew::html_nested! {
855
+ <WindowSlotColumn
856
+ column={self.draft.order_by.clone()}
857
+ column_type={ctx.props().metadata.get_column_table_type(&self.draft.order_by)}
858
+ action={DragTarget::WindowOrderBy}
859
+ presentation={ctx.props().presentation.clone()}
860
+ trailing={html! {
861
+ <span
862
+ class={format!("sort-icon {}", dir)}
863
+ {onmousedown}
864
+ />
865
+ }}
866
+ />
867
+ }
868
+ }) }
869
+ </DragDropList<WindowEditor, WindowSlotColumn, WindowOrderByContext>>
870
+ };
871
+
872
+ let partition_exclude: HashSet<String> = self
873
+ .draft
874
+ .partition_by
875
+ .iter()
876
+ .cloned()
877
+ .chain(non_table.iter().cloned())
878
+ .collect();
879
+ let partition_list = html! {
880
+ <DragDropList<WindowEditor, PivotColumn, WindowPartitionByContext>
881
+ name="window-partition-by"
882
+ parent={ctx.link().clone()}
883
+ presentation={ctx.props().presentation.clone()}
884
+ column_dropdown={self.column_dropdown.clone()}
885
+ exclude={partition_exclude}
886
+ is_dragover={ctx.props().presentation.is_dragover(DragTarget::WindowPartitionBy)}
887
+ is_invalid={is_invalid_drag(DragTarget::WindowPartitionBy)}
888
+ >
889
+ { for self.draft.partition_by.iter().map(|col| yew::html_nested! {
890
+ <PivotColumn
891
+ column={col.clone()}
892
+ column_type={ctx.props().metadata.get_column_table_type(col)}
893
+ action={DragTarget::WindowPartitionBy}
894
+ presentation={ctx.props().presentation.clone()}
895
+ />
896
+ }) }
897
+ </DragDropList<WindowEditor, PivotColumn, WindowPartitionByContext>>
898
+ };
899
+
900
+ let show_frame = declared
901
+ .as_ref()
902
+ .map(|x| !x.frames.is_empty())
903
+ .unwrap_or_default();
904
+ let show_offset = declared.as_ref().map(|x| x.offset).unwrap_or_default();
905
+ let show_alpha = declared.as_ref().map(|x| x.alpha).unwrap_or_default();
906
+
907
+ html! {
908
+ <>
909
+ <div id="window-editor-slots">{ source_list }{ order_list }{ partition_list }</div>
910
+ <div id="window-editor-container">
911
+ if show_frame {
912
+ <div class="column-style-label"><label id="window-frame-label" /></div>
913
+ <div class="row">
914
+ <Select<String>
915
+ id="window-frame-type"
916
+ values={frame_types}
917
+ selected={self.draft.frame_type.clone()}
918
+ on_select={ctx.link().callback(WindowEditorMsg::SetFrameType)}
919
+ />
920
+ </div>
921
+ if self.draft.frame_type == "Rows" {
922
+ <div class="row">
923
+ <input
924
+ type="number"
925
+ class="parameter"
926
+ min="0"
927
+ value={self.draft.frame_rows.to_string()}
928
+ oninput={on_number_input(WindowEditorMsg::SetFrameRows)}
929
+ />
930
+ </div>
931
+ }
932
+ if self.draft.frame_type == "Range" {
933
+ <div class="row">
934
+ <input
935
+ type="number"
936
+ class="parameter"
937
+ min="0"
938
+ value={self.draft.frame_range.to_string()}
939
+ oninput={on_number_input(WindowEditorMsg::SetFrameRange)}
940
+ />
941
+ </div>
942
+ }
943
+ }
944
+ if show_offset {
945
+ <div class="column-style-label">
946
+ <label id="window-offset-label" class="indent" />
947
+ </div>
948
+ <div class="row">
949
+ <input
950
+ type="number"
951
+ class="parameter"
952
+ min="0"
953
+ value={self.draft.offset.to_string()}
954
+ oninput={on_number_input(WindowEditorMsg::SetOffset)}
955
+ />
956
+ </div>
957
+ }
958
+ if show_alpha {
959
+ <div class="column-style-label">
960
+ <label id="window-alpha-label" class="indent" />
961
+ </div>
962
+ <div class="row">
963
+ <input
964
+ type="number"
965
+ class="parameter"
966
+ step="0.05"
967
+ min="0"
968
+ max="1"
969
+ value={self.draft.alpha.to_string()}
970
+ oninput={on_number_input(WindowEditorMsg::SetAlpha)}
971
+ />
972
+ </div>
973
+ }
974
+ if let Some(error) = &self.error {
975
+ <div class="row window-editor-error">{ error.clone() }</div>
976
+ }
977
+ </div>
978
+ <ColumnDropDownPortal
979
+ element={self.column_dropdown.clone()}
980
+ theme={ctx.props().selected_theme.clone().unwrap_or_default()}
981
+ />
982
+ </>
983
+ }
984
+ }
985
+ }