ravenrs_app/
record_action.rs1use ravenrs_criteria::{Criteria, Scope};
5
6use crate::{
7 Action, ActionCategory, CONTEXT_SAVE, CriteriaMode, EntityInfo, ModelError, Module, Operation, RuleWords,
8 Selection, SpaceError, ViewController, Violation,
9};
10
11#[derive(Clone, Debug, PartialEq, Eq)]
28#[non_exhaustive]
29#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
30pub struct RecordAction {
31 pub caption: String,
33 pub id: Option<String>,
35 pub selection: Selection,
37 pub enabled: Option<String>,
39 pub set: String,
41 pub confirm: Option<String>,
43}
44
45#[cfg_attr(feature = "live", ravenrs_live::expose)]
46impl RecordAction {
47 pub fn new(caption: impl Into<String>) -> Self {
49 Self {
50 caption: caption.into(),
51 id: None,
52 selection: Selection::RequireMultiple,
53 enabled: None,
54 set: String::new(),
55 confirm: None,
56 }
57 }
58
59 pub fn id(mut self, id: impl Into<String>) -> Self {
61 self.id = Some(id.into());
62 self
63 }
64
65 pub fn selection(mut self, selection: Selection) -> Self {
67 self.selection = selection;
68 self
69 }
70
71 pub fn enabled(mut self, expression: impl Into<String>) -> Self {
73 self.enabled = Some(expression.into());
74 self
75 }
76
77 pub fn set(mut self, assignments: impl Into<String>) -> Self {
79 self.set = assignments.into();
80 self
81 }
82
83 pub fn confirm(mut self, question: impl Into<String>) -> Self {
85 self.confirm = Some(question.into());
86 self
87 }
88
89 pub fn id_on(&self, entity: &EntityInfo) -> String {
91 self.id.clone().unwrap_or_else(|| format!("{}.{}", entity.name, self.caption))
92 }
93
94 pub fn to_action(&self, entity: &EntityInfo) -> Action {
96 let mut action = Action::simple(self.id_on(entity), self.caption.clone())
97 .category(ActionCategory::Edit)
98 .target_entity(entity.name.clone())
99 .selection(self.selection)
100 .requires(Operation::Write);
101 if let Some(enabled) = &self.enabled {
102 action = action.target_criteria(enabled.clone(), CriteriaMode::TrueForAll);
103 }
104 if let Some(question) = &self.confirm {
105 action = action.confirmation(question.clone());
106 }
107 action.assign(entity, &self.set)
108 }
109
110 pub(crate) fn check(&self, entity: &EntityInfo) -> Result<(), ModelError> {
113 let id = self.id_on(entity);
114 let wrong = |what: String| ModelError(format!("the action {id}: {what}"));
115 if let Some(enabled) = &self.enabled {
116 Criteria::parse(enabled).map_err(|e| wrong(e.to_string()))?;
117 }
118 assignments(entity, &self.set).map(drop).map_err(wrong)
119 }
120}
121
122pub(crate) fn assignments(entity: &EntityInfo, text: &str) -> Result<Vec<(String, Criteria)>, String> {
124 let found = Criteria::assignments(text).map_err(|e| e.to_string())?;
125 if found.is_empty() {
126 return Err("it sets nothing".to_owned());
127 }
128 for (field, _) in &found {
129 if !entity.fields.iter().any(|known| &known.name == field) {
130 return Err(format!("{} has no field {field}", entity.name));
131 }
132 }
133 Ok(found)
134}
135
136pub(crate) fn assign(
139 entity: &EntityInfo,
140 set: &[(String, Criteria)],
141 cx: &crate::ActionContext,
142) -> Result<(), String> {
143 let key = entity.key_field().map(|key| key.name.clone()).unwrap_or_default();
144 let words = cx.service::<RuleWords>().map(|words| (*words).clone()).unwrap_or_default();
145 let environment = crate::environment().at(crate::local_now());
146 let mut space = cx.space.borrow_mut();
147 let result = (|| {
148 for record in &cx.selected {
149 let mut stored =
150 space.find(&entity.name, &record.get(&key)).map_err(|e| e.to_string())?.ok_or_else(|| {
151 SpaceError::Conflict(format!("a record of {} is gone", entity.name)).to_string()
152 })?;
153 let values = {
154 let scope = Scope::new(&environment).row(&stored);
155 set.iter()
156 .map(|(field, expression)| {
157 scope
158 .evaluate(expression)
159 .map(|value| (field, value))
160 .map_err(|e| format!("{field}: {e}"))
161 })
162 .collect::<Result<Vec<_>, String>>()?
163 };
164 for (field, value) in values {
165 stored.set(field, value);
166 }
167 let wrong: Vec<String> = crate::validate(entity, &stored, CONTEXT_SAVE, &*space, &words)
168 .into_iter()
169 .filter(Violation::is_error)
170 .map(|violation| violation.message)
171 .collect();
172 if !wrong.is_empty() {
173 return Err(wrong.join("; "));
174 }
175 space.save(stored).map_err(|e| e.to_string())?;
176 }
177 space.commit().map_err(|e| e.to_string())
178 })();
179 if result.is_err() {
180 space.rollback();
181 }
182 drop(space);
183 if result.is_ok() {
184 cx.records_changed();
185 }
186 result
187}
188
189struct Declared {
190 actions: Vec<Action>,
191}
192
193impl ViewController for Declared {
194 fn id(&self) -> &str {
195 "RecordActions"
196 }
197
198 fn actions(&self) -> Vec<Action> {
199 self.actions.clone()
200 }
201}
202
203#[derive(Debug)]
205pub(crate) struct RecordActionModule(pub(crate) Vec<EntityInfo>);
206
207impl Module for RecordActionModule {
208 fn name(&self) -> &str {
209 "RecordActions"
210 }
211
212 fn view_controllers(&self) -> Vec<Box<dyn ViewController>> {
213 let actions = self
214 .0
215 .iter()
216 .flat_map(|entity| entity.actions.iter().map(move |declared| declared.to_action(entity)))
217 .collect();
218 vec![Box::new(Declared { actions })]
219 }
220}