1use serde::{Deserialize, Serialize};
7
8use super::model::{GridColumn, SortDirection, Summary};
9use super::query::GridQuery;
10
11#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
13#[serde(default)]
14#[non_exhaustive]
15#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
16pub struct ColumnLayout {
17 pub title: String,
19 pub width: f32,
21 pub hidden: bool,
23 pub pinned: bool,
25}
26
27#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
29#[serde(default)]
30#[non_exhaustive]
31#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
32pub struct SortLayout {
33 pub title: String,
35 pub descending: bool,
37}
38
39#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
41#[serde(default)]
42#[non_exhaustive]
43#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
44pub struct FilterLayout {
45 pub title: String,
47 pub text: String,
49}
50
51#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
53#[serde(default)]
54#[non_exhaustive]
55#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
56pub struct SummaryLayout {
57 pub title: String,
59 pub kind: String,
61 pub formula: Option<String>,
63}
64
65fn summary_name(summary: Summary) -> &'static str {
66 match summary {
67 Summary::Count => "count",
68 Summary::Average => "average",
69 Summary::Min => "min",
70 Summary::Max => "max",
71 _ => "sum",
72 }
73}
74
75fn summary_from(name: &str) -> Option<Summary> {
76 match name {
77 "count" => Some(Summary::Count),
78 "sum" => Some(Summary::Sum),
79 "average" => Some(Summary::Average),
80 "min" => Some(Summary::Min),
81 "max" => Some(Summary::Max),
82 _ => None,
83 }
84}
85
86#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
96#[serde(default)]
97#[non_exhaustive]
98#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
99pub struct GridLayout {
100 pub columns: Vec<ColumnLayout>,
102 pub sort: Vec<SortLayout>,
104 pub group_by: Vec<String>,
106 pub filters: Vec<FilterLayout>,
108 pub search: String,
110 pub group_panel: bool,
112 pub page_size: Option<usize>,
114 pub summaries: Vec<SummaryLayout>,
116}
117
118impl Default for GridLayout {
119 fn default() -> Self {
120 Self {
121 columns: Vec::new(),
122 sort: Vec::new(),
123 group_by: Vec::new(),
124 filters: Vec::new(),
125 search: String::new(),
126 group_panel: true,
127 page_size: None,
128 summaries: Vec::new(),
129 }
130 }
131}
132
133#[cfg_attr(feature = "live", ravenrs_live::expose)]
134impl GridLayout {
135 pub fn to_json(&self) -> String {
137 serde_json::to_string(self).unwrap_or_default()
138 }
139
140 pub fn from_json(text: &str) -> Result<Self, String> {
143 serde_json::from_str(text).map_err(|e| e.to_string())
144 }
145}
146
147#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
149#[non_exhaustive]
150#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
151pub struct GridScenario {
152 pub name: String,
154 pub layout: GridLayout,
156}
157
158#[cfg_attr(feature = "live", ravenrs_live::expose)]
159impl GridScenario {
160 pub fn new(name: impl Into<String>, layout: GridLayout) -> Self {
162 Self { name: name.into(), layout }
163 }
164
165 pub fn list_to_json(scenarios: &[Self]) -> String {
167 serde_json::to_string(scenarios).unwrap_or_default()
168 }
169
170 pub fn list_from_json(text: &str) -> Result<Vec<Self>, String> {
172 serde_json::from_str(text).map_err(|e| e.to_string())
173 }
174}
175
176#[derive(Clone, Debug, PartialEq)]
178pub(crate) struct Arrangement {
179 pub order: Vec<usize>,
181 pub pinned: Vec<bool>,
183 pub group_panel: bool,
184 pub page_size: Option<usize>,
185 pub summaries: Vec<Option<Summary>>,
187 pub formulas: Vec<Option<String>>,
189}
190
191#[cfg_attr(feature = "live", ravenrs_live::expose)]
192impl Arrangement {
193 pub fn new(columns: &[GridColumn], group_panel: bool, page_size: Option<usize>) -> Self {
194 Self {
195 order: (0..columns.len()).collect(),
196 pinned: columns.iter().map(|c| c.pinned).collect(),
197 group_panel,
198 page_size,
199 summaries: columns.iter().map(|c| c.summary).collect(),
200 formulas: columns.iter().map(|c| c.formula.clone()).collect(),
201 }
202 }
203
204 pub fn move_column(&mut self, column: usize, before: Option<usize>) {
206 self.order.retain(|c| *c != column);
207 let at = before.and_then(|b| self.order.iter().position(|c| *c == b)).unwrap_or(self.order.len());
208 self.order.insert(at, column);
209 }
210
211 pub fn visible(&self, query: &GridQuery) -> Vec<usize> {
213 let shown = |c: &usize| !query.is_hidden(*c) && !query.group_by.contains(c);
214 let pinned = |c: &usize| self.pinned.get(*c).copied().unwrap_or(false);
215 let mut out: Vec<usize> = self.order.iter().filter(|c| shown(c) && pinned(c)).copied().collect();
216 out.extend(self.order.iter().filter(|c| shown(c) && !pinned(c)));
217 out
218 }
219}
220
221pub(crate) fn capture(
223 columns: &[GridColumn],
224 query: &GridQuery,
225 widths: &[f32],
226 arrangement: &Arrangement,
227) -> GridLayout {
228 let title = |c: usize| columns.get(c).map_or(String::new(), |col| col.title.clone());
229 GridLayout {
230 columns: arrangement
231 .order
232 .iter()
233 .map(|&c| ColumnLayout {
234 title: title(c),
235 width: widths.get(c).copied().unwrap_or_default(),
236 hidden: query.is_hidden(c),
237 pinned: arrangement.pinned.get(c).copied().unwrap_or(false),
238 })
239 .collect(),
240 sort: query
241 .sort
242 .iter()
243 .map(|(c, d)| SortLayout { title: title(*c), descending: *d == SortDirection::Descending })
244 .collect(),
245 group_by: query.group_by.iter().map(|c| title(*c)).collect(),
246 filters: (0..columns.len())
247 .filter(|c| !query.filter(*c).trim().is_empty())
248 .map(|c| FilterLayout { title: title(c), text: query.filter(c).to_owned() })
249 .collect(),
250 search: query.search.clone(),
251 group_panel: arrangement.group_panel,
252 page_size: arrangement.page_size,
253 summaries: (0..columns.len())
254 .map(|c| SummaryLayout {
255 title: title(c),
256 kind: arrangement.summaries[c].map_or_else(String::new, |s| summary_name(s).to_owned()),
257 formula: arrangement.formulas[c].clone(),
258 })
259 .collect(),
260 }
261}
262
263pub(crate) fn apply(
266 layout: &GridLayout,
267 columns: &[GridColumn],
268 query: &mut GridQuery,
269 widths: &mut [f32],
270 arrangement: &mut Arrangement,
271) {
272 let index = |title: &str| columns.iter().position(|c| c.title == title);
273 let named: Vec<(usize, &ColumnLayout)> =
274 layout.columns.iter().filter_map(|c| Some((index(&c.title)?, c))).collect();
275 if !named.is_empty() {
276 let mut order: Vec<usize> = named.iter().map(|(c, _)| *c).collect();
277 order.extend((0..columns.len()).filter(|c| !named.iter().any(|(n, _)| n == c)));
278 arrangement.order = order;
279 for (c, column) in named {
280 if column.width > 0.0 {
281 widths[c] = column.width.max(columns[c].min_width);
282 }
283 if query.hidden.len() <= c {
284 query.hidden.resize(c + 1, false);
285 }
286 query.hidden[c] = column.hidden;
287 arrangement.pinned[c] = column.pinned;
288 }
289 }
290 query.sort = layout
291 .sort
292 .iter()
293 .filter_map(|s| {
294 let direction = if s.descending { SortDirection::Descending } else { SortDirection::Ascending };
295 Some((index(&s.title)?, direction))
296 })
297 .collect();
298 query.group_by = layout.group_by.iter().filter_map(|t| index(t)).collect();
299 query.collapsed.clear();
300 query.filters.clear();
301 for filter in &layout.filters {
302 if let Some(c) = index(&filter.title) {
303 query.set_filter(c, filter.text.clone());
304 }
305 }
306 query.search.clone_from(&layout.search);
307 arrangement.group_panel = layout.group_panel;
308 arrangement.page_size = layout.page_size;
309 for summary in &layout.summaries {
310 if let Some(c) = index(&summary.title) {
311 arrangement.summaries[c] = summary_from(&summary.kind);
312 arrangement.formulas[c].clone_from(&summary.formula);
313 }
314 }
315}
316
317#[cfg(test)]
318mod tests {
319 use super::*;
320
321 fn columns() -> Vec<GridColumn> {
322 vec![GridColumn::text("A"), GridColumn::text("B"), GridColumn::number("C")]
323 }
324
325 #[test]
326 fn a_layout_comes_back_as_it_was() {
327 let columns = columns();
328 let mut arrangement = Arrangement::new(&columns, true, Some(50));
329 arrangement.move_column(2, Some(0));
330 arrangement.pinned[1] = true;
331 let mut query = GridQuery { search: "x".into(), group_by: vec![1], ..GridQuery::default() };
332 query.sort.push((2, SortDirection::Descending));
333 query.set_filter(0, ">1".into());
334 let widths = vec![100.0, 200.0, 300.0];
335 let saved = capture(&columns, &query, &widths, &arrangement);
336 let read = GridLayout::from_json(&saved.to_json()).expect("json");
337 let (mut q, mut w, mut a) =
338 (GridQuery::default(), vec![1.0, 1.0, 1.0], Arrangement::new(&columns, false, None));
339 apply(&read, &columns, &mut q, &mut w, &mut a);
340 assert_eq!(a.order, [2, 0, 1]);
341 assert_eq!(a.pinned, [false, true, false]);
342 assert_eq!(a.page_size, Some(50));
343 assert!(a.group_panel);
344 assert_eq!(w, [100.0, 200.0, 300.0]);
345 assert_eq!(q.group_by, [1]);
346 assert_eq!(q.sort, [(2, SortDirection::Descending)]);
347 assert_eq!(q.filter(0), ">1");
348 assert_eq!(q.search, "x");
349 }
350
351 #[test]
352 fn a_scenario_survives_columns_that_changed() {
353 let layout = GridLayout {
354 columns: vec![
355 ColumnLayout { title: "C".into(), width: 90.0, ..ColumnLayout::default() },
356 ColumnLayout { title: "Gone".into(), width: 90.0, ..ColumnLayout::default() },
357 ],
358 group_by: vec!["Gone".into(), "B".into()],
359 ..GridLayout::default()
360 };
361 let columns = columns();
362 let (mut q, mut w, mut a) =
363 (GridQuery::default(), vec![1.0, 1.0, 1.0], Arrangement::new(&columns, true, None));
364 apply(&layout, &columns, &mut q, &mut w, &mut a);
365 assert_eq!(a.order, [2, 0, 1], "what it does not name goes last");
366 assert_eq!(q.group_by, [1]);
367 }
368
369 #[test]
370 fn what_an_older_version_did_not_know_takes_its_default() {
371 let layout = GridLayout::from_json(r#"{"search":"x"}"#).expect("json");
372 assert!(layout.group_panel);
373 assert_eq!(layout.search, "x");
374 }
375
376 #[test]
377 fn pinned_columns_come_first_and_grouped_ones_leave() {
378 let columns = columns();
379 let mut arrangement = Arrangement::new(&columns, true, None);
380 arrangement.pinned[2] = true;
381 let query = GridQuery { group_by: vec![1], ..GridQuery::default() };
382 assert_eq!(arrangement.visible(&query), [2, 0]);
383 }
384
385 #[test]
386 fn formulas_go_with_the_layout() {
387 let columns = columns();
388 let mut arrangement = Arrangement::new(&columns, true, None);
389 arrangement.formulas[2] = Some("Sum([C]) / Count()".into());
390 arrangement.summaries[1] = Some(Summary::Count);
391 let saved = capture(&columns, &GridQuery::default(), &[1.0, 1.0, 1.0], &arrangement);
392 let read = GridLayout::from_json(&saved.to_json()).expect("json");
393 let (mut q, mut w, mut a) =
394 (GridQuery::default(), vec![1.0, 1.0, 1.0], Arrangement::new(&columns, true, None));
395 apply(&read, &columns, &mut q, &mut w, &mut a);
396 assert_eq!(a.formulas[2].as_deref(), Some("Sum([C]) / Count()"));
397 assert_eq!(a.summaries[1], Some(Summary::Count));
398 }
399
400 #[test]
401 fn scenarios_go_to_json_and_back() {
402 let list = vec![GridScenario::new("Por conta", GridLayout::default())];
403 assert_eq!(GridScenario::list_from_json(&GridScenario::list_to_json(&list)), Ok(list));
404 }
405}