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}