Skip to main content

polyspec_crudui_generator/
view.rs

1//! Row view state of form instances, kept outside the record data.
2
3use serde_json::Value;
4
5/// The row paths `{collection}.{key}` rendered collapsed, in the order the rows were collapsed and without duplicates.
6/// It is never submitted with the data.
7#[derive(Debug, Clone, Default, PartialEq, Eq)]
8pub struct ViewState {
9    /// The collapsed row paths.
10    pub collapsed: Vec<String>,
11}
12
13/// Whether `path` is the row path `row_path` or lies inside that row.
14fn inside(row_path: &str, path: &str) -> bool {
15    path == row_path
16        || path
17            .strip_prefix(row_path)
18            .is_some_and(|rest| rest.starts_with('.'))
19}
20
21/// The view with no collapsed row. Replacing the record returns to it.
22pub fn initial_view() -> ViewState {
23    ViewState::default()
24}
25
26/// The row paths of every collapsible row of the evaluated nodes, depth first in node order.
27pub fn collapsible_rows(nodes: &[Value]) -> Vec<String> {
28    let mut rows = Vec::new();
29    collect(nodes, &mut rows);
30    rows
31}
32
33fn children(node: &Value) -> &[Value] {
34    node.get("children")
35        .and_then(Value::as_array)
36        .map_or(&[], Vec::as_slice)
37}
38
39fn collect(nodes: &[Value], rows: &mut Vec<String>) {
40    for node in nodes {
41        if node["kind"] != "collection" {
42            collect(children(node), rows);
43            continue;
44        }
45        for row in children(node) {
46            if row["collapsible"] == true {
47                rows.push(format!(
48                    "{}.{}",
49                    node["path"].as_str().unwrap_or_default(),
50                    row["key"].as_str().unwrap_or_default()
51                ));
52            }
53            collect(children(row), rows);
54        }
55    }
56}
57
58/// Expand a collapsed row or collapse an expanded one, appending a collapsed row last.
59pub fn toggle_row_view(view: &ViewState, row_path: &str) -> ViewState {
60    let mut collapsed: Vec<String> = view
61        .collapsed
62        .iter()
63        .filter(|path| *path != row_path)
64        .cloned()
65        .collect();
66    if collapsed.len() == view.collapsed.len() {
67        collapsed.push(row_path.to_owned());
68    }
69    ViewState { collapsed }
70}
71
72/// The view with no collapsed row when `expanded` is true, and otherwise the view that collapses every collapsible
73/// row of the evaluated nodes.
74pub fn set_all_expanded_view(nodes: &[Value], expanded: bool) -> ViewState {
75    if expanded {
76        initial_view()
77    } else {
78        ViewState {
79            collapsed: collapsible_rows(nodes),
80        }
81    }
82}
83
84/// Drop the collapsed state of a removed row and of the rows inside it.
85pub fn remove_row_view(view: &ViewState, row_path: &str) -> ViewState {
86    ViewState {
87        collapsed: view
88            .collapsed
89            .iter()
90            .filter(|path| !inside(row_path, path))
91            .cloned()
92            .collect(),
93    }
94}
95
96/// Move the collapsed state of a rekeyed row and of the rows inside it to the new row path.
97pub fn rekey_row_view(view: &ViewState, old_path: &str, new_path: &str) -> ViewState {
98    let mut collapsed: Vec<String> = Vec::new();
99    for path in &view.collapsed {
100        let moved = if inside(old_path, path) {
101            format!("{new_path}{}", &path[old_path.len()..])
102        } else {
103            path.clone()
104        };
105        if !collapsed.contains(&moved) {
106            collapsed.push(moved);
107        }
108    }
109    ViewState { collapsed }
110}