1use serde::{Deserialize, Serialize};
2use serde_json::{Number, Value};
3
4use crate::models::{GridCellPosition, GridColumnDef, GridColumnType, GridOptions, GridRow};
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
7#[serde(rename_all = "camelCase")]
8pub enum GridMoveDirection {
9 Left,
10 Right,
11 Up,
12 Down,
13}
14
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct GridEditSession {
17 pub focused_cell: GridCellPosition,
18 pub editing_cell: GridCellPosition,
19 pub editing_value: String,
20}
21
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct GridFocusCellResult {
24 pub focused_cell: GridCellPosition,
25 pub should_begin_edit: bool,
26}
27
28pub fn is_grid_cell_position(
29 position: Option<&GridCellPosition>,
30 row_id: &str,
31 column_name: &str,
32) -> bool {
33 position
34 .is_some_and(|position| position.row_id == row_id && position.column_name == column_name)
35}
36
37pub fn begin_grid_edit_session(
38 row_id: impl Into<String>,
39 column_name: impl Into<String>,
40 editing_value: impl Into<String>,
41) -> GridEditSession {
42 let position = GridCellPosition {
43 row_id: row_id.into(),
44 column_name: column_name.into(),
45 };
46
47 GridEditSession {
48 focused_cell: position.clone(),
49 editing_cell: position,
50 editing_value: editing_value.into(),
51 }
52}
53
54pub fn should_grid_edit_on_focus(options: &GridOptions, column: &GridColumnDef) -> bool {
55 column.enable_cell_edit_on_focus || options.enable_cell_edit_on_focus
56}
57
58pub fn build_grid_focus_cell_result(
59 current_focused_cell: Option<&GridCellPosition>,
60 current_editing_cell: Option<&GridCellPosition>,
61 row_id: impl Into<String>,
62 column_name: impl Into<String>,
63 should_edit_on_focus: bool,
64 is_cell_editable: bool,
65) -> GridFocusCellResult {
66 let focused_cell = GridCellPosition {
67 row_id: row_id.into(),
68 column_name: column_name.into(),
69 };
70
71 let should_begin_edit = should_edit_on_focus
72 && is_cell_editable
73 && !is_grid_cell_position(
74 current_focused_cell,
75 &focused_cell.row_id,
76 &focused_cell.column_name,
77 )
78 && !is_grid_cell_position(
79 current_editing_cell,
80 &focused_cell.row_id,
81 &focused_cell.column_name,
82 );
83
84 GridFocusCellResult {
85 focused_cell,
86 should_begin_edit,
87 }
88}
89
90pub fn clear_grid_edit_session() -> (Option<GridCellPosition>, String) {
91 (None, String::new())
92}
93
94pub fn find_next_grid_cell<F>(
95 rows: &[GridRow],
96 columns: &[GridColumnDef],
97 row_id: &str,
98 column_name: &str,
99 direction: GridMoveDirection,
100 is_cell_allowed: Option<F>,
101) -> Option<GridCellPosition>
102where
103 F: Fn(&GridRow, &GridColumnDef) -> bool,
104{
105 let row_index = rows.iter().position(|candidate| candidate.id == row_id)?;
106 let column_index = columns
107 .iter()
108 .position(|candidate| candidate.name == column_name)?;
109
110 let mut next_row_index = row_index as isize;
111 let mut next_column_index = column_index as isize;
112
113 loop {
114 match direction {
115 GridMoveDirection::Left => {
116 next_column_index -= 1;
117 if next_column_index < 0 {
118 next_row_index -= 1;
119 next_column_index = columns.len() as isize - 1;
120 }
121 }
122 GridMoveDirection::Right => {
123 next_column_index += 1;
124 if next_column_index >= columns.len() as isize {
125 next_row_index += 1;
126 next_column_index = 0;
127 }
128 }
129 GridMoveDirection::Up => next_row_index -= 1,
130 GridMoveDirection::Down => next_row_index += 1,
131 }
132
133 if next_row_index < 0
134 || next_row_index >= rows.len() as isize
135 || next_column_index < 0
136 || next_column_index >= columns.len() as isize
137 {
138 return None;
139 }
140
141 let next_row = &rows[next_row_index as usize];
142 let next_column = &columns[next_column_index as usize];
143 let allowed = is_cell_allowed
144 .as_ref()
145 .is_none_or(|predicate| predicate(next_row, next_column));
146 if allowed {
147 return Some(GridCellPosition {
148 row_id: next_row.id.clone(),
149 column_name: next_column.name.clone(),
150 });
151 }
152 }
153}
154
155pub fn stringify_grid_editor_value(value: &Value) -> String {
156 match value {
157 Value::Null => String::new(),
158 Value::String(value) => value.clone(),
159 Value::Bool(value) => value.to_string(),
160 Value::Number(value) => value.to_string(),
161 other => other.to_string(),
162 }
163}
164
165pub fn parse_grid_edited_value(column: &GridColumnDef, value: &str, old_value: &Value) -> Value {
166 match column.r#type {
167 GridColumnType::Number => value
168 .parse::<f64>()
169 .ok()
170 .filter(|parsed| parsed.is_finite())
171 .and_then(Number::from_f64)
172 .map(Value::Number)
173 .unwrap_or_else(|| old_value.clone()),
174 GridColumnType::Boolean => Value::Bool(value == "true"),
175 GridColumnType::Date | GridColumnType::String | GridColumnType::Object => {
176 Value::String(value.to_string())
177 }
178 }
179}