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    #[serde(default)]
24    pub pinned_columns: BTreeMap<String, String>,
25}
26
27#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
28#[serde(rename_all = "camelCase")]
29pub struct GridRestoreMutationPlan {
30    #[serde(default)]
31    pub column_order: Option<Vec<String>>,
32    #[serde(default)]
33    pub filters: Option<BTreeMap<String, String>>,
34    #[serde(default)]
35    pub sort: Option<SortState>,
36    #[serde(default)]
37    pub grouping: Option<Vec<String>>,
38    #[serde(default)]
39    pub pagination: Option<GridSavedPaginationState>,
40    #[serde(default)]
41    pub expandable: Option<BTreeMap<String, bool>>,
42    #[serde(default)]
43    pub tree_view: Option<BTreeMap<String, bool>>,
44    #[serde(default)]
45    pub pinning: Option<BTreeMap<String, String>>,
46}
47
48pub fn build_grid_saved_state(context: BuildGridSavedStateContext) -> GridSavedState {
49    GridSavedState {
50        column_order: context.column_order,
51        filters: context.active_filters,
52        sort: Some(context.sort_state),
53        grouping: context.group_by_columns,
54        pagination: Some(GridSavedPaginationState {
55            pagination_current_page: current_page_value(context.current_page),
56            pagination_page_size: effective_page_size(context.page_size, context.total_items),
57        }),
58        expandable: context.expanded_rows,
59        tree_view: context.expanded_tree_rows,
60        pinning: context.pinned_columns,
61    }
62}
63
64pub fn normalize_grid_saved_state(value: &Value) -> GridSavedState {
65    let mut normalized = GridSavedState::default();
66    let Some(state) = value.as_object() else {
67        return normalized;
68    };
69
70    if let Some(Value::Array(column_order)) = state.get("columnOrder") {
71        normalized.column_order = column_order
72            .iter()
73            .filter_map(|value| value.as_str())
74            .filter(|value| is_safe_state_key(value))
75            .map(ToOwned::to_owned)
76            .collect();
77    }
78
79    if let Some(Value::Object(filters)) = state.get("filters") {
80        normalized.filters = filters
81            .iter()
82            .filter_map(|(key, value)| {
83                let string_value = value.as_str()?;
84                is_safe_state_key(key).then(|| (key.clone(), string_value.to_string()))
85            })
86            .collect();
87    }
88
89    if let Some(Value::Object(sort)) = state.get("sort") {
90        normalized.sort = Some(SortState {
91            column_name: sort
92                .get("columnName")
93                .and_then(Value::as_str)
94                .filter(|value| is_safe_state_key(value))
95                .map(ToOwned::to_owned),
96            direction: match sort.get("direction").and_then(Value::as_str) {
97                Some("asc") => SortDirection::Asc,
98                Some("desc") => SortDirection::Desc,
99                _ => SortDirection::None,
100            },
101        });
102    }
103
104    if let Some(Value::Array(grouping)) = state.get("grouping") {
105        normalized.grouping = grouping
106            .iter()
107            .filter_map(Value::as_str)
108            .filter(|value| is_safe_state_key(value))
109            .map(ToOwned::to_owned)
110            .collect();
111    }
112
113    if let Some(Value::Object(pagination)) = state.get("pagination") {
114        normalized.pagination = Some(GridSavedPaginationState {
115            pagination_current_page: pagination
116                .get("paginationCurrentPage")
117                .and_then(Value::as_u64)
118                .map(|value| value.max(1) as usize)
119                .unwrap_or(1),
120            pagination_page_size: pagination
121                .get("paginationPageSize")
122                .and_then(Value::as_u64)
123                .map(|value| value as usize)
124                .unwrap_or(0),
125        });
126    }
127
128    if let Some(Value::Object(expandable)) = state.get("expandable") {
129        normalized.expandable = normalize_boolean_map(expandable);
130    }
131
132    if let Some(Value::Object(tree_view)) = state.get("treeView") {
133        normalized.tree_view = normalize_boolean_map(tree_view);
134    }
135
136    if let Some(Value::Object(pinning)) = state.get("pinning") {
137        normalized.pinning = pinning
138            .iter()
139            .filter_map(|(key, value)| match value.as_str() {
140                Some("left") | Some("right") if is_safe_state_key(key) => {
141                    Some((key.clone(), value.as_str().unwrap().to_string()))
142                }
143                _ => None,
144            })
145            .collect();
146    }
147
148    normalized
149}
150
151pub fn create_grid_restore_mutation_plan(state: &GridSavedState) -> GridRestoreMutationPlan {
152    let normalized =
153        normalize_grid_saved_state(&serde_json::to_value(state).unwrap_or(Value::Null));
154
155    GridRestoreMutationPlan {
156        column_order: (!normalized.column_order.is_empty()).then_some(normalized.column_order),
157        filters: (!normalized.filters.is_empty()).then_some(normalized.filters),
158        sort: normalized.sort,
159        grouping: (!normalized.grouping.is_empty()).then_some(normalized.grouping),
160        pagination: normalized.pagination,
161        expandable: (!normalized.expandable.is_empty()).then_some(normalized.expandable),
162        tree_view: (!normalized.tree_view.is_empty()).then_some(normalized.tree_view),
163        pinning: (!normalized.pinning.is_empty()).then_some(normalized.pinning),
164    }
165}
166
167pub fn serialize_grid_saved_state(state: &GridSavedState) -> Result<String, serde_json::Error> {
168    serde_json::to_string(state)
169}
170
171pub fn deserialize_grid_saved_state(value: &str) -> Result<GridSavedState, serde_json::Error> {
172    let parsed: Value = serde_json::from_str(value)?;
173    Ok(normalize_grid_saved_state(&parsed))
174}
175
176pub fn serialize_grid_saved_state_with<T>(
177    state: &GridSavedState,
178    serializer: impl FnOnce(&GridSavedState) -> T,
179) -> T {
180    serializer(state)
181}
182
183pub fn deserialize_grid_saved_state_with<T, E>(
184    value: T,
185    deserializer: impl FnOnce(T) -> Result<GridSavedState, E>,
186) -> Result<GridSavedState, E> {
187    let state = deserializer(value)?;
188    let normalized =
189        normalize_grid_saved_state(&serde_json::to_value(state).unwrap_or(Value::Null));
190    Ok(normalized)
191}
192
193fn current_page_value(current_page: usize) -> usize {
194    current_page.max(1)
195}
196
197fn effective_page_size(page_size: usize, total_items: usize) -> usize {
198    if page_size > 0 {
199        page_size
200    } else {
201        total_items
202    }
203}
204
205pub fn sanitize_download_filename(value: &str) -> String {
206    let sanitized = value
207        .chars()
208        .map(|ch| {
209            if ch.is_ascii_alphanumeric() || matches!(ch, '.' | '_' | '-') {
210                ch
211            } else {
212                '_'
213            }
214        })
215        .collect::<String>();
216    let trimmed = sanitized.trim_matches('_').to_string();
217    if trimmed.is_empty() {
218        "ui-grid".to_string()
219    } else {
220        trimmed
221    }
222}
223
224pub fn normalize_boolean_map(value: &serde_json::Map<String, Value>) -> BTreeMap<String, bool> {
225    value
226        .iter()
227        .filter_map(|(key, entry)| {
228            let boolean = entry.as_bool()?;
229            is_safe_state_key(key).then(|| (key.clone(), boolean))
230        })
231        .collect()
232}
233
234pub fn is_safe_state_key(value: &str) -> bool {
235    !matches!(value, "__proto__" | "constructor" | "prototype")
236}