Skip to main content

ravenrs_app/
action.rs

1//! What people can do: actions, when they apply, and what happens when they run.
2
3use std::cell::RefCell;
4use std::collections::BTreeMap;
5use std::fmt;
6use std::rc::Rc;
7
8use ravenrs_criteria::{Criteria, Environment, Scope, Value};
9use ravenrs_reactive::State;
10
11use crate::frame::{FrameContext, ViewInfo, ViewKind};
12use crate::{EntityInfo, FieldKind, ModelError, Object, ObjectSpace};
13
14/// The reason an action or a controller is off because its view is not the one it targets.
15pub const REASON_SCOPE: &str = "scope";
16/// The reason an action is off because its controller is.
17pub const REASON_CONTROLLER: &str = "controller";
18/// The reason an action is off because of what is selected.
19pub const REASON_SELECTION: &str = "selection";
20/// The reason an action is off because the selected records do not meet its criteria.
21pub const REASON_CRITERIA: &str = "criteria";
22
23/// The reason an action is off because of an appearance rule.
24pub const REASON_APPEARANCE: &str = "appearance";
25/// The reason permissions turned an action off.
26pub const REASON_SECURITY: &str = "security";
27/// Off while its work runs away from the screen.
28pub const REASON_RUNNING: &str = "running";
29
30/// Why something is on or off: pairs of reason and answer. It is on only if every answer is yes,
31/// so one part of the application turning it off cannot be undone by another.
32#[derive(Clone, Debug, Default, PartialEq, Eq)]
33#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
34pub struct Reasons(BTreeMap<String, bool>);
35
36#[cfg_attr(feature = "live", ravenrs_live::expose)]
37impl Reasons {
38    /// Whether every answer is yes.
39    pub fn all(&self) -> bool {
40        self.0.values().all(|on| *on)
41    }
42
43    /// Gives the answer for a reason.
44    pub fn set(&mut self, reason: &str, on: bool) {
45        self.0.insert(reason.to_owned(), on);
46    }
47
48    /// The answer for a reason, if it was given.
49    pub fn get(&self, reason: &str) -> Option<bool> {
50        self.0.get(reason).copied()
51    }
52
53    /// The reasons that say no.
54    pub fn against(&self) -> impl Iterator<Item = &str> {
55        self.0.iter().filter(|(_, on)| !**on).map(|(reason, _)| reason.as_str())
56    }
57}
58
59/// What an action needs selected to be enabled.
60#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
61#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
62pub enum Selection {
63    /// Nothing in particular.
64    #[default]
65    Independent,
66    /// Exactly one record.
67    RequireSingle,
68    /// One record or more.
69    RequireMultiple,
70}
71
72#[cfg_attr(feature = "live", ravenrs_live::expose)]
73impl Selection {
74    fn allows(self, count: usize) -> bool {
75        match self {
76            Self::Independent => true,
77            Self::RequireSingle => count == 1,
78            Self::RequireMultiple => count >= 1,
79        }
80    }
81}
82
83/// How [`Action::target_criteria`] counts the selected records.
84#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
85#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
86pub enum CriteriaMode {
87    /// All of them have to meet it.
88    #[default]
89    TrueForAll,
90    /// At least one has to.
91    TrueAtLeastForOne,
92}
93
94/// Where an action belongs, which decides the place it shows in.
95#[derive(Clone, Debug, Default, PartialEq, Eq)]
96#[non_exhaustive]
97#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
98pub enum ActionCategory {
99    /// Creating records.
100    New,
101    /// Changing records.
102    #[default]
103    Edit,
104    /// Saving and canceling.
105    Save,
106    /// Filtering and searching.
107    Filter,
108    /// Moving between records and views.
109    Navigation,
110    /// How the view shows things.
111    View,
112    /// Taking data out.
113    Export,
114    /// Anything else, with its name.
115    Custom(String),
116    /// On a form, inside its layout and not above it, in the place with this name that its entity
117    /// asks for with [`EntityInfo::form_actions`](crate::EntityInfo::form_actions).
118    InForm(String),
119}
120
121/// Whether views inside others count.
122#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
123#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
124pub enum Nesting {
125    /// Any view.
126    #[default]
127    Any,
128    /// Only a view of its own window.
129    Root,
130    /// Only a view inside another.
131    Nested,
132}
133
134/// Which views an action or a controller is for. Anything left out does not narrow it.
135#[derive(Clone, Debug, Default, PartialEq, Eq)]
136#[non_exhaustive]
137#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
138pub struct TargetScope {
139    /// Only views of this entity.
140    pub entity: Option<String>,
141    /// Only the view with this id.
142    pub view_id: Option<String>,
143    /// Only views of this kind.
144    pub view_kind: Option<ViewKind>,
145    /// Root or nested.
146    pub nesting: Nesting,
147}
148
149#[cfg_attr(feature = "live", ravenrs_live::expose)]
150impl TargetScope {
151    /// Every view.
152    pub fn any() -> Self {
153        Self::default()
154    }
155
156    /// Only views of this entity.
157    pub fn entity(mut self, entity: impl Into<String>) -> Self {
158        self.entity = Some(entity.into());
159        self
160    }
161
162    /// Only the view with this id.
163    pub fn view_id(mut self, id: impl Into<String>) -> Self {
164        self.view_id = Some(id.into());
165        self
166    }
167
168    /// Only views of this kind.
169    pub fn view_kind(mut self, kind: ViewKind) -> Self {
170        self.view_kind = Some(kind);
171        self
172    }
173
174    /// Root or nested.
175    pub fn nesting(mut self, nesting: Nesting) -> Self {
176        self.nesting = nesting;
177        self
178    }
179
180    /// Whether it covers this view, in a frame of this context. No view is covered
181    /// only by what asks nothing of one.
182    pub fn covers(&self, view: Option<&ViewInfo>, context: FrameContext) -> bool {
183        let nesting = match self.nesting {
184            Nesting::Any => true,
185            Nesting::Root => context != FrameContext::Nested,
186            Nesting::Nested => context == FrameContext::Nested,
187        };
188        nesting
189            && match view {
190                Some(view) => {
191                    self.entity.as_ref().is_none_or(|e| view.entity.as_ref() == Some(e))
192                        && self.view_id.as_ref().is_none_or(|id| *id == view.id)
193                        && self.view_kind.is_none_or(|kind| kind == view.kind)
194                }
195                None => self.entity.is_none() && self.view_id.is_none() && self.view_kind.is_none(),
196            }
197    }
198}
199
200/// One option of a single-choice action, which can hold others.
201#[derive(Clone, Debug, PartialEq, Eq)]
202#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
203pub struct ChoiceItem {
204    /// What the handler receives.
205    pub id: String,
206    /// What people see.
207    pub caption: String,
208    /// The options under this one.
209    pub items: Vec<ChoiceItem>,
210}
211
212#[cfg_attr(feature = "live", ravenrs_live::expose)]
213impl ChoiceItem {
214    /// An option with nothing under it.
215    pub fn new(id: impl Into<String>, caption: impl Into<String>) -> Self {
216        Self { id: id.into(), caption: caption.into(), items: Vec::new() }
217    }
218
219    /// With this option under it.
220    pub fn item(mut self, item: ChoiceItem) -> Self {
221        self.items.push(item);
222        self
223    }
224
225    fn has(&self, id: &str) -> bool {
226        self.id == id || self.items.iter().any(|item| item.has(id))
227    }
228}
229
230/// How an action asks for what it needs.
231#[derive(Clone, Debug, PartialEq)]
232#[non_exhaustive]
233#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
234pub enum ActionKind {
235    /// A button: it just runs.
236    Simple,
237    /// Opens a view in a window, and runs on OK.
238    Popup {
239        /// The view it opens.
240        view: String,
241    },
242    /// Offers options, and runs with the one picked.
243    SingleChoice(Vec<ChoiceItem>),
244    /// Asks for a value, and runs with it.
245    Parametrized(FieldKind),
246    /// Opens an address made from the selected record, where `{field}` takes its value.
247    Url(String),
248}
249
250/// What the running of an action asks for next, by whoever shows the application.
251#[derive(Clone, Debug, PartialEq)]
252#[non_exhaustive]
253#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
254pub enum ActionResult {
255    /// Nothing else.
256    Done,
257    /// Open this view in a window; [`Frame::accept_popup`](crate::Frame::accept_popup) or
258    /// [`Frame::cancel_popup`](crate::Frame::cancel_popup) ends it.
259    Popup(String),
260    /// Show this view in the frame.
261    ShowView(String),
262    /// Open this address.
263    OpenUrl(String),
264    /// Records changed, so what shows them has to read again.
265    RecordsChanged,
266    /// Its work started away from the screen, as the job with this id.
267    Started(u64),
268}
269
270/// What people gave an action that asks for something.
271#[derive(Clone, Debug, Default, PartialEq)]
272#[non_exhaustive]
273#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
274pub enum ActionInput {
275    /// Nothing.
276    #[default]
277    None,
278    /// The option picked.
279    Choice(String),
280    /// The value typed.
281    Parameter(Value),
282    /// The record filled in on a form.
283    Record(Object),
284}
285
286/// How much a [`Message`] matters.
287#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
288#[non_exhaustive]
289#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
290pub enum MessageKind {
291    /// Something to know.
292    #[default]
293    Information,
294    /// It went well.
295    Success,
296    /// Something to look at.
297    Warning,
298    /// It went badly.
299    Error,
300}
301
302/// Something to tell people, which goes away by itself.
303#[derive(Clone)]
304#[non_exhaustive]
305#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
306pub struct Message {
307    /// What it says.
308    pub text: String,
309    /// How much it matters.
310    pub kind: MessageKind,
311    /// How long it shows; nothing means the usual.
312    pub seconds: Option<f64>,
313    /// A button on it: what it says and what it does. It closes the message.
314    pub action: Option<(String, Rc<dyn Fn()>)>,
315    /// What happens once it goes away, by itself or closed by a person.
316    pub on_close: Option<Rc<dyn Fn()>>,
317}
318
319impl fmt::Debug for Message {
320    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
321        f.debug_struct("Message").field("text", &self.text).field("kind", &self.kind).finish_non_exhaustive()
322    }
323}
324
325#[cfg_attr(feature = "live", ravenrs_live::expose)]
326impl Message {
327    /// A message saying this.
328    pub fn new(text: impl Into<String>, kind: MessageKind) -> Self {
329        Self { text: text.into(), kind, seconds: None, action: None, on_close: None }
330    }
331
332    /// Shows for this long.
333    pub fn seconds(mut self, seconds: f64) -> Self {
334        self.seconds = Some(seconds);
335        self
336    }
337
338    /// A button on it, which closes the message.
339    pub fn action(mut self, label: impl Into<String>, then: impl Fn() + 'static) -> Self {
340        self.action = Some((label.into(), Rc::new(then)));
341        self
342    }
343
344    /// Runs this once it goes away.
345    pub fn on_close(mut self, then: impl Fn() + 'static) -> Self {
346        self.on_close = Some(Rc::new(then));
347        self
348    }
349}
350
351/// What an action sees while running. A copy is the same run: what it asks for next counts.
352#[derive(Clone)]
353#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
354pub struct ActionContext {
355    /// The frame it runs in.
356    pub frame: crate::Frame,
357    /// The records to read and write. A handler commits when it means to.
358    pub space: Rc<RefCell<dyn ObjectSpace>>,
359    /// The records selected.
360    pub selected: Vec<Object>,
361    /// The view it runs on.
362    pub view: Option<ViewInfo>,
363    /// What people gave it.
364    pub input: ActionInput,
365    next: Rc<RefCell<Option<ActionResult>>>,
366}
367
368impl fmt::Debug for ActionContext {
369    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
370        f.debug_struct("ActionContext").field("selected", &self.selected.len()).finish_non_exhaustive()
371    }
372}
373
374#[cfg_attr(feature = "live", ravenrs_live::expose)]
375impl ActionContext {
376    pub(crate) fn new(
377        frame: crate::Frame,
378        space: Rc<RefCell<dyn ObjectSpace>>,
379        selected: Vec<Object>,
380        view: Option<ViewInfo>,
381        input: ActionInput,
382    ) -> Self {
383        Self { frame, space, selected, view, input, next: Rc::default() }
384    }
385
386    /// Something the application gave its actions, by its type.
387    pub fn service<T: 'static>(&self) -> Option<Rc<T>> {
388        self.frame.extension::<T>()
389    }
390
391    /// Tells people something, as long as whoever shows the application listens.
392    pub fn message(&self, message: Message) {
393        self.frame.show_message(message);
394    }
395
396    /// Asks the frame to show this view when the action ends.
397    pub fn show_view(&self, id: impl Into<String>) {
398        *self.next.borrow_mut() = Some(ActionResult::ShowView(id.into()));
399    }
400
401    /// Tells whoever shows the application that records changed, so what shows them reads again.
402    pub fn records_changed(&self) {
403        *self.next.borrow_mut() = Some(ActionResult::RecordsChanged);
404    }
405
406    pub(crate) fn take_next(&self) -> Option<ActionResult> {
407        self.next.borrow_mut().take()
408    }
409}
410
411/// Why an action did not run.
412#[derive(Clone, Debug, PartialEq, Eq)]
413#[non_exhaustive]
414#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
415pub enum ActionError {
416    /// No action has this id.
417    Unknown(String),
418    /// It is out of place here, with the reasons.
419    Inactive(Vec<String>),
420    /// It is shown but off, with the reasons.
421    Disabled(Vec<String>),
422    /// Something asked first said no.
423    Canceled,
424    /// What people gave it is not what it asks for.
425    BadInput(String),
426    /// It ran and failed, in words for people.
427    Failed(String),
428}
429
430impl fmt::Display for ActionError {
431    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
432        match self {
433            Self::Unknown(id) => write!(f, "there is no action {id}"),
434            Self::Inactive(why) => write!(f, "the action is out of place: {}", why.join(", ")),
435            Self::Disabled(why) => write!(f, "the action is off: {}", why.join(", ")),
436            Self::Canceled => f.write_str("the action was canceled"),
437            Self::BadInput(why) | Self::Failed(why) => f.write_str(why),
438        }
439    }
440}
441
442impl std::error::Error for ActionError {}
443
444type Handler = Rc<dyn Fn(&ActionContext) -> Result<(), String>>;
445type Guard = Rc<dyn Fn(&ActionContext) -> bool>;
446type Notice = Rc<dyn Fn(&ActionContext)>;
447type Prepare = Rc<dyn Fn(&ActionContext, &mut Object)>;
448
449#[derive(Clone)]
450struct ActionInner {
451    id: String,
452    caption: String,
453    kind: ActionKind,
454    category: ActionCategory,
455    scope: TargetScope,
456    selection: Selection,
457    criteria: Option<(String, CriteriaMode)>,
458    validation_contexts: Vec<String>,
459    confirmation: Option<String>,
460    confirmation_detail: Option<String>,
461    confirmation_answer: Option<String>,
462    prepare: Option<Prepare>,
463    hotkey: Option<String>,
464    icon: Option<String>,
465    accessible_name: Option<String>,
466    accessible_description: Option<String>,
467    requires: Option<crate::Operation>,
468    on_execute: Option<Handler>,
469    on_cancel: Option<Notice>,
470    on_executing: Option<Guard>,
471    on_executed: Option<Notice>,
472    background: Option<crate::JobWork>,
473    problem: Option<String>,
474    active: State<Reasons>,
475    enabled: State<Reasons>,
476}
477
478/// Something people can do. A handle: the frame and its controller hold the same action, so a
479/// controller can turn it off by a reason of its own.
480///
481/// ```
482/// use ravenrs_app::{Action, ActionCategory, CriteriaMode, Selection};
483///
484/// let pay = Action::simple("Pay", "Pagar")
485///     .category(ActionCategory::Edit)
486///     .target_entity("Bill")
487///     .selection(Selection::RequireMultiple)
488///     .target_criteria("[Status] = 'Open'", CriteriaMode::TrueForAll)
489///     .on_execute(|cx| {
490///         for bill in &cx.selected {
491///             let mut bill = bill.clone();
492///             bill.set("Status", "Paid");
493///             cx.space.borrow_mut().save(bill).map_err(|e| e.to_string())?;
494///         }
495///         cx.space.borrow_mut().commit().map_err(|e| e.to_string())
496///     });
497/// assert_eq!(pay.caption(), "Pagar");
498/// ```
499#[derive(Clone)]
500#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
501pub struct Action(Rc<ActionInner>);
502
503impl fmt::Debug for Action {
504    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
505        f.debug_struct("Action").field("id", &self.0.id).finish_non_exhaustive()
506    }
507}
508
509#[cfg_attr(feature = "live", ravenrs_live::expose)]
510impl Action {
511    fn new(id: impl Into<String>, caption: impl Into<String>, kind: ActionKind) -> Self {
512        Self(Rc::new(ActionInner {
513            id: id.into(),
514            caption: caption.into(),
515            kind,
516            category: ActionCategory::default(),
517            scope: TargetScope::default(),
518            selection: Selection::default(),
519            criteria: None,
520            validation_contexts: Vec::new(),
521            confirmation: None,
522            confirmation_detail: None,
523            confirmation_answer: None,
524            prepare: None,
525            hotkey: None,
526            requires: None,
527            icon: None,
528            accessible_name: None,
529            accessible_description: None,
530            on_execute: None,
531            on_cancel: None,
532            on_executing: None,
533            on_executed: None,
534            background: None,
535            problem: None,
536            active: State::new(Reasons::default()),
537            enabled: State::new(Reasons::default()),
538        }))
539    }
540
541    /// A button: it just runs.
542    pub fn simple(id: impl Into<String>, caption: impl Into<String>) -> Self {
543        Self::new(id, caption, ActionKind::Simple)
544    }
545
546    /// A button whose work runs away from the screen, on a thread of its own: it shows how far it
547    /// got, can be stopped, and what it hands back is kept on the screen's thread. While it runs,
548    /// the action is off, so it never runs twice at once.
549    ///
550    /// ```
551    /// use ravenrs_app::{Action, Selection};
552    ///
553    /// let archive = Action::background("Archive", "Arquivar", |job| {
554    ///     for record in job.selected() {
555    ///         let mut record = record.clone();
556    ///         record.set("Archived", true);
557    ///         job.save(record);
558    ///     }
559    ///     Ok(())
560    /// })
561    /// .selection(Selection::RequireMultiple);
562    /// assert!(archive.is_background());
563    /// ```
564    pub fn background(
565        id: impl Into<String>,
566        caption: impl Into<String>,
567        work: impl Fn(&crate::JobContext) -> Result<(), String> + Send + Sync + 'static,
568    ) -> Self {
569        Self::simple(id, caption).in_background(work)
570    }
571
572    /// Opens a view in a window, and runs on OK.
573    pub fn popup(id: impl Into<String>, caption: impl Into<String>, view: impl Into<String>) -> Self {
574        Self::new(id, caption, ActionKind::Popup { view: view.into() })
575    }
576
577    /// Offers these options, and runs with the one picked.
578    pub fn single_choice(id: impl Into<String>, caption: impl Into<String>, items: Vec<ChoiceItem>) -> Self {
579        Self::new(id, caption, ActionKind::SingleChoice(items))
580    }
581
582    /// Asks for a value of this kind, and runs with it.
583    pub fn parametrized(id: impl Into<String>, caption: impl Into<String>, kind: FieldKind) -> Self {
584        Self::new(id, caption, ActionKind::Parametrized(kind))
585    }
586
587    /// Opens an address made from the selected record, where `{field}` takes its value.
588    pub fn url(id: impl Into<String>, caption: impl Into<String>, template: impl Into<String>) -> Self {
589        Self::new(id, caption, ActionKind::Url(template.into()))
590    }
591
592    fn change(mut self, f: impl FnOnce(&mut ActionInner)) -> Self {
593        // While it is being declared nothing else holds it, unless a copy was kept on the way, as
594        // the playground's interpreter does: then the change goes to an action of its own, which
595        // starts with the same reasons and does not share them.
596        if Rc::get_mut(&mut self.0).is_none() {
597            let mut copy = (*self.0).clone();
598            copy.active = State::new(self.0.active.get());
599            copy.enabled = State::new(self.0.enabled.get());
600            self.0 = Rc::new(copy);
601        }
602        if let Some(inner) = Rc::get_mut(&mut self.0) {
603            f(inner);
604        }
605        self
606    }
607
608    /// Runs this work away from the screen once it runs, after `on_execute`, with what people gave
609    /// it. See [`Action::background`].
610    pub fn in_background(
611        self,
612        work: impl Fn(&crate::JobContext) -> Result<(), String> + Send + Sync + 'static,
613    ) -> Self {
614        self.change(|a| a.background = Some(std::sync::Arc::new(work)))
615    }
616
617    /// Whether its work runs away from the screen.
618    pub fn is_background(&self) -> bool {
619        self.0.background.is_some()
620    }
621
622    pub(crate) fn work(&self) -> Option<crate::JobWork> {
623        self.0.background.clone()
624    }
625
626    /// Where it belongs.
627    pub fn category(self, category: ActionCategory) -> Self {
628        self.change(|a| a.category = category)
629    }
630
631    /// Only on views of this entity.
632    pub fn target_entity(self, entity: impl Into<String>) -> Self {
633        self.change(|a| a.scope.entity = Some(entity.into()))
634    }
635
636    /// Only on the view with this id.
637    pub fn target_view(self, id: impl Into<String>) -> Self {
638        self.change(|a| a.scope.view_id = Some(id.into()))
639    }
640
641    /// Only on views of this kind.
642    pub fn target_view_kind(self, kind: ViewKind) -> Self {
643        self.change(|a| a.scope.view_kind = Some(kind))
644    }
645
646    /// Only on root views, or only on nested ones.
647    pub fn nesting(self, nesting: Nesting) -> Self {
648        self.change(|a| a.scope.nesting = nesting)
649    }
650
651    /// What it needs selected.
652    pub fn selection(self, selection: Selection) -> Self {
653        self.change(|a| a.selection = selection)
654    }
655
656    /// What the selected records have to meet. With nothing selected, it does not stop anything;
657    /// [`selection`](Self::selection) says whether something has to be.
658    pub fn target_criteria(self, criteria: impl Into<String>, mode: CriteriaMode) -> Self {
659        self.change(|a| a.criteria = Some((criteria.into(), mode)))
660    }
661
662    /// The validation contexts to pass before it runs.
663    pub fn validation_contexts(self, contexts: impl IntoIterator<Item = impl Into<String>>) -> Self {
664        self.change(|a| a.validation_contexts = contexts.into_iter().map(Into::into).collect())
665    }
666
667    /// A question to ask before it runs.
668    pub fn confirmation(self, text: impl Into<String>) -> Self {
669        self.change(|a| a.confirmation = Some(text.into()))
670    }
671
672    /// What the question says besides, and what the answer that runs it says.
673    pub fn confirmation_words(self, detail: impl Into<String>, answer: impl Into<String>) -> Self {
674        self.change(|a| {
675            a.confirmation_detail = Some(detail.into());
676            a.confirmation_answer = Some(answer.into());
677        })
678    }
679
680    /// For a popup that opens a form: fills in the record it starts with.
681    pub fn on_prepare(self, handler: impl Fn(&ActionContext, &mut Object) + 'static) -> Self {
682        self.change(|a| a.prepare = Some(Rc::new(handler)))
683    }
684
685    /// What it does to the records of its view, so it fades for who cannot. The secured space
686    /// refuses it anyway.
687    pub fn requires(self, operation: crate::Operation) -> Self {
688        self.change(|a| a.requires = Some(operation))
689    }
690
691    /// The keys that run it, in the words of whoever shows it.
692    pub fn hotkey(self, keys: impl Into<String>) -> Self {
693        self.change(|a| a.hotkey = Some(keys.into()))
694    }
695
696    /// The name of its icon.
697    pub fn icon(self, icon: impl Into<String>) -> Self {
698        self.change(|a| a.icon = Some(icon.into()))
699    }
700
701    /// What screen readers call it, instead of its caption: for a caption that only makes sense
702    /// on the screen, such as an arrow.
703    pub fn accessible_name(self, name: impl Into<String>) -> Self {
704        self.change(|a| a.accessible_name = Some(name.into()))
705    }
706
707    /// What screen readers say after its name, such as what it does to the selected records.
708    pub fn accessible_description(self, text: impl Into<String>) -> Self {
709        self.change(|a| a.accessible_description = Some(text.into()))
710    }
711
712    /// What it does.
713    pub fn on_execute(self, handler: impl Fn(&ActionContext) -> Result<(), String> + 'static) -> Self {
714        self.change(|a| a.on_execute = Some(Rc::new(handler)))
715    }
716
717    /// Runs as an update: sets fields of every record selected of this entity, as
718    /// `field = expression; ...`, each read from the record as it was; checks them against the
719    /// rules of `Save`, and keeps them all or none. What does not parse, or names a field the
720    /// entity has not, is found when the application is put together.
721    ///
722    /// ```
723    /// use ravenrs_app::{Action, EntityInfo, FieldInfo, FieldKind, Selection};
724    ///
725    /// let task = EntityInfo::new("Task")
726    ///     .field(FieldInfo::new("id", FieldKind::Integer).key())
727    ///     .field(FieldInfo::new("state", FieldKind::Text));
728    /// let finish = Action::simple("Finish", "Concluir")
729    ///     .target_entity("Task")
730    ///     .selection(Selection::RequireMultiple)
731    ///     .assign(&task, "state = 'done'");
732    /// assert_eq!(finish.caption(), "Concluir");
733    /// ```
734    pub fn assign(self, entity: &EntityInfo, set: &str) -> Self {
735        match crate::record_action::assignments(entity, set) {
736            Ok(found) => {
737                let entity = entity.clone();
738                self.on_execute(move |cx| crate::record_action::assign(&entity, &found, cx))
739            }
740            Err(problem) => self.change(|a| a.problem = Some(problem)),
741        }
742    }
743
744    /// What a popup does when canceled.
745    pub fn on_cancel(self, handler: impl Fn(&ActionContext) + 'static) -> Self {
746        self.change(|a| a.on_cancel = Some(Rc::new(handler)))
747    }
748
749    /// Asked before it runs; false stops it.
750    pub fn on_executing(self, guard: impl Fn(&ActionContext) -> bool + 'static) -> Self {
751        self.change(|a| a.on_executing = Some(Rc::new(guard)))
752    }
753
754    /// Told after it ran.
755    pub fn on_executed(self, notice: impl Fn(&ActionContext) + 'static) -> Self {
756        self.change(|a| a.on_executed = Some(Rc::new(notice)))
757    }
758
759    /// Its id.
760    pub fn id(&self) -> &str {
761        &self.0.id
762    }
763
764    /// What people see.
765    pub fn caption(&self) -> &str {
766        &self.0.caption
767    }
768
769    /// How it asks for what it needs.
770    pub fn kind(&self) -> &ActionKind {
771        &self.0.kind
772    }
773
774    /// Where it belongs.
775    pub fn action_category(&self) -> &ActionCategory {
776        &self.0.category
777    }
778
779    /// The question to ask before it runs.
780    pub fn confirmation_text(&self) -> Option<&str> {
781        self.0.confirmation.as_deref()
782    }
783
784    /// What the question says besides.
785    pub fn confirmation_detail(&self) -> Option<&str> {
786        self.0.confirmation_detail.as_deref()
787    }
788
789    /// What the answer that runs it says.
790    pub fn confirmation_answer(&self) -> Option<&str> {
791        self.0.confirmation_answer.as_deref()
792    }
793
794    pub(crate) fn run_prepare(&self, cx: &ActionContext, object: &mut Object) {
795        if let Some(prepare) = &self.0.prepare {
796            prepare(cx, object);
797        }
798    }
799
800    /// What it does to the records of its view, if it said.
801    pub fn required_operation(&self) -> Option<crate::Operation> {
802        self.0.requires
803    }
804
805    /// The keys that run it.
806    pub fn hotkey_text(&self) -> Option<&str> {
807        self.0.hotkey.as_deref()
808    }
809
810    /// The name of its icon.
811    pub fn icon_name(&self) -> Option<&str> {
812        self.0.icon.as_deref()
813    }
814
815    /// What screen readers call it, when it is not its caption.
816    pub fn accessible_name_text(&self) -> Option<&str> {
817        self.0.accessible_name.as_deref()
818    }
819
820    /// What screen readers say after its name, when anything.
821    pub fn accessible_description_text(&self) -> Option<&str> {
822        self.0.accessible_description.as_deref()
823    }
824
825    /// The validation contexts to pass before it runs.
826    pub fn contexts(&self) -> &[String] {
827        &self.0.validation_contexts
828    }
829
830    /// Whether it is in place. If not, it is not shown.
831    pub fn is_active(&self) -> bool {
832        self.0.active.with(Reasons::all)
833    }
834
835    /// Whether it can be used. If not, it is shown faded.
836    pub fn is_enabled(&self) -> bool {
837        self.0.enabled.with(Reasons::all)
838    }
839
840    /// Why it is or is not in place.
841    pub fn active_reasons(&self) -> Reasons {
842        self.0.active.get()
843    }
844
845    /// Why it can or cannot be used.
846    pub fn enabled_reasons(&self) -> Reasons {
847        self.0.enabled.get()
848    }
849
850    /// Gives a reason for it being in place or not.
851    pub fn set_active(&self, reason: &str, on: bool) {
852        if self.0.active.peek().get(reason) != Some(on) {
853            self.0.active.update(|reasons| reasons.set(reason, on));
854        }
855    }
856
857    /// Gives a reason for it being usable or not. Prefer this to
858    /// [`set_active`](Self::set_active) when it depends on the record: taking a button away
859    /// moves the ones beside it, and people click the wrong one.
860    pub fn set_enabled(&self, reason: &str, on: bool) {
861        if self.0.enabled.peek().get(reason) != Some(on) {
862            self.0.enabled.update(|reasons| reasons.set(reason, on));
863        }
864    }
865
866    /// Says what is wrong with how it was declared.
867    pub(crate) fn check(&self) -> Result<(), ModelError> {
868        if let Some(problem) = &self.0.problem {
869            return Err(ModelError(format!("the action {}: {problem}", self.0.id)));
870        }
871        if let Some((text, _)) = &self.0.criteria {
872            Criteria::parse(text).map_err(|e| ModelError(format!("the action {}: {e}", self.0.id)))?;
873        }
874        Ok(())
875    }
876
877    /// Answers the reasons that follow from the frame.
878    pub(crate) fn update(
879        &self,
880        view: Option<&ViewInfo>,
881        context: FrameContext,
882        selected: &[Object],
883        environment: &Environment,
884    ) {
885        self.set_active(REASON_SCOPE, self.0.scope.covers(view, context));
886        self.set_enabled(REASON_SELECTION, self.0.selection.allows(selected.len()));
887        self.set_enabled(REASON_CRITERIA, self.meets_criteria(selected, environment));
888    }
889
890    fn meets_criteria(&self, selected: &[Object], environment: &Environment) -> bool {
891        let Some((text, mode)) = &self.0.criteria else { return true };
892        if selected.is_empty() {
893            return true;
894        }
895        // `check` refuses what does not parse, when the application is put together.
896        let Ok(criteria) = Criteria::parse(text) else { return false };
897        let meets = |object: &Object| Scope::new(environment).row(object).matches(&criteria);
898        match mode {
899            CriteriaMode::TrueForAll => selected.iter().all(meets),
900            CriteriaMode::TrueAtLeastForOne => selected.iter().any(meets),
901        }
902    }
903
904    /// Says whether what people gave it is what it asks for.
905    pub(crate) fn check_input(&self, input: &ActionInput) -> Result<(), ActionError> {
906        match (&self.0.kind, input) {
907            (ActionKind::SingleChoice(items), ActionInput::Choice(id)) => {
908                // With no options declared, they come from the view it is on.
909                if items.is_empty() || items.iter().any(|item| item.has(id)) {
910                    Ok(())
911                } else {
912                    Err(ActionError::BadInput(format!("{} has no option {id}", self.0.id)))
913                }
914            }
915            (ActionKind::SingleChoice(_), _) => {
916                Err(ActionError::BadInput(format!("{} needs an option", self.0.id)))
917            }
918            (ActionKind::Parametrized(_), ActionInput::Parameter(_)) => Ok(()),
919            (ActionKind::Parametrized(_), _) => {
920                Err(ActionError::BadInput(format!("{} needs a value", self.0.id)))
921            }
922            _ => Ok(()),
923        }
924    }
925
926    pub(crate) fn run_guard(&self, cx: &ActionContext) -> bool {
927        self.0.on_executing.as_ref().is_none_or(|guard| guard(cx))
928    }
929
930    pub(crate) fn run(&self, cx: &ActionContext) -> Result<(), String> {
931        self.0.on_execute.as_ref().map_or(Ok(()), |handler| handler(cx))
932    }
933
934    pub(crate) fn run_notice(&self, cx: &ActionContext) {
935        if let Some(notice) = &self.0.on_executed {
936            notice(cx);
937        }
938    }
939
940    pub(crate) fn run_cancel(&self, cx: &ActionContext) {
941        if let Some(notice) = &self.0.on_cancel {
942            notice(cx);
943        }
944    }
945}
946
947/// Puts a value in an address, where it cannot change what the address means.
948fn encoded(value: &Value) -> String {
949    let text = match value {
950        Value::Null => String::new(),
951        Value::Text(text) => text.clone(),
952        Value::Number(number) if number.fract() == 0.0 => format!("{}", *number as i64),
953        Value::Number(number) => number.to_string(),
954        Value::Bool(on) => on.to_string(),
955        other => format!("{other:?}"),
956    };
957    let mut out = String::with_capacity(text.len());
958    for byte in text.bytes() {
959        if byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.' | b'~') {
960            out.push(char::from(byte));
961        } else {
962            out.push_str(&format!("%{byte:02X}"));
963        }
964    }
965    out
966}
967
968/// The address of a [`ActionKind::Url`], with `{field}` taken from the record.
969pub(crate) fn fill_url(template: &str, object: Option<&Object>) -> String {
970    let mut out = String::with_capacity(template.len());
971    let mut rest = template;
972    while let Some(start) = rest.find('{') {
973        out.push_str(&rest[..start]);
974        let after = &rest[start + 1..];
975        let Some(end) = after.find('}') else {
976            out.push_str(&rest[start..]);
977            return out;
978        };
979        out.push_str(&encoded(&object.map_or(Value::Null, |o| o.get(&after[..end]))));
980        rest = &after[end + 1..];
981    }
982    out.push_str(rest);
983    out
984}
985
986#[cfg(test)]
987mod tests {
988    use std::collections::BTreeMap;
989
990    use super::*;
991
992    fn object(status: &str) -> Object {
993        let values = BTreeMap::from([("Status".to_owned(), Value::Text(status.to_owned()))]);
994        Object::from_values("Bill", values)
995    }
996
997    fn view(id: &str, kind: ViewKind, entity: Option<&str>) -> ViewInfo {
998        ViewInfo::new(id, kind, entity)
999    }
1000
1001    #[test]
1002    fn a_copy_kept_on_the_way_does_not_swallow_a_change() {
1003        let first = Action::simple("a", "a");
1004        let kept = first.clone();
1005        kept.set_enabled("mine", false);
1006        let changed = first.selection(Selection::RequireSingle);
1007        assert_eq!(changed.0.selection, Selection::RequireSingle);
1008        assert_eq!(kept.0.selection, Selection::Independent, "the copy stays as it was");
1009        assert!(!changed.is_enabled(), "it starts with the same reasons");
1010        changed.set_enabled("mine", true);
1011        assert!(!kept.is_enabled(), "and does not share them");
1012    }
1013
1014    #[test]
1015    fn every_reason_has_to_say_yes() {
1016        let mut reasons = Reasons::default();
1017        assert!(reasons.all(), "with none, nothing says no");
1018        reasons.set("a", true);
1019        reasons.set("b", false);
1020        assert!(!reasons.all());
1021        assert_eq!(reasons.against().collect::<Vec<_>>(), ["b"]);
1022        reasons.set("b", true);
1023        assert!(reasons.all());
1024    }
1025
1026    #[test]
1027    fn each_part_of_the_scope_narrows_it() {
1028        let list = view("Bill_ListView", ViewKind::List, Some("Bill"));
1029        let main = FrameContext::Main;
1030        assert!(TargetScope::any().covers(Some(&list), main));
1031        assert!(TargetScope::any().entity("Bill").covers(Some(&list), main));
1032        assert!(!TargetScope::any().entity("Other").covers(Some(&list), main));
1033        assert!(TargetScope::any().view_id("Bill_ListView").covers(Some(&list), main));
1034        assert!(!TargetScope::any().view_id("Other").covers(Some(&list), main));
1035        assert!(TargetScope::any().view_kind(ViewKind::List).covers(Some(&list), main));
1036        assert!(!TargetScope::any().view_kind(ViewKind::Detail).covers(Some(&list), main));
1037        assert!(TargetScope::any().nesting(Nesting::Root).covers(Some(&list), main));
1038        assert!(!TargetScope::any().nesting(Nesting::Nested).covers(Some(&list), main));
1039        assert!(!TargetScope::any().nesting(Nesting::Root).covers(Some(&list), FrameContext::Nested));
1040        assert!(TargetScope::any().nesting(Nesting::Nested).covers(Some(&list), FrameContext::Nested));
1041        assert!(TargetScope::any().entity("Bill").view_kind(ViewKind::List).covers(Some(&list), main));
1042        assert!(!TargetScope::any().entity("Bill").view_kind(ViewKind::Detail).covers(Some(&list), main));
1043    }
1044
1045    #[test]
1046    fn with_no_view_only_what_asks_nothing_of_one_applies() {
1047        assert!(TargetScope::any().covers(None, FrameContext::Main));
1048        assert!(!TargetScope::any().entity("Bill").covers(None, FrameContext::Main));
1049        assert!(!TargetScope::any().view_kind(ViewKind::List).covers(None, FrameContext::Main));
1050    }
1051
1052    #[test]
1053    fn selection_counts_what_is_selected() {
1054        let enabled = |selection: Selection, count: usize| selection.allows(count);
1055        assert!(enabled(Selection::Independent, 0));
1056        assert!(enabled(Selection::Independent, 5));
1057        assert!(!enabled(Selection::RequireSingle, 0));
1058        assert!(enabled(Selection::RequireSingle, 1));
1059        assert!(!enabled(Selection::RequireSingle, 2));
1060        assert!(!enabled(Selection::RequireMultiple, 0));
1061        assert!(enabled(Selection::RequireMultiple, 1));
1062        assert!(enabled(Selection::RequireMultiple, 2));
1063    }
1064
1065    #[test]
1066    fn criteria_count_all_or_at_least_one() {
1067        let env = Environment::new();
1068        let open = "[Status] = 'Open'";
1069        let all = Action::simple("a", "a").target_criteria(open, CriteriaMode::TrueForAll);
1070        let one = Action::simple("b", "b").target_criteria(open, CriteriaMode::TrueAtLeastForOne);
1071        let mixed = [object("Open"), object("Paid")];
1072        assert!(!all.meets_criteria(&mixed, &env));
1073        assert!(one.meets_criteria(&mixed, &env));
1074        let open_only = [object("Open"), object("Open")];
1075        assert!(all.meets_criteria(&open_only, &env));
1076        let paid_only = [object("Paid")];
1077        assert!(!one.meets_criteria(&paid_only, &env));
1078        assert!(all.meets_criteria(&[], &env), "with nothing selected, it stops nothing");
1079        assert!(one.meets_criteria(&[], &env));
1080    }
1081
1082    #[test]
1083    fn a_bad_criteria_is_found_when_it_is_checked() {
1084        let bad = Action::simple("a", "a").target_criteria("[Status] =", CriteriaMode::TrueForAll);
1085        assert!(bad.check().is_err_and(|e| e.0.contains("the action a")));
1086        assert!(Action::simple("b", "b").check().is_ok());
1087    }
1088
1089    #[test]
1090    fn addresses_take_values_that_cannot_change_their_meaning() {
1091        let values = BTreeMap::from([
1092            ("q".to_owned(), Value::Text("a b&c=d/ção".to_owned())),
1093            ("n".to_owned(), Value::Number(12.0)),
1094        ]);
1095        let record = Object::from_values("X", values);
1096        assert_eq!(
1097            fill_url("https://x.dev/s?q={q}&n={n}", Some(&record)),
1098            "https://x.dev/s?q=a%20b%26c%3Dd%2F%C3%A7%C3%A3o&n=12"
1099        );
1100        assert_eq!(fill_url("https://x.dev/{none}", Some(&record)), "https://x.dev/");
1101        assert_eq!(fill_url("https://x.dev/{q}", None), "https://x.dev/");
1102        assert_eq!(fill_url("no close {q", Some(&record)), "no close {q");
1103    }
1104
1105    #[test]
1106    fn options_are_found_at_any_depth() {
1107        let action = Action::single_choice(
1108            "Mode",
1109            "Modo",
1110            vec![ChoiceItem::new("a", "A").item(ChoiceItem::new("a1", "A1"))],
1111        );
1112        assert!(action.check_input(&ActionInput::Choice("a1".into())).is_ok());
1113        assert!(action.check_input(&ActionInput::Choice("z".into())).is_err());
1114        assert!(action.check_input(&ActionInput::None).is_err());
1115    }
1116}