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}