polyspec_crudui_generator/
actions.rs1use crate::error::{FormError, FormResult};
4use crate::instance::{AddRowOptions, Form};
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum ActionName {
9 MoveUp,
11 MoveDown,
13 AddRow,
15 CopyRow,
17 RemoveRow,
19 ToggleRow,
21 SelectRow,
23 ExpandAll,
25 CollapseAll,
27 Undo,
29 Redo,
31}
32
33impl ActionName {
34 pub fn as_str(self) -> &'static str {
36 match self {
37 Self::MoveUp => "move-up",
38 Self::MoveDown => "move-down",
39 Self::AddRow => "add-row",
40 Self::CopyRow => "copy-row",
41 Self::RemoveRow => "remove-row",
42 Self::ToggleRow => "toggle-row",
43 Self::SelectRow => "select-row",
44 Self::ExpandAll => "expand-all",
45 Self::CollapseAll => "collapse-all",
46 Self::Undo => "undo",
47 Self::Redo => "redo",
48 }
49 }
50
51 pub fn parse(name: &str) -> Option<Self> {
53 Some(match name {
54 "move-up" => Self::MoveUp,
55 "move-down" => Self::MoveDown,
56 "add-row" => Self::AddRow,
57 "copy-row" => Self::CopyRow,
58 "remove-row" => Self::RemoveRow,
59 "toggle-row" => Self::ToggleRow,
60 "select-row" => Self::SelectRow,
61 "expand-all" => Self::ExpandAll,
62 "collapse-all" => Self::CollapseAll,
63 "undo" => Self::Undo,
64 "redo" => Self::Redo,
65 _ => return None,
66 })
67 }
68}
69
70#[derive(Debug, Default, Clone, serde::Deserialize)]
73pub struct ActionInput {
74 pub action: Option<String>,
76 pub path: Option<String>,
78 pub key: Option<String>,
80}
81
82#[derive(Debug, Clone, PartialEq, Eq)]
84pub struct ActionTarget {
85 pub name: ActionName,
87 pub path: Option<String>,
89 pub key: Option<String>,
91}
92
93impl ActionTarget {
94 pub fn named(name: &str, path: Option<String>, key: Option<String>) -> FormResult<Self> {
97 let Some(name) = ActionName::parse(name) else {
98 return Err(FormError::input(format!("Unknown action: {name}")));
99 };
100 Ok(Self { name, path, key })
101 }
102}
103
104#[derive(Debug, Clone, PartialEq, Eq)]
106pub struct FocusTarget {
107 pub path: String,
109 pub key: Option<String>,
111}
112
113#[derive(Debug, Default, Clone, PartialEq, Eq)]
115pub struct ActionResult {
116 pub focus: Option<FocusTarget>,
118}
119
120pub fn resolve_action(input: &ActionInput) -> Option<ActionTarget> {
122 let name = ActionName::parse(input.action.as_deref()?)?;
123 Some(ActionTarget {
124 name,
125 path: non_empty(&input.path),
126 key: non_empty(&input.key),
127 })
128}
129
130pub fn run_action(form: &mut Form, target: &ActionTarget) -> FormResult<Option<ActionResult>> {
133 match target.name {
134 ActionName::ExpandAll | ActionName::CollapseAll => {
135 form.set_all_expanded(target.name == ActionName::ExpandAll)?;
136 return Ok(Some(ActionResult::default()));
137 }
138 ActionName::Undo => {
139 form.undo()?;
140 return Ok(Some(ActionResult::default()));
141 }
142 ActionName::Redo => {
143 form.redo()?;
144 return Ok(Some(ActionResult::default()));
145 }
146 _ => {}
147 }
148 let Some(path) = target.path.as_deref().filter(|path| !path.is_empty()) else {
149 return Ok(None);
150 };
151 if target.name == ActionName::AddRow {
152 let options = AddRowOptions {
154 after_key: target.key.clone(),
155 ..AddRowOptions::default()
156 };
157 let key = form.add_row(path, options)?;
158 return Ok(Some(focus_on(path, &key)));
159 }
160 let Some(key) = target.key.as_deref().filter(|key| !key.is_empty()) else {
161 return Ok(None);
162 };
163 let result = match target.name {
164 ActionName::CopyRow => {
165 let copied = form.copy_row(path, key, AddRowOptions::default())?;
166 focus_on(path, &copied)
167 }
168 ActionName::RemoveRow => {
169 let keys = row_keys(form, path)?;
170 let index = keys.iter().position(|known| known == key);
171 let neighbour = index.and_then(|index| {
173 if index > 0 {
174 keys.get(index - 1)
175 } else {
176 keys.get(index + 1)
177 }
178 });
179 form.remove_row(path, key)?;
180 match neighbour {
181 Some(neighbour) => focus_on(path, neighbour),
182 None => ActionResult {
183 focus: Some(FocusTarget {
184 path: path.to_owned(),
185 key: None,
186 }),
187 },
188 }
189 }
190 ActionName::ToggleRow => {
191 form.toggle_row(path, key)?;
192 ActionResult::default()
193 }
194 ActionName::SelectRow => focus_on(path, key),
196 ActionName::MoveUp | ActionName::MoveDown => {
197 let keys = row_keys(form, path)?;
198 let from = keys
200 .iter()
201 .position(|known| known == key)
202 .map_or(-1, |index| index as i64);
203 let step = if target.name == ActionName::MoveUp {
204 -1
205 } else {
206 1
207 };
208 form.move_row(path, key, from + step)?;
209 focus_on(path, key)
210 }
211 ActionName::AddRow => unreachable!("add-row returns before the key is read"),
212 ActionName::ExpandAll | ActionName::CollapseAll | ActionName::Undo | ActionName::Redo => {
213 unreachable!("the form-wide actions return before the path is read")
214 }
215 };
216 Ok(Some(result))
217}
218
219fn non_empty(value: &Option<String>) -> Option<String> {
221 value
222 .as_deref()
223 .filter(|text| !text.is_empty())
224 .map(str::to_owned)
225}
226
227fn focus_on(path: &str, key: &str) -> ActionResult {
229 ActionResult {
230 focus: Some(FocusTarget {
231 path: path.to_owned(),
232 key: Some(key.to_owned()),
233 }),
234 }
235}
236
237fn row_keys(form: &Form, path: &str) -> FormResult<Vec<String>> {
239 match form.get_value(path)? {
240 serde_json::Value::Object(rows) => Ok(rows.keys().cloned().collect()),
241 _ => Err(FormError::input(format!(
242 "Repeated data must be a keyed object: {path}"
243 ))),
244 }
245}