Skip to main content

ravenrs_app_ui/
app.rs

1//! The short way to an application.
2
3use std::cell::RefCell;
4use std::rc::Rc;
5
6use ravenrs_app::{
7    Application, ApplicationBuilder, AttachmentStore, BusinessObject, MemoryDatabase, ModelError, ModelStore,
8    Module, ObjectSpace,
9};
10use ravenrs_app_i18n::{Catalog, Language, Texts};
11use ravenrs_widgets::{Alert, AnyView, Labels, Localized, Tone};
12
13use crate::editors::EditorRegistry;
14use crate::session::{SpaceFactory, Words};
15use crate::{AppShell, SystemModule};
16
17/// Texts given as JSON, by the code of their language, to put over the ones that come with it.
18pub(crate) type TextPacks = Vec<(String, String)>;
19
20/// The texts that come with the framework with these over them, in this language.
21pub(crate) fn texts_of(language: Language, packs: &[(String, String)]) -> Result<Texts, ModelError> {
22    let mut catalog = Catalog::builtin();
23    for (code, json) in packs {
24        catalog.merge_json(code, json).map_err(|error| ModelError(error.0))?;
25    }
26    Ok(catalog.texts(language))
27}
28
29type Seed = Box<dyn FnOnce(&mut dyn ObjectSpace) -> Result<(), String>>;
30type Storage = Box<dyn FnOnce(&Application) -> SpaceFactory>;
31type SessionStep = Box<dyn FnOnce(&crate::Session)>;
32
33/// An application from its records: the module with the actions every application needs,
34/// the screens, and records in memory unless told otherwise.
35///
36/// ```
37/// use ravenrs_app::{BusinessObject, ObjectSpaceExt};
38/// use ravenrs_app_ui::{App, Language};
39///
40/// #[derive(BusinessObject)]
41/// #[object(caption = "Contas", navigation = "Cadastros")]
42/// struct Account {
43///     #[field(key)]
44///     id: i64,
45///     #[field(caption = "Nome")]
46///     name: String,
47/// }
48///
49/// let window = App::new("Raven Contas")
50///     .language(Language::BrazilianPortuguese)
51///     .entity::<Account>()
52///     .seed(|space| {
53///         space.add(&Account { id: 0, name: "Carteira".into() }).map_err(|e| e.to_string())
54///     })
55///     .build()?;
56/// # let _ = window;
57/// # Ok::<(), ravenrs_app::ModelError>(())
58/// ```
59#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
60pub struct App {
61    builder: ApplicationBuilder,
62    language: Language,
63    texts: TextPacks,
64    modules: Vec<Box<dyn FnOnce(ApplicationBuilder) -> ApplicationBuilder>>,
65    editors: EditorRegistry,
66    seed: Option<Seed>,
67    storage: Option<Storage>,
68    kept: Option<Rc<dyn ravenrs_app::Storage>>,
69    list_editors: Vec<(String, crate::list_view::ListEditorFactory)>,
70    attachment_store: Option<Rc<dyn AttachmentStore>>,
71    model_store: Option<Rc<dyn ModelStore>>,
72    steps: Vec<SessionStep>,
73    administrator: Option<String>,
74    security: Option<(Rc<ravenrs_app::SecurityContext>, ravenrs_app::SpaceMaker)>,
75    actions: Vec<ActionMaker>,
76}
77
78type ActionMaker = Rc<dyn Fn() -> ravenrs_app::Action>;
79
80/// The actions an application declared one by one, as a module.
81struct OwnActions(Vec<ActionMaker>);
82
83impl Module for OwnActions {
84    fn name(&self) -> &str {
85        "OwnActions"
86    }
87
88    fn view_controllers(&self) -> Vec<Box<dyn ravenrs_app::ViewController>> {
89        vec![Box::new(OwnActionsController(self.0.iter().map(|make| make()).collect()))]
90    }
91}
92
93struct OwnActionsController(Vec<ravenrs_app::Action>);
94
95impl ravenrs_app::ViewController for OwnActionsController {
96    fn id(&self) -> &str {
97        "OwnActions"
98    }
99
100    fn actions(&self) -> Vec<ravenrs_app::Action> {
101        self.0.clone()
102    }
103}
104
105impl std::fmt::Debug for App {
106    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
107        f.debug_struct("App").field("language", &self.language).finish_non_exhaustive()
108    }
109}
110
111#[cfg_attr(feature = "live", ravenrs_live::expose)]
112impl App {
113    /// An application with this title, and nothing in it yet.
114    pub fn new(title: impl Into<String>) -> Self {
115        Self {
116            builder: Application::builder(title),
117            language: Language::default(),
118            texts: Vec::new(),
119            modules: Vec::new(),
120            editors: EditorRegistry::default(),
121            seed: None,
122            storage: None,
123            kept: None,
124            list_editors: Vec::new(),
125            attachment_store: None,
126            model_store: None,
127            steps: Vec::new(),
128            administrator: None,
129            security: None,
130            actions: Vec::new(),
131        }
132    }
133
134    /// What the administrator of an installation set in the model, as a difference in JSON. Fields
135    /// it adds to an entity reach storage too.
136    pub fn administrator_settings(mut self, difference: impl Into<String>) -> Self {
137        self.administrator = Some(difference.into());
138        self
139    }
140
141    /// The records of an entity no storage keeps, for its list and its forms.
142    pub fn records_of(
143        mut self,
144        entity: impl Into<String>,
145        source: impl Fn() -> Vec<ravenrs_app::Object> + 'static,
146    ) -> Self {
147        let entity = entity.into();
148        self.steps.push(Box::new(move |session| session.use_records(entity, source)));
149        self
150    }
151
152    /// The records of a list no storage keeps, inside a form of this entity.
153    pub fn collection_of(
154        mut self,
155        entity: impl Into<String>,
156        field: impl Into<String>,
157        source: impl Fn(&ravenrs_app::Object) -> Vec<ravenrs_app::Object> + 'static,
158    ) -> Self {
159        let (entity, field) = (entity.into(), field.into());
160        self.steps.push(Box::new(move |session| session.use_collection(entity, field, source)));
161        self
162    }
163
164    /// The language of its screens.
165    pub fn language(mut self, language: Language) -> Self {
166        self.language = language;
167        self
168    }
169
170    /// Adds or replaces words of its screens, and of any component in them, as a flat JSON of keys
171    /// in the language of this code: `{ "ui.save": "Gravar" }`. The keys that come with it are in
172    /// `ravenrs-app-i18n`; keys of its own, for its modules, go here too, and screens read them with
173    /// [`Session::texts`](crate::Session::texts). Given more than once, the last one wins.
174    pub fn texts(mut self, language: impl Into<String>, json: impl Into<String>) -> Self {
175        self.texts.push((language.into(), json.into()));
176        self
177    }
178
179    /// Adds a record declared as a struct.
180    pub fn entity<T: BusinessObject>(mut self) -> Self {
181        self.builder = self.builder.entity_of::<T>();
182        self
183    }
184
185    /// Adds a record declared at run time.
186    pub fn entity_info(mut self, entity: ravenrs_app::EntityInfo) -> Self {
187        self.builder = self.builder.entity(entity);
188        self
189    }
190
191    /// Adds an action of its own, with no module to write. It is made again for each window, so
192    /// each one turns it on and off by itself; its target entity or view says where it shows.
193    ///
194    /// ```
195    /// use ravenrs_app::{Action, BusinessObject, Selection};
196    /// use ravenrs_app_ui::App;
197    ///
198    /// #[derive(BusinessObject)]
199    /// struct Bill {
200    ///     #[field(key)]
201    ///     id: i64,
202    ///     paid: bool,
203    /// }
204    ///
205    /// let app = App::new("Contas").entity::<Bill>().action(|| {
206    ///     Action::background("Pay", "Pagar", |job| {
207    ///         for bill in job.selected() {
208    ///             let mut bill = bill.clone();
209    ///             bill.set("paid", true);
210    ///             job.save(bill);
211    ///         }
212    ///         Ok(())
213    ///     })
214    ///     .target_entity("Bill")
215    ///     .selection(Selection::RequireMultiple)
216    /// });
217    /// # let _ = app;
218    /// ```
219    pub fn action(mut self, make: impl Fn() -> ravenrs_app::Action + 'static) -> Self {
220        self.actions.push(Rc::new(make));
221        self
222    }
223
224    /// Adds a module of its own, over the one with the actions every application needs.
225    pub fn module(mut self, module: impl Module + 'static) -> Self {
226        self.modules.push(Box::new(move |builder| builder.module(module)));
227        self
228    }
229
230    /// Controls of its own for fields.
231    pub fn editors(mut self, editors: EditorRegistry) -> Self {
232        self.editors = editors;
233        self
234    }
235
236    /// The model in another language, as a difference in JSON. The one for its
237    /// [`language`](Self::language), `en` or `pt-BR`, is the one used. Words of the screens, not of
238    /// the model, go to [`texts`](Self::texts).
239    pub fn translation(mut self, language: impl Into<String>, difference: impl Into<String>) -> Self {
240        self.builder = self.builder.translation(language, difference);
241        self
242    }
243
244    /// Gives actions and controllers something the application has.
245    pub fn service<T: 'static>(mut self, service: T) -> Self {
246        self.builder = self.builder.service(service);
247        self
248    }
249
250    /// Adds a view of other views together.
251    pub fn dashboard(mut self, dashboard: ravenrs_app::DashboardInfo) -> Self {
252        self.builder = self.builder.dashboard(dashboard);
253        self
254    }
255
256    /// A way to show a list, for the model to pick by name.
257    pub fn list_editor(
258        mut self,
259        name: impl Into<String>,
260        editor: impl Fn(&crate::ListEditorRequest) -> AnyView + 'static,
261    ) -> Self {
262        self.list_editors.push((name.into(), Rc::new(editor)));
263        self
264    }
265
266    /// Keeps the content of attached files there, apart from the records.
267    pub fn attachment_store(mut self, store: impl AttachmentStore + 'static) -> Self {
268        self.attachment_store = Some(Rc::new(store));
269        self
270    }
271
272    /// Keeps the person's own settings there.
273    pub fn model_store(mut self, store: impl ModelStore + 'static) -> Self {
274        self.model_store = Some(Rc::new(store));
275        self
276    }
277
278    /// What is in its records when it opens, kept with a commit once this is done.
279    pub fn seed(mut self, seed: impl FnOnce(&mut dyn ObjectSpace) -> Result<(), String> + 'static) -> Self {
280        self.seed = Some(Box::new(seed));
281        self
282    }
283
284    /// Keeps the records and the person's own settings in `storage` between runs: a
285    /// [`FileStorage`](ravenrs_app::FileStorage) folder on the desktop, the browser's IndexedDB
286    /// on the web. The records stay in memory while the app runs and are written after every
287    /// commit, under `records`; the settings go under `settings`. The [`seed`](Self::seed) runs
288    /// only the first time, while nothing was kept yet.
289    pub fn keep_in(mut self, storage: impl ravenrs_app::Storage + 'static) -> Self {
290        let storage: Rc<dyn ravenrs_app::Storage> = Rc::new(storage);
291        self.model_store = Some(Rc::new(ravenrs_app::StoredModel::new(storage.clone(), "settings")));
292        self.kept = Some(storage);
293        self
294    }
295
296    /// Keeps records elsewhere than in memory: this makes the unit of work of each view.
297    pub fn storage<S: ObjectSpace + 'static>(mut self, space: impl Fn() -> S + 'static) -> Self {
298        self.storage = Some(Box::new(move |_| {
299            Rc::new(move || Rc::new(RefCell::new(space())) as Rc<RefCell<dyn ObjectSpace>>)
300        }));
301        self
302    }
303
304    /// Keeps records elsewhere than in memory, in something made from the entities as the model
305    /// has them, with the fields its layers added: this makes what makes the unit of work of each view.
306    pub fn storage_for<S, F>(mut self, make: impl FnOnce(&[ravenrs_app::EntityInfo]) -> F + 'static) -> Self
307    where
308        S: ObjectSpace + 'static,
309        F: Fn() -> S + 'static,
310    {
311        self.storage = Some(Box::new(move |application| {
312            let space = make(application.entities());
313            Rc::new(move || Rc::new(RefCell::new(space())) as Rc<RefCell<dyn ObjectSpace>>)
314        }));
315        self
316    }
317
318    /// For this person: every frame goes by what they can do, and what the application itself
319    /// needs in its records is put there through `unsecured`.
320    pub(crate) fn secured(
321        mut self,
322        context: Rc<ravenrs_app::SecurityContext>,
323        unsecured: ravenrs_app::SpaceMaker,
324    ) -> Self {
325        self.seed = None;
326        self.security = Some((context, unsecured));
327        self
328    }
329
330    /// Says who is signed in on the window, and how they leave.
331    pub(crate) fn signed_in(mut self, name: String, sign_out: impl Fn() + 'static) -> Self {
332        let sign_out: Rc<dyn Fn()> = Rc::new(sign_out);
333        self.steps.push(Box::new(move |session| {
334            session.use_account(crate::session::Account { name, sign_out });
335            session.register_control(crate::security::MODEL_EDITOR, crate::security::model_editor(session));
336        }));
337        self
338    }
339
340    /// Runs this with its session, once it is put together, for what needs a session from outside.
341    pub fn with_session(mut self, then: impl FnOnce(&crate::Session) + 'static) -> Self {
342        self.steps.push(Box::new(then));
343        self
344    }
345
346    /// Puts it together, or says what is wrong with how it was declared.
347    pub fn build(self) -> Result<AnyView, ModelError> {
348        let texts = texts_of(self.language, &self.texts)?;
349        let words = Words::from_texts(&texts);
350        let mut builder = self.builder.module(SystemModule::with_words(words.clone()));
351        for add in self.modules {
352            builder = add(builder);
353        }
354        if !self.actions.is_empty() {
355            builder = builder.module(OwnActions(self.actions));
356        }
357        let mut application = builder.build()?;
358        if let Some(text) = &self.administrator {
359            let difference = ravenrs_app::Diff::parse(text)?;
360            application.layers_mut().put(&ravenrs_app::ModelLayer::Administrator, difference);
361        }
362        // Before storage is made, for it to know the fields the layers add.
363        if let Some(store) = &self.model_store {
364            application.layers_mut().load(&ravenrs_app::ModelLayer::User, &**store)?;
365        }
366        application.set_language(self.language.code())?;
367        let unsecured = self.security.as_ref().map(|(_, unsecured)| unsecured.clone());
368        if let Some((context, _)) = &self.security {
369            application.set_security(context.clone());
370        }
371        let mut restored = false;
372        let factory: SpaceFactory = match (self.storage, self.kept) {
373            (Some(storage), _) => storage(&application),
374            (None, Some(kept)) => {
375                let database = MemoryDatabase::new(application.entities().to_vec())?
376                    .stored_in(kept, "records")
377                    .map_err(|e| ModelError(e.to_string()))?;
378                restored = database.was_restored();
379                Rc::new(move || Rc::new(RefCell::new(database.space())) as Rc<RefCell<dyn ObjectSpace>>)
380            }
381            (None, None) => {
382                let database = MemoryDatabase::new(application.entities().to_vec())?;
383                Rc::new(move || Rc::new(RefCell::new(database.space())) as Rc<RefCell<dyn ObjectSpace>>)
384            }
385        };
386        match unsecured {
387            Some(unsecured) => {
388                let mut space = unsecured();
389                application.run_initial_records(&mut *space).map_err(ModelError)?;
390            }
391            None => {
392                let space = factory();
393                let mut space = space.borrow_mut();
394                application.run_initial_records(&mut *space).map_err(ModelError)?;
395            }
396        }
397        if let Some(seed) = self.seed.filter(|_| !restored) {
398            let space = factory();
399            let mut space = space.borrow_mut();
400            seed(&mut *space).map_err(ModelError)?;
401            space.commit().map_err(|e| ModelError(e.to_string()))?;
402        }
403        let shell = AppShell::from_factory(application, factory, words, self.editors);
404        let session = shell.session();
405        session.use_texts(texts.clone());
406        session.set_access_words(ravenrs_app_a11y::AccessWords::for_language(self.language.code()));
407        for (name, editor) in self.list_editors {
408            session.register_list_editor(name, move |request| editor(request));
409        }
410        if let Some(store) = self.attachment_store {
411            session.use_attachment_store(store);
412        }
413        for step in self.steps {
414            step(&session);
415        }
416        if let Some(store) = self.model_store {
417            session.use_model_store(store).map_err(ModelError)?;
418        }
419        Ok(Box::new(Localized::new(Labels::from_texts(&texts), shell)))
420    }
421
422    /// Puts it together, showing on the screen what is wrong with how it was declared.
423    pub fn into_view(self) -> AnyView {
424        self.build().unwrap_or_else(|error| Box::new(Alert::new("RavenRS", error.0).tone(Tone::Danger)))
425    }
426}