Skip to main content

ui_grid_core/
state.rs

1use std::collections::BTreeMap;
2
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5
6use crate::{
7    constants::SortDirection,
8    models::{GridSavedPaginationState, GridSavedState, SortState},
9};
10
11#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
12#[serde(rename_all = "camelCase")]
13pub struct BuildGridSavedStateContext {
14    pub column_order: Vec<String>,
15    pub active_filters: BTreeMap<String, String>,
16    pub sort_state: SortState,
17    pub group_by_columns: Vec<String>,
18    pub current_page: usize,
19    pub page_size: usize,
20    pub total_items: usize,
21    pub expanded_rows: BTreeMap<String, bool>,
22    pub expanded_tree_rows: BTreeMap<String, bool>,
23}
24
25pub fn build_grid_saved_state(context: BuildGridSavedStateContext) -> GridSavedState {
26    GridSavedState {
27        column_order: context.column_order,
28        filters: context.active_filters,
29        sort: Some(context.sort_state),
30        grouping: context.group_by_columns,
31        pagination: Some(GridSavedPaginationState {
32            pagination_current_page: current_page_value(context.current_page),
33            pagination_page_size: effective_page_size(context.page_size, context.total_items),
34        }),
35        expandable: context.expanded_rows,
36        tree_view: context.expanded_tree_rows,
37    }
38}
39
40pub fn normalize_grid_saved_state(value: &Value) -> GridSavedState {
41    let mut normalized = GridSavedState::default();
42    let Some(state) = value.as_object() else {
43        return normalized;
44    };
45
46    if let Some(Value::Array(column_order)) = state.get("columnOrder") {
47        normalized.column_order = column_order
48            .iter()
49            .filter_map(|value| value.as_str())
50            .filter(|value| is_safe_state_key(value))
51            .map(ToOwned::to_owned)
52            .collect();
53    }
54
55    if let Some(Value::Object(filters)) = state.get("filters") {
56        normalized.filters = filters
57            .iter()
58            .filter_map(|(key, value)| {
59                let string_value = value.as_str()?;
60                is_safe_state_key(key).then(|| (key.clone(), string_value.to_string()))
61            })
62            .collect();
63    }
64
65    if let Some(Value::Object(sort)) = state.get("sort") {
66        normalized.sort = Some(SortState {
67            column_name: sort
68                .get("columnName")
69                .and_then(Value::as_str)
70                .filter(|value| is_safe_state_key(value))
71                .map(ToOwned::to_owned),
72            direction: match sort.get("direction").and_then(Value::as_str) {
73                Some("asc") => SortDirection::Asc,
74                Some("desc") => SortDirection::Desc,
75                _ => SortDirection::None,
76            },
77        });
78    }
79
80    if let Some(Value::Array(grouping)) = state.get("grouping") {
81        normalized.grouping = grouping
82            .iter()
83            .filter_map(Value::as_str)
84            .filter(|value| is_safe_state_key(value))
85            .map(ToOwned::to_owned)
86            .collect();
87    }
88
89    if let Some(Value::Object(pagination)) = state.get("pagination") {
90        normalized.pagination = Some(GridSavedPaginationState {
91            pagination_current_page: pagination
92                .get("paginationCurrentPage")
93                .and_then(Value::as_u64)
94                .map(|value| value.max(1) as usize)
95                .unwrap_or(1),
96            pagination_page_size: pagination
97                .get("paginationPageSize")
98                .and_then(Value::as_u64)
99                .map(|value| value as usize)
100                .unwrap_or(0),
101        });
102    }
103
104    if let Some(Value::Object(expandable)) = state.get("expandable") {
105        normalized.expandable = normalize_boolean_map(expandable);
106    }
107
108    if let Some(Value::Object(tree_view)) = state.get("treeView") {
109        normalized.tree_view = normalize_boolean_map(tree_view);
110    }
111
112    normalized
113}
114
115fn current_page_value(current_page: usize) -> usize {
116    current_page.max(1)
117}
118
119fn effective_page_size(page_size: usize, total_items: usize) -> usize {
120    if page_size > 0 {
121        page_size
122    } else {
123        total_items
124    }
125}
126
127pub fn sanitize_download_filename(value: &str) -> String {
128    let sanitized = value
129        .chars()
130        .map(|ch| {
131            if ch.is_ascii_alphanumeric() || matches!(ch, '.' | '_' | '-') {
132                ch
133            } else {
134                '_'
135            }
136        })
137        .collect::<String>();
138    let trimmed = sanitized.trim_matches('_').to_string();
139    if trimmed.is_empty() {
140        "ui-grid".to_string()
141    } else {
142        trimmed
143    }
144}
145
146pub fn normalize_boolean_map(value: &serde_json::Map<String, Value>) -> BTreeMap<String, bool> {
147    value
148        .iter()
149        .filter_map(|(key, entry)| {
150            let boolean = entry.as_bool()?;
151            is_safe_state_key(key).then(|| (key.clone(), boolean))
152        })
153        .collect()
154}
155
156pub fn is_safe_state_key(value: &str) -> bool {
157    !matches!(value, "__proto__" | "constructor" | "prototype")
158}