1use ravenrs_app::{
4 Action, ActionCategory, ActionInput, Criteria, Frame, Module, Nesting, Operation, Selection, TargetScope,
5 Value, ViewController, ViewKind,
6};
7
8use crate::nested::NestedOwner;
9
10use crate::session::{Session, Words};
11
12pub mod ids {
14 pub const NEW: &str = "New";
16 pub const DELETE: &str = "Delete";
18 pub const SAVE: &str = "Save";
20 pub const SAVE_AND_CLOSE: &str = "SaveAndClose";
22 pub const SAVE_AND_NEW: &str = "SaveAndNew";
24 pub const CANCEL: &str = "Cancel";
26 pub const PREVIOUS: &str = "PreviousObject";
28 pub const NEXT: &str = "NextObject";
30 pub const REFRESH: &str = "Refresh";
32 pub const BACK: &str = "Back";
34 pub const VARIANTS: &str = "ViewVariants";
36 pub const FILTERS: &str = "SetFilter";
38 pub const CLONE: &str = "CloneObject";
40 pub const LINK: &str = "LinkObject";
42 pub const UNLINK: &str = "UnlinkObject";
44}
45
46#[derive(Debug, Default)]
49#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
50pub struct SystemModule {
51 words: Words,
52}
53
54#[cfg_attr(feature = "live", ravenrs_live::expose)]
55impl SystemModule {
56 pub fn new() -> Self {
58 Self::default()
59 }
60
61 pub fn with_words(words: Words) -> Self {
63 Self { words }
64 }
65}
66
67fn session_of(cx: &ravenrs_app::ActionContext) -> Result<Session, String> {
68 Session::of(cx).ok_or_else(|| "this action needs the application's screens".to_owned())
69}
70
71fn entity_of(cx: &ravenrs_app::ActionContext) -> Result<String, String> {
72 cx.view.as_ref().and_then(|view| view.entity.clone()).ok_or_else(|| "this view has no entity".to_owned())
73}
74
75fn off_on_a_dashboard(frame: &Frame, ids: &[&str]) {
77 let on = frame.view().is_none_or(|view| view.kind != ViewKind::Dashboard);
78 for id in ids {
79 if let Some(action) = frame.action(id) {
80 action.set_active("dashboard", on);
81 }
82 }
83}
84
85struct NewObjectController(Words);
86
87impl ViewController for NewObjectController {
88 fn id(&self) -> &str {
89 "NewObject"
90 }
91 fn scope(&self) -> TargetScope {
92 TargetScope::any().nesting(Nesting::Root)
93 }
94 fn activated(&mut self, frame: &Frame) {
95 off_on_a_dashboard(frame, &[ids::NEW]);
96 }
97 fn actions(&self) -> Vec<Action> {
98 vec![
99 Action::simple(ids::NEW, self.0.new.clone())
100 .category(ActionCategory::New)
101 .requires(Operation::Create)
102 .icon("plus")
103 .on_execute(|cx| {
104 let (session, entity) = (session_of(cx)?, entity_of(cx)?);
105 let opener = session.clone();
106 session.leaving(move || opener.open_new(&entity));
107 Ok(())
108 }),
109 ]
110 }
111}
112
113struct DeleteController(Words);
114
115impl ViewController for DeleteController {
116 fn id(&self) -> &str {
117 "DeleteObjects"
118 }
119 fn activated(&mut self, frame: &Frame) {
120 off_on_a_dashboard(frame, &[ids::DELETE]);
121 }
122 fn actions(&self) -> Vec<Action> {
123 vec![
124 Action::simple(ids::DELETE, self.0.delete.clone())
125 .category(ActionCategory::Edit)
126 .requires(Operation::Delete)
127 .icon("trash-2")
128 .selection(Selection::RequireMultiple)
129 .confirmation(self.0.delete_question.clone())
130 .on_execute(|cx| session_of(cx)?.delete(&cx.selected)),
131 ]
132 }
133}
134
135struct ModificationsController(Words);
136
137impl ViewController for ModificationsController {
138 fn id(&self) -> &str {
139 "Modifications"
140 }
141 fn scope(&self) -> TargetScope {
142 TargetScope::any().view_kind(ViewKind::Detail)
143 }
144 fn actions(&self) -> Vec<Action> {
145 vec![
146 Action::simple(ids::SAVE, self.0.save.clone())
147 .category(ActionCategory::Save)
148 .requires(Operation::Write)
149 .icon("save")
150 .hotkey("command+s")
151 .on_execute(|cx| session_of(cx)?.save()),
152 Action::simple(ids::SAVE_AND_CLOSE, self.0.save_and_close.clone())
153 .category(ActionCategory::Save)
154 .requires(Operation::Write)
155 .hotkey("command+shift+s")
156 .on_execute(|cx| session_of(cx)?.save_then(Session::back_now)),
157 Action::simple(ids::SAVE_AND_NEW, self.0.save_and_new.clone())
158 .category(ActionCategory::Save)
159 .requires(Operation::Write)
160 .on_execute(|cx| {
161 let (session, entity) = (session_of(cx)?, entity_of(cx)?);
162 session.save_then(move |session| session.open_new(&entity))
163 }),
164 Action::simple(ids::CANCEL, self.0.cancel.clone()).category(ActionCategory::Save).on_execute(
165 |cx| {
166 session_of(cx)?.cancel();
167 Ok(())
168 },
169 ),
170 ]
171 }
172}
173
174struct RecordsNavigationController(Words);
175
176impl ViewController for RecordsNavigationController {
177 fn id(&self) -> &str {
178 "RecordsNavigation"
179 }
180 fn scope(&self) -> TargetScope {
181 TargetScope::any().view_kind(ViewKind::Detail)
182 }
183 fn actions(&self) -> Vec<Action> {
184 vec![
185 Action::simple(ids::BACK, self.0.back.clone())
186 .category(ActionCategory::Navigation)
187 .icon("arrow-left")
188 .on_execute(|cx| {
189 session_of(cx)?.back();
190 Ok(())
191 }),
192 Action::simple(ids::PREVIOUS, self.0.previous.clone())
193 .category(ActionCategory::Navigation)
194 .icon("chevron-left")
195 .on_execute(|cx| {
196 session_of(cx)?.step(-1);
197 Ok(())
198 }),
199 Action::simple(ids::NEXT, self.0.next.clone())
200 .category(ActionCategory::Navigation)
201 .icon("chevron-right")
202 .on_execute(|cx| {
203 session_of(cx)?.step(1);
204 Ok(())
205 }),
206 ]
207 }
208}
209
210struct RefreshController(Words);
211
212impl ViewController for RefreshController {
213 fn id(&self) -> &str {
214 "Refresh"
215 }
216 fn scope(&self) -> TargetScope {
217 TargetScope::any().nesting(ravenrs_app::Nesting::Root)
218 }
219 fn actions(&self) -> Vec<Action> {
220 vec![
221 Action::simple(ids::REFRESH, self.0.refresh.clone())
222 .category(ActionCategory::View)
223 .icon("refresh-cw")
224 .on_execute(|cx| {
225 session_of(cx)?.refresh();
226 Ok(())
227 }),
228 ]
229 }
230}
231
232struct ChoicesController {
234 id: &'static str,
235 action: &'static str,
236 part: &'static str,
237 caption: String,
238 fewest: usize,
239}
240
241impl ViewController for ChoicesController {
242 fn id(&self) -> &str {
243 self.id
244 }
245 fn actions(&self) -> Vec<Action> {
246 let part = self.part;
247 vec![
248 Action::single_choice(self.action, self.caption.clone(), Vec::new())
249 .category(ActionCategory::View)
250 .on_execute(move |cx| {
251 let session = session_of(cx)?;
252 let view = cx.view.as_ref().ok_or_else(|| "no view".to_owned())?;
253 match &cx.input {
254 ActionInput::Choice(choice) => session.choose(&view.id, part, choice),
255 _ => Ok(()),
256 }
257 }),
258 ]
259 }
260 fn activated(&mut self, frame: &Frame) {
261 let enough = frame.view().zip(frame.model()).is_some_and(|(view, model)| {
262 model.at(&["Views", &view.id, self.part]).is_some_and(|all| all.children.len() >= self.fewest)
263 });
264 if let Some(action) = frame.action(self.action) {
265 action.set_active("choices", enough);
266 }
267 }
268}
269
270struct LinkController(Words);
271
272fn point(
274 cx: &ravenrs_app::ActionContext,
275 record: &ravenrs_app::Object,
276 field: &str,
277 to: Value,
278) -> Result<(), String> {
279 let mut record = record.clone();
280 record.set(field, to);
281 cx.space.borrow_mut().save(record).map_err(|e| e.to_string())
282}
283
284fn keep(cx: &ravenrs_app::ActionContext) -> Result<(), String> {
285 let kept = cx.space.borrow_mut().commit().map_err(|e| e.to_string());
286 if kept.is_err() {
287 cx.space.borrow_mut().rollback();
288 }
289 kept?;
290 session_of(cx)?.touch();
291 Ok(())
292}
293
294impl ViewController for LinkController {
295 fn id(&self) -> &str {
296 "LinkObjects"
297 }
298 fn scope(&self) -> TargetScope {
299 TargetScope::any().view_kind(ViewKind::List).nesting(Nesting::Nested)
300 }
301 fn actions(&self) -> Vec<Action> {
302 vec![
303 Action::simple(ids::LINK, self.0.link.clone())
304 .category(ActionCategory::Edit)
305 .icon("link")
306 .on_execute(|cx| {
307 let owner = cx.frame.extension::<NestedOwner>().ok_or_else(|| "no list".to_owned())?;
308 let ActionInput::Parameter(key) = &cx.input else {
309 let free = Criteria::parse(&format!("IsNull([{}])", owner.field)).ok().map(|empty| {
312 let elsewhere = Criteria::parse(&format!(
313 "[{}] <> {}",
314 owner.field,
315 crate::editors::literal_of(&owner.key)
316 ));
317 elsewhere.map_or(empty.clone(), |elsewhere| empty.or(elsewhere))
318 });
319 let filter = match (free, owner.link_filter.clone()) {
320 (Some(free), Some(own)) => Some(free.and(own)),
321 (free, own) => free.or(own),
322 };
323 session_of(cx)?.set_popup_filter(filter);
324 cx.frame.open_popup_on(ids::LINK, &format!("{}_LookupListView", owner.target));
325 return Ok(());
326 };
327 let found = cx.space.borrow().find(&owner.target, key).map_err(|e| e.to_string())?;
328 let Some(record) = found else { return Ok(()) };
329 point(cx, &record, &owner.field, owner.key.clone())?;
330 keep(cx)
331 }),
332 Action::simple(ids::UNLINK, self.0.unlink.clone())
333 .category(ActionCategory::Edit)
334 .icon("unlink")
335 .selection(Selection::RequireMultiple)
336 .on_execute(|cx| {
337 let owner = cx.frame.extension::<NestedOwner>().ok_or_else(|| "no list".to_owned())?;
338 for record in &cx.selected {
339 point(cx, record, &owner.field, Value::Null)?;
340 }
341 keep(cx)
342 }),
343 ]
344 }
345}
346
347impl Module for SystemModule {
348 fn name(&self) -> &str {
349 "System"
350 }
351
352 fn view_controllers(&self) -> Vec<Box<dyn ViewController>> {
353 let words = || self.words.clone();
354 vec![
355 Box::new(RecordsNavigationController(words())),
356 Box::new(NewObjectController(words())),
357 Box::new(ModificationsController(words())),
358 Box::new(DeleteController(words())),
359 Box::new(RefreshController(words())),
360 Box::new(ChoicesController {
361 id: "ViewVariants",
362 action: ids::VARIANTS,
363 part: "Variants",
364 caption: self.words.variant.clone(),
365 fewest: 2,
367 }),
368 Box::new(ChoicesController {
369 id: "ListFilters",
370 action: ids::FILTERS,
371 part: "Filters",
372 caption: self.words.filter.clone(),
373 fewest: 1,
374 }),
375 Box::new(LinkController(words())),
376 ]
377 }
378}
379
380struct CloneController(Words);
381
382impl ViewController for CloneController {
383 fn id(&self) -> &str {
384 "CloneObject"
385 }
386 fn scope(&self) -> TargetScope {
387 TargetScope::any().nesting(Nesting::Root)
388 }
389 fn actions(&self) -> Vec<Action> {
390 let unsaved = self.0.clone_unsaved.clone();
391 vec![
392 Action::simple(ids::CLONE, self.0.clone.clone())
393 .category(ActionCategory::Edit)
394 .icon("copy")
395 .selection(Selection::RequireSingle)
396 .on_execute(move |cx| {
397 let session = session_of(cx)?;
398 if session.has_unsaved_changes() {
400 return Err(unsaved.clone());
401 }
402 let source = cx.selected.first().ok_or_else(|| "nothing selected".to_owned())?;
403 session.clone_record(source)
404 }),
405 ]
406 }
407 fn activated(&mut self, frame: &Frame) {
408 let cloneable = frame.view().and_then(|view| view.entity).is_some_and(|name| {
409 Session::of_frame(frame).and_then(|session| session.entity(&name)).is_some_and(|e| e.cloneable)
410 });
411 if let Some(action) = frame.action(ids::CLONE) {
412 action.set_active("cloneable", cloneable);
413 }
414 }
415}
416
417#[derive(Debug, Default)]
421#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
422pub struct CloneModule {
423 words: Words,
424}
425
426#[cfg_attr(feature = "live", ravenrs_live::expose)]
427impl CloneModule {
428 pub fn with_words(words: Words) -> Self {
430 Self { words }
431 }
432}
433
434impl Module for CloneModule {
435 fn name(&self) -> &str {
436 "CloneObject"
437 }
438
439 fn view_controllers(&self) -> Vec<Box<dyn ViewController>> {
440 vec![Box::new(CloneController(self.words.clone()))]
441 }
442}