1use std::cell::{Cell, RefCell};
4use std::collections::{BTreeMap, BTreeSet};
5use std::rc::Rc;
6
7use ravenrs_app::{
8 Action, ActionContext, ActionInput, ActionResult, Application, AttachmentStore, CONTEXT_DELETE,
9 CONTEXT_SAVE, Criteria, EntityInfo, FieldKind, Frame, FrameContext, Message, MessageKind, ModelLayer,
10 ModelNode, ModelStore, Object, ObjectSpace, Query, RuleWords, Severity, Value, ViewInfo, ViewKind,
11 Violation, merge_changes, validate,
12};
13use ravenrs_app_a11y::{AccessWords, Announcer};
14use ravenrs_app_i18n::{Language, Texts};
15use ravenrs_reactive::State;
16use ravenrs_widgets::{ConfirmDialog, Date, FilePicker, ToastExtras, Toaster, Toasts, Tone};
17
18use crate::editors::EditorRegistry;
19use crate::values::key_text;
20
21#[derive(Clone, Debug)]
23#[non_exhaustive]
24#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
25pub struct Words {
26 pub new: String,
28 pub delete: String,
30 pub save: String,
32 pub save_and_close: String,
34 pub save_and_new: String,
36 pub cancel: String,
38 pub previous: String,
40 pub next: String,
42 pub refresh: String,
44 pub back: String,
46 pub delete_question: String,
48 pub delete_detail: String,
50 pub discard_question: String,
52 pub discard_detail: String,
54 pub discard: String,
56 pub new_record: String,
58 pub variant: String,
60 pub link: String,
62 pub unlink: String,
64 pub save_first: String,
66 pub filter: String,
68 pub clone: String,
70 pub clone_unsaved: String,
72 pub save_file: String,
74 pub warning_question: String,
76 pub warning_answer: String,
78 pub conflict_question: String,
80 pub conflict_detail: String,
82 pub conflict_answer: String,
84 pub list_editor_needs: String,
86 pub no_resource: String,
88 pub chart_count: String,
90 pub reminders: String,
92 pub working: String,
94 pub stopping: String,
96 pub snooze: String,
98 pub dismiss: String,
100 pub open: String,
102 pub yes: String,
104 pub no: String,
106 pub access_denied: String,
108 pub protected_value: String,
110 pub sign_in: String,
112 pub sign_out: String,
114 pub sign_in_failed: String,
116 pub user_name: String,
118 pub password: String,
120 pub current_password: String,
122 pub new_password: String,
124 pub repeat_password: String,
126 pub sign_in_unavailable: String,
128 pub passwords_differ: String,
130 pub password_too_short: String,
132 pub password_wrong_current: String,
134 pub password_saved: String,
136 pub save_password: String,
138 pub change_password: String,
140 pub set_password: String,
142 pub must_change_password: String,
144 pub first_user: String,
146 pub create_user: String,
148 pub model_editor: String,
150 pub search_model: String,
152 pub pick_model_node: String,
154 pub apply: String,
156 pub restore_model: String,
158 pub record_gone: String,
160 pub rules: RuleWords,
162 pub money_prefix: String,
164 pub thousands: char,
166 pub decimal: char,
168}
169
170impl Default for Words {
171 fn default() -> Self {
172 Self::from_texts(&Texts::new(Language::English))
173 }
174}
175
176#[cfg_attr(feature = "live", ravenrs_live::expose)]
177impl Words {
178 pub fn from_texts(texts: &Texts) -> Self {
180 Self {
181 new: texts.get("ui.new"),
182 delete: texts.get("ui.delete"),
183 save: texts.get("ui.save"),
184 save_and_close: texts.get("ui.save_and_close"),
185 save_and_new: texts.get("ui.save_and_new"),
186 cancel: texts.get("ui.cancel"),
187 previous: texts.get("ui.previous"),
188 next: texts.get("ui.next"),
189 refresh: texts.get("ui.refresh"),
190 back: texts.get("ui.back"),
191 delete_question: texts.get("ui.delete_question"),
192 delete_detail: texts.get("ui.delete_detail"),
193 discard_question: texts.get("ui.discard_question"),
194 discard_detail: texts.get("ui.discard_detail"),
195 discard: texts.get("ui.discard"),
196 new_record: texts.get("ui.new_record"),
197 variant: texts.get("ui.variant"),
198 link: texts.get("ui.link"),
199 unlink: texts.get("ui.unlink"),
200 save_first: texts.get("ui.save_first"),
201 filter: texts.get("ui.filter"),
202 clone: texts.get("ui.clone"),
203 clone_unsaved: texts.get("ui.clone_unsaved"),
204 save_file: texts.get("ui.save_file"),
205 warning_question: texts.get("ui.warning_question"),
206 warning_answer: texts.get("ui.warning_answer"),
207 conflict_question: texts.get("ui.conflict_question"),
208 conflict_detail: texts.get("ui.conflict_detail"),
209 conflict_answer: texts.get("ui.conflict_answer"),
210 list_editor_needs: texts.get("ui.list_editor_needs"),
211 no_resource: texts.get("ui.no_resource"),
212 chart_count: texts.get("ui.chart_count"),
213 reminders: texts.get("ui.reminders"),
214 working: texts.get("ui.working"),
215 stopping: texts.get("ui.stopping"),
216 snooze: texts.get("ui.snooze"),
217 dismiss: texts.get("ui.dismiss"),
218 open: texts.get("ui.open"),
219 yes: texts.get("ui.yes"),
220 no: texts.get("ui.no"),
221 access_denied: texts.get("ui.access_denied"),
222 protected_value: texts.get("ui.protected_value"),
223 sign_in: texts.get("ui.sign_in"),
224 sign_out: texts.get("ui.sign_out"),
225 sign_in_failed: texts.get("ui.sign_in_failed"),
226 user_name: texts.get("ui.user_name"),
227 password: texts.get("ui.password"),
228 current_password: texts.get("ui.current_password"),
229 new_password: texts.get("ui.new_password"),
230 repeat_password: texts.get("ui.repeat_password"),
231 sign_in_unavailable: texts.get("ui.sign_in_unavailable"),
232 passwords_differ: texts.get("ui.passwords_differ"),
233 password_too_short: texts.get("ui.password_too_short"),
234 password_wrong_current: texts.get("ui.password_wrong_current"),
235 password_saved: texts.get("ui.password_saved"),
236 save_password: texts.get("ui.save_password"),
237 change_password: texts.get("ui.change_password"),
238 set_password: texts.get("ui.set_password"),
239 must_change_password: texts.get("ui.must_change_password"),
240 first_user: texts.get("ui.first_user"),
241 create_user: texts.get("ui.create_user"),
242 model_editor: texts.get("ui.model_editor"),
243 search_model: texts.get("ui.search_model"),
244 pick_model_node: texts.get("ui.pick_model_node"),
245 apply: texts.get("ui.apply"),
246 restore_model: texts.get("ui.restore_model"),
247 record_gone: texts.get("ui.record_gone"),
248 rules: RuleWords::from_texts(texts),
249 money_prefix: texts.get("format.money_prefix"),
250 thousands: texts.char("format.thousands", ','),
251 decimal: texts.char("format.decimal", '.'),
252 }
253 }
254
255 pub fn pt_br() -> Self {
257 Self::from_texts(&Texts::new(Language::BrazilianPortuguese))
258 }
259}
260
261#[derive(Debug)]
263pub(crate) struct FormState {
264 pub entity: EntityInfo,
265 pub original: Object,
266 pub is_new: bool,
267 pub fields: BTreeMap<String, State<Value>>,
268}
269
270#[cfg_attr(feature = "live", ravenrs_live::expose)]
271impl FormState {
272 fn new(entity: EntityInfo, original: Object, is_new: bool) -> Self {
273 let fields = entity
274 .fields
275 .iter()
276 .filter(|f| f.calculated.is_none())
277 .map(|f| (f.name.clone(), State::new(original.get(&f.name))))
278 .collect();
279 Self { entity, original, is_new, fields }
280 }
281
282 pub fn current(&self) -> Object {
284 let mut object = self.original.clone();
285 for (name, value) in &self.fields {
286 object.set(name, value.peek().clone());
287 }
288 object
289 }
290
291 pub fn watched(&self) -> Object {
293 let mut object = self.original.clone();
294 for (name, value) in &self.fields {
295 object.set(name, value.get());
296 }
297 object
298 }
299
300 pub fn is_dirty(&self) -> bool {
301 self.fields.iter().any(|(name, value)| *value.peek() != self.original.get(name))
302 }
303}
304
305pub(crate) type FileHandler = Rc<dyn Fn(String, Vec<u8>)>;
307
308pub(crate) type UrlHandler = Rc<dyn Fn(String)>;
310
311pub(crate) type SpaceFactory = Rc<dyn Fn() -> Rc<RefCell<dyn ObjectSpace>>>;
313
314pub type FormCaption = Rc<dyn Fn(&str, &Object) -> Option<String>>;
317
318pub type CollectionSource = Rc<dyn Fn(&Object) -> Vec<Object>>;
320
321pub(crate) type ControlFactory = Rc<dyn Fn() -> ravenrs_widgets::AnyView>;
323
324pub(crate) type Presented = (String, ControlFactory);
326
327struct SessionInner {
328 application: Rc<RefCell<Application>>,
329 new_space: SpaceFactory,
330 frame: Frame,
331 words: Words,
332 editors: EditorRegistry,
333 revision: State<u64>,
335 form: State<Option<Rc<FormState>>>,
336 checking: State<bool>,
338 noting: State<bool>,
340 toasts: Toasts,
341 record_sources: Rc<RefCell<BTreeMap<String, crate::list_view::ListSource>>>,
343 collection_sources: Rc<RefCell<BTreeMap<(String, String), CollectionSource>>>,
345 history: State<Vec<String>>,
347 error: State<Option<String>>,
348 confirm: ConfirmDialog,
349 on_file: Rc<RefCell<Option<FileHandler>>>,
350 on_url: Rc<RefCell<Option<UrlHandler>>>,
351 controls: Rc<RefCell<BTreeMap<String, ControlFactory>>>,
352 presented: State<Option<Presented>>,
353 full: State<Option<ControlFactory>>,
355 reminders: State<(usize, bool)>,
357 form_caption: RefCell<Option<FormCaption>>,
358 lists_field_errors: Cell<bool>,
360 list_editors: Rc<RefCell<BTreeMap<String, crate::list_view::ListEditorFactory>>>,
361 attachment_store: Rc<RefCell<Option<Rc<dyn AttachmentStore>>>>,
362 texts: Rc<RefCell<Texts>>,
363 model_store: Rc<RefCell<Option<Rc<dyn ModelStore>>>>,
364 popup_filter: RefCell<Option<Criteria>>,
366 pending: RefCell<Vec<Object>>,
368 left: State<BTreeSet<String>>,
370 today: Cell<Date>,
371 siblings: RefCell<Vec<Value>>,
373 account: RefCell<Option<Account>>,
375 announcer: Announcer,
377 access_words: Rc<RefCell<AccessWords>>,
378}
379
380#[derive(Clone)]
382pub(crate) struct Account {
383 pub(crate) name: String,
384 pub(crate) sign_out: Rc<dyn Fn()>,
385}
386
387#[derive(Clone)]
390#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
391pub struct Session(Rc<SessionInner>);
392
393impl std::fmt::Debug for Session {
394 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
395 f.debug_struct("Session").field("frame", &self.0.frame).finish_non_exhaustive()
396 }
397}
398
399struct SessionLink(std::rc::Weak<SessionInner>);
401
402#[cfg_attr(feature = "live", ravenrs_live::expose(part = "part1"))]
403impl Session {
404 pub(crate) fn new(
405 application: Application,
406 new_space: SpaceFactory,
407 words: Words,
408 editors: EditorRegistry,
409 today: Date,
410 ) -> Self {
411 let frame = application.new_frame(FrameContext::Main, new_space());
412 Self::linked(SessionInner {
413 application: Rc::new(RefCell::new(application)),
414 new_space,
415 frame,
416 words,
417 editors,
418 revision: State::new(0),
419 form: State::new(None),
420 checking: State::new(false),
421 noting: State::new(false),
422 toasts: Toasts::new(),
423 record_sources: Rc::default(),
424 collection_sources: Rc::default(),
425 history: State::new(Vec::new()),
426 error: State::new(None),
427 confirm: ConfirmDialog::new(),
428 on_file: Rc::default(),
429 on_url: Rc::default(),
430 controls: Rc::default(),
431 presented: State::new(None),
432 full: State::new(None),
433 reminders: State::new((0, false)),
434 form_caption: RefCell::default(),
435 lists_field_errors: Cell::new(true),
436 list_editors: Rc::default(),
437 attachment_store: Rc::default(),
438 texts: Rc::default(),
439 model_store: Rc::default(),
440 popup_filter: RefCell::default(),
441 pending: RefCell::default(),
442 left: State::new(BTreeSet::new()),
443 today: Cell::new(today),
444 siblings: RefCell::default(),
445 account: RefCell::default(),
446 announcer: Announcer::new(),
447 access_words: Rc::default(),
448 })
449 }
450
451 fn linked(inner: SessionInner) -> Self {
452 let is_first = inner.list_editors.borrow().is_empty();
453 let inner = Rc::new(inner);
454 let frame = inner.frame.clone();
455 frame.set_extension(Rc::new(SessionLink(Rc::downgrade(&inner))));
456 frame.set_extension(Rc::new(inner.words.rules.clone()));
458 let telling = Rc::downgrade(&inner);
459 frame.set_messenger(move |message| {
460 if let Some(inner) = telling.upgrade() {
461 Session(inner).show_message(message);
462 }
463 });
464 frame.set_validator(|contexts, cx| {
465 let Some(session) = Session::of(cx) else { return Ok(()) };
466 let form = session.form();
467 let records = match (&form, cx.selected.is_empty()) {
468 (Some(form), true) => vec![form.current()],
469 _ => cx.selected.clone(),
470 };
471 let mut messages = Vec::new();
472 for record in &records {
473 for context in contexts {
474 let found = session.violations_of(record, context);
475 messages.extend(found.into_iter().filter(Violation::is_error).map(|v| v.message));
476 }
477 }
478 if messages.is_empty() { Ok(()) } else { Err(messages.join("; ")) }
479 });
480 let session = Self(inner);
481 if is_first {
482 crate::list_editors::register(&session);
483 }
484 session
485 }
486
487 pub(crate) fn child(&self, context: FrameContext) -> Self {
490 let parent = &self.0;
491 Self::linked(SessionInner {
492 application: parent.application.clone(),
493 new_space: parent.new_space.clone(),
494 frame: self.new_frame(context),
495 words: parent.words.clone(),
496 editors: parent.editors.clone(),
497 revision: parent.revision.clone(),
498 form: State::new(None),
499 checking: State::new(false),
500 noting: State::new(false),
501 toasts: parent.toasts.clone(),
502 record_sources: parent.record_sources.clone(),
503 collection_sources: parent.collection_sources.clone(),
504 history: State::new(Vec::new()),
505 error: State::new(None),
506 confirm: ConfirmDialog::new(),
507 on_file: parent.on_file.clone(),
508 on_url: parent.on_url.clone(),
509 controls: parent.controls.clone(),
510 presented: parent.presented.clone(),
511 full: parent.full.clone(),
512 reminders: parent.reminders.clone(),
513 form_caption: RefCell::new(parent.form_caption.borrow().clone()),
514 lists_field_errors: Cell::new(parent.lists_field_errors.get()),
515 list_editors: parent.list_editors.clone(),
516 attachment_store: parent.attachment_store.clone(),
517 texts: parent.texts.clone(),
518 model_store: parent.model_store.clone(),
519 popup_filter: RefCell::default(),
520 pending: RefCell::default(),
521 left: State::new(BTreeSet::new()),
522 today: Cell::new(parent.today.get()),
523 siblings: RefCell::default(),
524 account: RefCell::default(),
525 announcer: parent.announcer.clone(),
526 access_words: parent.access_words.clone(),
527 })
528 }
529
530 pub fn embedded<S: ObjectSpace + 'static>(
533 application: Application,
534 space: impl Fn() -> S + 'static,
535 words: Words,
536 ) -> Self {
537 let factory: SpaceFactory =
538 Rc::new(move || Rc::new(RefCell::new(space())) as Rc<RefCell<dyn ObjectSpace>>);
539 Self::standalone(application, factory, words)
540 }
541
542 pub fn show_list(&self, id: &str) {
544 self.show_view_now(id);
545 }
546
547 pub fn confirmations(&self) -> ConfirmDialog {
549 self.0.confirm.clone()
550 }
551
552 pub fn toaster(&self) -> Toaster {
554 Toaster::new(self.0.toasts.clone())
555 }
556
557 pub fn show_message(&self, message: Message) {
559 let tone = match message.kind {
560 MessageKind::Success => Tone::Success,
561 MessageKind::Warning => Tone::Warning,
562 MessageKind::Error => Tone::Danger,
563 _ => Tone::Accent,
564 };
565 let mut extras = ToastExtras::new();
566 if let Some((label, run)) = message.action {
567 extras = extras.action(label, move || run());
568 }
569 if let Some(run) = message.on_close {
570 extras = extras.on_close(move || run());
571 }
572 self.0.toasts.show_with(self.title(), message.text, tone, message.seconds.unwrap_or(4.0), extras);
573 }
574
575 pub fn use_records(&self, entity: impl Into<String>, source: impl Fn() -> Vec<Object> + 'static) {
577 self.0.record_sources.borrow_mut().insert(entity.into(), Rc::new(source));
578 }
579
580 pub fn use_collection(
582 &self,
583 entity: impl Into<String>,
584 field: impl Into<String>,
585 source: impl Fn(&Object) -> Vec<Object> + 'static,
586 ) {
587 self.0.collection_sources.borrow_mut().insert((entity.into(), field.into()), Rc::new(source));
588 }
589
590 pub(crate) fn record_source(&self, entity: &str) -> Option<crate::list_view::ListSource> {
591 self.0.record_sources.borrow().get(entity).cloned()
592 }
593
594 pub(crate) fn collection_source(&self, entity: &str, field: &str) -> Option<CollectionSource> {
595 self.0.collection_sources.borrow().get(&(entity.to_owned(), field.to_owned())).cloned()
596 }
597
598 pub fn export_captions(&self) -> String {
600 self.0.application.borrow().export_captions()
601 }
602
603 pub fn import_captions(&self, language: &str, csv: &str) -> Result<(), String> {
606 self.0.application.borrow_mut().import_captions(language, csv).map_err(|e| e.0)
607 }
608
609 pub fn register_control(
611 &self,
612 name: impl Into<String>,
613 control: impl Fn() -> ravenrs_widgets::AnyView + 'static,
614 ) {
615 self.0.controls.borrow_mut().insert(name.into(), Rc::new(control));
616 }
617
618 pub(crate) fn control(&self, name: &str) -> Option<ControlFactory> {
619 self.0.controls.borrow().get(name).cloned()
620 }
621
622 pub(crate) fn account(&self) -> Option<Account> {
623 self.0.account.borrow().clone()
624 }
625
626 pub(crate) fn use_account(&self, account: Account) {
627 *self.0.account.borrow_mut() = Some(account);
628 }
629
630 pub(crate) fn can_open(&self, group: Option<&str>, item: &str, view: &str) -> bool {
633 let Some(security) = self.0.frame.security() else { return true };
634 let model = self.model();
635 let entity = model.at(&["Views", view]).and_then(|node| node.text("Entity"));
636 security.with(|permissions| {
637 permissions.can_navigate(group, item)
638 && entity.is_none_or(|entity| permissions.can(ravenrs_app::Operation::Read, entity, None))
639 && (view != crate::security::MODEL_EDITOR_VIEW || permissions.can_edit_model())
640 })
641 }
642
643 pub fn set_model_setting(&self, path: &[&str], name: &str, value: &str) -> Result<(), String> {
645 self.check_model_permission()?;
646 self.0.application.borrow_mut().set_typed_setting(path, name, value).map_err(|e| e.0)?;
647 self.keep_settings()?;
648 self.touch();
649 Ok(())
650 }
651
652 pub fn restore_model_settings(&self) -> Result<(), String> {
654 self.check_model_permission()?;
655 self.0.application.borrow_mut().clear_user_settings().map_err(|e| e.0)?;
656 self.keep_settings()?;
657 self.touch();
658 Ok(())
659 }
660
661 fn check_model_permission(&self) -> Result<(), String> {
662 match self.0.frame.security() {
663 Some(security) if !security.with(ravenrs_app::Permissions::can_edit_model) => {
664 Err(self.0.words.access_denied.clone())
665 }
666 _ => Ok(()),
667 }
668 }
669
670 pub fn show_full(&self, content: impl Fn() -> ravenrs_widgets::AnyView + 'static) {
673 self.0.full.set(Some(Rc::new(content)));
674 }
675
676 pub fn close_full(&self) {
678 self.0.full.set(None);
679 }
680
681 pub(crate) fn entities_with_reminders(&self) -> Vec<EntityInfo> {
682 self.0.application.borrow().entities().iter().filter(|e| e.reminder.is_some()).cloned().collect()
683 }
684
685 pub(crate) fn reminders(&self) -> &State<(usize, bool)> {
686 &self.0.reminders
687 }
688
689 pub(crate) fn full(&self) -> Option<ControlFactory> {
690 self.0.full.get()
691 }
692
693 pub fn close_presented(&self) {
695 self.0.presented.set(None);
696 }
697
698 pub fn present(
700 &self,
701 title: impl Into<String>,
702 content: impl Fn() -> ravenrs_widgets::AnyView + 'static,
703 ) {
704 self.0.presented.set(Some((title.into(), Rc::new(content))));
705 }
706
707 pub(crate) fn presented(&self) -> &State<Option<Presented>> {
708 &self.0.presented
709 }
710
711 pub fn set_form_caption(&self, caption: FormCaption) {
713 *self.0.form_caption.borrow_mut() = Some(caption);
714 }
715
716 pub(crate) fn form_caption(&self) -> Option<FormCaption> {
717 self.0.form_caption.borrow().clone()
718 }
719
720 pub(crate) fn set_lists_field_errors(&self, lists: bool) {
722 self.0.lists_field_errors.set(lists);
723 }
724
725 pub(crate) fn lists_field_errors(&self) -> bool {
726 self.0.lists_field_errors.get()
727 }
728
729 pub fn form_record(&self) -> Option<Object> {
731 self.form().map(|form| form.watched())
732 }
733
734 pub(crate) fn choices(&self, view: &str, part: &str) -> Vec<(String, String)> {
736 let model = self.model();
737 model.at(&["Views", view, part]).map_or_else(Vec::new, |choices| {
738 choices
739 .in_order()
740 .iter()
741 .map(|item| (item.id.clone(), item.text("Caption").unwrap_or(&item.id).to_owned()))
742 .collect()
743 })
744 }
745
746 fn chosen(model: &ModelNode, view: &str, part: &str) -> Option<ModelNode> {
748 let choices = model.at(&["Views", view, part])?;
749 let current = model.at(&["Views", view]).and_then(|node| node.text(&format!("Current{part}")));
750 let order = choices.in_order();
751 current
752 .and_then(|id| order.iter().find(|item| item.id == id))
753 .or(order.first())
754 .map(|&item| item.clone())
755 }
756
757 pub(crate) fn model_filter(&self, view: &str) -> Option<Criteria> {
760 let model = self.model();
761 let own = model.at(&["Views", view]).and_then(|node| node.text("Criteria")).map(str::to_owned);
762 let picked = ["Variants", "Filters"].into_iter().map(|part| {
763 Self::chosen(&model, view, part).and_then(|item| item.text("Filter").map(str::to_owned))
764 });
765 std::iter::once(own)
766 .chain(picked)
767 .flatten()
768 .filter_map(|text| Criteria::parse(&text).ok())
769 .reduce(Criteria::and)
770 }
771
772 pub(crate) fn dashboard_shown(&self, view: &str) -> ViewInfo {
774 let model = self.model();
775 let id = Self::chosen(&model, view, "Variants")
776 .and_then(|item| item.text("View").map(str::to_owned))
777 .unwrap_or_else(|| view.to_owned());
778 ViewInfo::from_model(&model, &id).unwrap_or_else(|_| ViewInfo::new(view, ViewKind::Dashboard, None))
779 }
780
781 pub(crate) fn variant_editor(&self, view: &str) -> Option<String> {
783 let model = self.model();
784 Self::chosen(&model, view, "Variants").and_then(|item| item.text("EditorType").map(str::to_owned))
785 }
786
787 pub(crate) fn variant_columns(&self, view: &str) -> Option<Vec<String>> {
789 let item = Self::chosen(&self.model(), view, "Variants")?;
790 item.get("Columns").map(|columns| columns.in_order().iter().map(|c| c.id.clone()).collect())
791 }
792
793 pub(crate) fn choose(&self, view: &str, part: &str, choice: &str) -> Result<(), String> {
795 self.0
796 .application
797 .borrow_mut()
798 .set_user_setting(&["Views", view], &format!("Current{part}"), choice)
799 .map_err(|e| e.0)?;
800 self.keep_settings()?;
801 self.touch();
802 Ok(())
803 }
804
805 pub fn use_model_store(&self, store: Rc<dyn ModelStore>) -> Result<(), String> {
807 {
808 let mut application = self.0.application.borrow_mut();
809 application.layers_mut().load(&ModelLayer::User, &*store).map_err(|e| e.0)?;
810 application.refresh_model().map_err(|e| e.0)?;
811 }
812 *self.0.model_store.borrow_mut() = Some(store);
813 self.touch();
814 Ok(())
815 }
816
817 fn keep_settings(&self) -> Result<(), String> {
818 let store = self.0.model_store.borrow().clone();
819 let Some(store) = store else { return Ok(()) };
820 let mut application = self.0.application.borrow_mut();
821 application.layers_mut().save(&ModelLayer::User, &*store).map_err(|e| e.0)
822 }
823
824 pub fn set_language(&self, language: &str) -> Result<(), String> {
826 self.0.application.borrow_mut().set_language(language).map_err(|e| e.0)?;
827 self.touch();
828 Ok(())
829 }
830
831 pub fn register_list_editor(
834 &self,
835 name: impl Into<String>,
836 editor: impl Fn(&crate::ListEditorRequest) -> ravenrs_widgets::AnyView + 'static,
837 ) {
838 self.0.list_editors.borrow_mut().insert(name.into(), Rc::new(editor));
839 }
840
841 pub fn choose_list_editor(&self, view: &str, editor: &str) -> Result<(), String> {
843 self.0
844 .application
845 .borrow_mut()
846 .set_user_setting(&["Views", view], "EditorType", editor)
847 .map_err(|e| e.0)?;
848 self.keep_settings()?;
849 self.touch();
850 Ok(())
851 }
852
853 pub(crate) fn list_editor(&self, name: &str) -> Option<crate::list_view::ListEditorFactory> {
854 self.0.list_editors.borrow().get(name).cloned()
855 }
856
857 pub fn use_attachment_store(&self, store: Rc<dyn AttachmentStore>) {
860 *self.0.attachment_store.borrow_mut() = Some(store);
861 }
862
863 pub(crate) fn attachment_store(&self) -> Option<Rc<dyn AttachmentStore>> {
864 self.0.attachment_store.borrow().clone()
865 }
866
867 pub(crate) fn set_popup_filter(&self, filter: Option<Criteria>) {
868 *self.0.popup_filter.borrow_mut() = filter;
869 }
870
871 pub(crate) fn popup_filter(&self) -> Option<Criteria> {
872 self.0.popup_filter.borrow().clone()
873 }
874
875 pub(crate) fn left_field(&self, field: &str) {
877 if !self.0.left.peek().contains(field) {
878 self.0.left.update(|left| {
879 left.insert(field.to_owned());
880 });
881 }
882 }
883
884 pub(crate) fn clone_record(&self, source: &Object) -> Result<(), String> {
887 let entity = self.entity(source.entity()).ok_or_else(|| source.entity().to_owned())?;
888 let view = self.detail_of(&entity)?;
889 if self.0.frame.view().is_some_and(|v| v.kind == ViewKind::List) {
890 self.0.history.update(|history| history.push(view.id.clone()));
891 }
892 self.show(view);
893 let mut pending = Vec::new();
894 let copy = self.copy_of(&entity, source, &mut pending)?;
895 *self.0.pending.borrow_mut() = pending;
896 self.0.form.set(Some(Rc::new(FormState::new(entity.clone(), copy, true))));
897 self.update_position(&entity);
898 Ok(())
899 }
900
901 fn copy_of(
903 &self,
904 entity: &EntityInfo,
905 source: &Object,
906 pending: &mut Vec<Object>,
907 ) -> Result<Object, String> {
908 let space = self.0.frame.space();
909 let mut copy = space.borrow_mut().new_object(&entity.name).map_err(|e| e.to_string())?;
910 let key_name = entity.key_field().map(|key| key.name.clone()).unwrap_or_default();
911 let copied = entity.fields.iter().filter(|field| {
912 !field.is_key
913 && !field.non_cloneable
914 && field.calculated.is_none()
915 && !matches!(field.kind, FieldKind::Collection(_))
916 });
917 for field in copied {
918 copy.set(&field.name, source.get(&field.name));
919 }
920 for field in entity.fields.iter().filter(|field| field.aggregated) {
921 let FieldKind::Collection(target) = &field.kind else { continue };
922 let Some(child) = self.entity(target) else { continue };
923 let named = field.back_reference.as_deref();
924 let Some(back) = crate::nested::back_reference(self, entity, target, named) else { continue };
925 let filter = Criteria::property(&back).equals(Criteria::value(source.get(&key_name)));
926 let found = space.borrow().query(target, &Query::all().filter(filter));
927 for record in found.map_err(|e| e.to_string())? {
928 let mut child_copy = self.copy_of(&child, &record, pending)?;
929 child_copy.set(&back, copy.get(&key_name));
930 pending.push(child_copy);
931 }
932 }
933 Ok(copy)
934 }
935
936 pub fn user_settings(&self) -> Option<String> {
938 let mut application = self.0.application.borrow_mut();
939 application.layers_mut().difference(&ModelLayer::User).map(|diff| diff.to_json())
940 }
941
942 pub(crate) fn open_object(&self, entity: &EntityInfo, object: Object, is_new: bool) {
944 match self.detail_of(entity) {
945 Ok(view) => self.show(view),
946 Err(error) => return self.0.error.set(Some(error)),
947 }
948 self.0.form.set(Some(Rc::new(FormState::new(entity.clone(), object, is_new))));
949 }
950
951 pub fn set_form_value(&self, field: &str, value: Value) {
953 if let Some(state) = self.0.form.peek().as_ref().and_then(|form| form.fields.get(field).cloned()) {
954 state.set(value);
955 }
956 }
957
958 pub(crate) fn check_form(&self) -> Option<(Object, bool)> {
960 let form = self.0.form.peek().clone()?;
961 let object = form.current();
962 if !self.violations_of(&object, CONTEXT_SAVE).iter().any(Violation::is_error) {
963 self.0.checking.set(false);
964 Some((object, form.is_new))
965 } else {
966 self.0.checking.set(true);
967 None
968 }
969 }
970
971 pub fn of(cx: &ActionContext) -> Option<Self> {
973 Self::of_frame(&cx.frame)
974 }
975
976 pub fn of_frame(frame: &Frame) -> Option<Self> {
978 frame.extension::<SessionLink>().and_then(|link| link.0.upgrade()).map(Self)
979 }
980
981 pub fn frame(&self) -> Frame {
983 self.0.frame.clone()
984 }
985
986 pub fn words(&self) -> &Words {
988 &self.0.words
989 }
990
991 pub fn texts(&self) -> Texts {
994 self.0.texts.borrow().clone()
995 }
996
997 pub fn use_texts(&self, texts: Texts) {
999 *self.0.texts.borrow_mut() = texts;
1000 }
1001
1002 pub(crate) fn editors(&self) -> &EditorRegistry {
1003 &self.0.editors
1004 }
1005
1006 pub(crate) fn today(&self) -> Date {
1008 self.0.today.get()
1009 }
1010
1011 pub fn model(&self) -> Rc<ModelNode> {
1013 self.0.application.borrow().model()
1014 }
1015
1016 pub(crate) fn standalone(application: Application, new_space: SpaceFactory, words: Words) -> Self {
1018 Self::with_editors(application, new_space, words, EditorRegistry::default())
1019 }
1020
1021 pub(crate) fn with_editors(
1023 application: Application,
1024 new_space: SpaceFactory,
1025 words: Words,
1026 editors: EditorRegistry,
1027 ) -> Self {
1028 let today = Date::from_days(ravenrs_app::local_now().days);
1029 Self::new(application, new_space, words, editors, today)
1030 }
1031
1032 pub fn records_changed(&self) {
1034 self.touch();
1035 }
1036
1037 pub(crate) fn new_frame(&self, context: FrameContext) -> Frame {
1039 self.0.application.borrow().new_frame(context, (self.0.new_space)())
1040 }
1041
1042 pub fn title(&self) -> String {
1044 self.0.application.borrow().title().to_owned()
1045 }
1046
1047 pub fn entity(&self, name: &str) -> Option<EntityInfo> {
1049 self.0.application.borrow().entities().iter().find(|e| e.name == name).cloned()
1050 }
1051
1052 pub fn announcer(&self) -> &Announcer {
1055 &self.0.announcer
1056 }
1057
1058 pub fn set_access_words(&self, words: AccessWords) {
1060 *self.0.access_words.borrow_mut() = words;
1061 }
1062
1063 pub fn access_words(&self) -> AccessWords {
1065 self.0.access_words.borrow().clone()
1066 }
1067
1068 fn spoken_name(entity: &EntityInfo, object: &Object) -> String {
1070 entity
1071 .default_property()
1072 .map(|field| crate::values::display(field, &object.get(&field.name)))
1073 .filter(|name| !name.trim().is_empty())
1074 .unwrap_or_else(|| entity.caption.clone())
1075 }
1076
1077 pub fn revision(&self) -> u64 {
1079 self.0.revision.get()
1080 }
1081
1082 pub(crate) fn touch(&self) {
1083 self.0.revision.update(|r| *r += 1);
1084 }
1085
1086 pub(crate) fn form(&self) -> Option<Rc<FormState>> {
1087 self.0.form.get()
1088 }
1089
1090 pub fn violations_of(&self, record: &Object, context: &str) -> Vec<Violation> {
1092 let Some(entity) = self.entity(record.entity()) else { return Vec::new() };
1093 let space = self.0.frame.space();
1094 let space = space.borrow();
1095 validate(&entity, record, context, &*space, &self.words().rules)
1096 }
1097
1098 pub(crate) fn form_violations(&self) -> Vec<Violation> {
1100 let Some(form) = self.form() else { return Vec::new() };
1101 let found = |record: &Object| self.violations_of(record, CONTEXT_SAVE);
1102 if self.0.checking.get() || self.0.noting.get() {
1103 return found(&form.watched());
1104 }
1105 if !form.entity.checks_on_leave {
1106 return Vec::new();
1107 }
1108 let left = self.0.left.get();
1109 if left.is_empty() {
1110 return Vec::new();
1111 }
1112 let mut all = found(&form.watched());
1114 all.retain(|violation| violation.field.as_ref().is_some_and(|field| left.contains(field)));
1115 all
1116 }
1117
1118 pub fn error(&self) -> Option<String> {
1120 self.0.error.get()
1121 }
1122
1123 pub(crate) fn set_error(&self, error: Option<String>) {
1124 self.0.error.set(error);
1125 }
1126
1127 pub(crate) fn confirm(&self) -> &ConfirmDialog {
1128 &self.0.confirm
1129 }
1130
1131 pub(crate) fn set_on_file(&self, handler: FileHandler) {
1133 *self.0.on_file.borrow_mut() = Some(handler);
1134 }
1135
1136 pub(crate) fn set_on_url(&self, handler: UrlHandler) {
1138 *self.0.on_url.borrow_mut() = Some(handler);
1139 }
1140
1141 pub fn hand_file(&self, name: String, bytes: Vec<u8>, picker: &FilePicker) {
1143 let handler = self.0.on_file.borrow().clone();
1144 match handler {
1145 Some(handler) => handler(name, bytes),
1146 None => picker.save(&name, &bytes),
1147 }
1148 }
1149
1150 pub(crate) fn is_checking(&self) -> bool {
1152 *self.0.checking.peek()
1153 }
1154
1155 pub fn has_unsaved_changes(&self) -> bool {
1157 self.0.form.peek().as_ref().is_some_and(|form| form.is_dirty())
1158 }
1159
1160 pub(crate) fn leaving(&self, then: impl Fn() + 'static) {
1162 if self.has_unsaved_changes() {
1163 let words = self.words();
1164 self.0.confirm.ask(
1165 words.discard_question.clone(),
1166 words.discard_detail.clone(),
1167 words.discard.clone(),
1168 then,
1169 );
1170 } else {
1171 then();
1172 }
1173 }
1174
1175 fn show(&self, view: ViewInfo) {
1176 self.0.full.set(None);
1177 self.0.error.set(None);
1178 self.0.checking.set(false);
1179 self.0.noting.set(false);
1180 self.0.pending.borrow_mut().clear();
1181 if !self.0.left.peek().is_empty() {
1182 self.0.left.set(BTreeSet::new());
1183 }
1184 self.0.frame.set_view_in(Some(view), (self.0.new_space)());
1185 }
1186
1187 pub fn open_view(&self, id: &str) {
1189 let (session, id) = (self.clone(), id.to_owned());
1190 self.leaving(move || session.show_view_now(&id));
1191 }
1192
1193 pub(crate) fn open_navigation(&self, id: &str, key: Option<&str>) {
1196 let (session, id, key) = (self.clone(), id.to_owned(), key.map(str::to_owned));
1197 self.leaving(move || {
1198 let model = session.model();
1199 let Ok(view) = ViewInfo::from_model(&model, &id) else {
1200 return session.show_view_now(&id);
1201 };
1202 let Some(entity) = view.entity.as_deref().filter(|_| view.kind == ViewKind::Detail) else {
1203 return session.show_view_now(&id);
1204 };
1205 session.0.history.set(vec![id.clone()]);
1206 match (&key, session.entity(entity)) {
1207 (Some(key), Some(info)) => {
1208 session.open_detail(entity, &crate::values::key_from_text(&info, key));
1209 }
1210 _ => session.open_new(entity),
1211 }
1212 session.touch();
1213 });
1214 }
1215
1216 pub(crate) fn show_view_now(&self, id: &str) {
1217 let model = self.model();
1218 match ViewInfo::from_model(&model, id) {
1219 Ok(view) if view.kind == ViewKind::Detail => {
1220 self.0.history.set(vec![id.to_owned()]);
1221 match view.entity.clone() {
1222 Some(entity) => self.open_new(&entity),
1223 None => self.show(view),
1224 }
1225 self.touch();
1226 }
1227 Ok(view) => {
1228 self.0.form.set(None);
1229 self.0.history.set(vec![id.to_owned()]);
1230 if let Some(title) = model.at(&["Views", id]).and_then(|node| node.text("Caption")) {
1231 self.0.announcer.polite(self.access_words().showing(title));
1232 }
1233 self.show(view);
1234 self.touch();
1235 }
1236 Err(error) => self.0.error.set(Some(error.0)),
1237 }
1238 }
1239
1240 fn detail_of(&self, entity: &EntityInfo) -> Result<ViewInfo, String> {
1241 let id = format!("{}_DetailView", entity.name);
1242 ViewInfo::from_model(&self.model(), &id).map_err(|e| e.0)
1243 }
1244
1245 fn remember_siblings(&self, entity: &EntityInfo) {
1246 let Some(field) = entity.key_field() else { return };
1247 let space = self.0.frame.space();
1248 let found = space.borrow().query(&entity.name, &Query::all());
1249 *self.0.siblings.borrow_mut() =
1250 found.map_or_else(|_| Vec::new(), |all| all.iter().map(|o| o.get(&field.name)).collect());
1251 }
1252
1253 fn find_record(
1255 &self,
1256 entity: &EntityInfo,
1257 key: &Value,
1258 ) -> Result<Option<Object>, ravenrs_app::SpaceError> {
1259 if let Some(source) = self.record_source(&entity.name) {
1260 let name = entity.key_field().map(|field| field.name.clone()).unwrap_or_default();
1261 return Ok(source().into_iter().find(|object| object.get(&name) == *key));
1262 }
1263 self.0.frame.space().borrow().find(&entity.name, key)
1264 }
1265
1266 pub fn open_detail(&self, entity_name: &str, key: &Value) {
1268 let Some(entity) = self.entity(entity_name) else { return };
1269 let view = match self.detail_of(&entity) {
1270 Ok(view) => view,
1271 Err(error) => return self.0.error.set(Some(error)),
1272 };
1273 if self.0.frame.view().is_some_and(|v| v.kind == ViewKind::List) {
1274 self.remember_siblings(&entity);
1275 self.0.history.update(|history| history.push(view.id.clone()));
1276 }
1277 self.show(view);
1278 let found = self.find_record(&entity, key);
1279 match found {
1280 Ok(Some(object)) => {
1281 self.0.frame.set_selected(vec![object.clone()]);
1282 self.0.form.set(Some(Rc::new(FormState::new(entity.clone(), object, false))));
1283 }
1284 Ok(None) => self.0.error.set(Some(format!("{} {}", entity.caption, key_text(&entity, key)))),
1285 Err(error) => self.0.error.set(Some(error.to_string())),
1286 }
1287 self.update_position(&entity);
1288 }
1289
1290 pub fn open_new(&self, entity_name: &str) {
1292 let Some(entity) = self.entity(entity_name) else { return };
1293 let view = match self.detail_of(&entity) {
1294 Ok(view) => view,
1295 Err(error) => return self.0.error.set(Some(error)),
1296 };
1297 if self.0.frame.view().is_some_and(|v| v.kind == ViewKind::List) {
1298 self.0.history.update(|history| history.push(view.id.clone()));
1299 }
1300 self.show(view);
1301 let made = if entity.transient {
1303 Ok(entity.new_object())
1304 } else {
1305 self.0.frame.space().borrow_mut().new_object(entity_name)
1306 };
1307 match made {
1308 Ok(object) => self.0.form.set(Some(Rc::new(FormState::new(entity.clone(), object, true)))),
1309 Err(error) => self.0.error.set(Some(error.to_string())),
1310 }
1311 self.update_position(&entity);
1312 }
1313
1314 fn update_position(&self, entity: &EntityInfo) {
1316 let at = self.position(entity);
1317 let last = self.0.siblings.borrow().len().saturating_sub(1);
1318 if let Some(action) = self.0.frame.action(crate::ids::PREVIOUS) {
1319 action.set_enabled("position", at.is_some_and(|at| at > 0));
1320 }
1321 if let Some(action) = self.0.frame.action(crate::ids::NEXT) {
1322 action.set_enabled("position", at.is_some_and(|at| at < last));
1323 }
1324 }
1325
1326 fn position(&self, entity: &EntityInfo) -> Option<usize> {
1327 let form = self.0.form.peek().clone()?;
1328 if form.is_new {
1329 return None;
1330 }
1331 let key = entity.key_field().map(|f| form.original.get(&f.name))?;
1332 self.0.siblings.borrow().iter().position(|k| *k == key)
1333 }
1334
1335 pub(crate) fn step(&self, by: isize) {
1337 let Some(form) = self.0.form.peek().clone() else { return };
1338 let Some(at) = self.position(&form.entity) else { return };
1339 let Some(to) = at.checked_add_signed(by) else { return };
1340 let Some(key) = self.0.siblings.borrow().get(to).cloned() else { return };
1341 let (session, name) = (self.clone(), form.entity.name.clone());
1342 self.leaving(move || session.open_detail(&name, &key));
1343 }
1344
1345 pub(crate) fn save(&self) -> Result<(), String> {
1347 self.save_then(|_| {})
1348 }
1349
1350 pub(crate) fn save_then(&self, then: impl Fn(&Self) + 'static) -> Result<(), String> {
1353 let Some(form) = self.0.form.peek().clone() else { return Ok(()) };
1354 let object = form.current();
1355 let found = self.violations_of(&object, CONTEXT_SAVE);
1356 let errors: Vec<String> = found.iter().filter(|v| v.is_error()).map(|v| v.message.clone()).collect();
1357 if !errors.is_empty() {
1358 self.0.error.set(None);
1359 self.0.checking.set(true);
1360 let why = errors.join("; ");
1361 self.0.announcer.assertive(self.access_words().not_saved(&why));
1362 return Err(why);
1363 }
1364 self.0.checking.set(false);
1365 let then: Rc<dyn Fn(&Self)> = Rc::new(then);
1366 let warnings: Vec<String> =
1367 found.iter().filter(|v| v.severity == Severity::Warning).map(|v| v.message.clone()).collect();
1368 if warnings.is_empty() {
1369 return self.save_merged(&then);
1370 }
1371 let session = self.clone();
1372 let words = self.words();
1373 self.0.confirm.ask(
1374 words.warning_question.clone(),
1375 warnings.join("\n"),
1376 words.warning_answer.clone(),
1377 move || {
1378 let _ = session.save_merged(&then);
1380 },
1381 );
1382 Ok(())
1383 }
1384
1385 fn save_merged(&self, then: &Rc<dyn Fn(&Self)>) -> Result<(), String> {
1388 let Some(form) = self.0.form.peek().clone() else { return Ok(()) };
1389 let object = form.current();
1390 if form.is_new || form.entity.transient {
1391 return self.keep(&form, object, then);
1392 }
1393 let key = form.entity.key_field().map_or(Value::Null, |field| object.get(&field.name));
1394 let stored = self.0.frame.space().borrow().find(&form.entity.name, &key);
1395 let stored = match stored {
1396 Ok(stored) => stored,
1397 Err(error) => {
1398 let message = error.to_string();
1399 self.0.error.set(Some(message.clone()));
1400 return Err(message);
1401 }
1402 };
1403 let Some(stored) = stored else {
1404 let message = self.words().record_gone.clone();
1405 self.0.error.set(Some(message.clone()));
1406 return Err(message);
1407 };
1408 let merged = merge_changes(&form.entity, &form.original, &object, &stored);
1409 if merged.conflicts.is_empty() {
1410 return self.keep(&form, merged.record, then);
1411 }
1412 let names: Vec<&str> = merged
1413 .conflicts
1414 .iter()
1415 .filter_map(|name| form.entity.field_named(name))
1416 .map(|field| field.caption.as_str())
1417 .collect();
1418 let (session, then, record) = (self.clone(), then.clone(), merged.record);
1419 let (words, kept_form) = (self.words(), form.clone());
1420 self.0.confirm.ask(
1421 words.conflict_question.clone(),
1422 words.conflict_detail.replace("{0}", &names.join(", ")),
1423 words.conflict_answer.clone(),
1424 move || {
1425 let _ = session.keep(&kept_form, record.clone(), &then);
1427 },
1428 );
1429 Ok(())
1430 }
1431
1432 fn keep(&self, form: &Rc<FormState>, object: Object, then: &Rc<dyn Fn(&Self)>) -> Result<(), String> {
1434 let has_notes = self.violations_of(&object, CONTEXT_SAVE).iter().any(|v| !v.is_error());
1435 self.0.noting.set(has_notes);
1436 if form.entity.transient {
1437 self.0.error.set(None);
1438 self.0.form.set(Some(Rc::new(FormState::new(form.entity.clone(), object, false))));
1439 self.touch();
1440 then(self);
1441 return Ok(());
1442 }
1443 let space = self.0.frame.space();
1444 let done = {
1445 let mut space = space.borrow_mut();
1446 let mut kept =
1447 if form.is_new { space.insert(object.clone()) } else { space.save(object.clone()) };
1448 for record in self.0.pending.borrow_mut().drain(..) {
1450 kept = kept.and_then(|()| space.insert(record));
1451 }
1452 kept.and_then(|()| space.commit())
1453 };
1454 match done {
1455 Ok(()) => {
1456 self.0.error.set(None);
1457 self.0.announcer.polite(self.access_words().saved(&Self::spoken_name(&form.entity, &object)));
1458 let saved = form.entity.key_field().map(|f| object.get(&f.name));
1459 self.touch();
1460 if let Some(key) = saved {
1461 self.remember_siblings(&form.entity);
1463 let found = space.borrow().find(&form.entity.name, &key).ok().flatten();
1464 let fresh = found.unwrap_or(object);
1465 self.0.frame.set_selected(vec![fresh.clone()]);
1466 self.0.form.set(Some(Rc::new(FormState::new(form.entity.clone(), fresh, false))));
1467 self.update_position(&form.entity);
1468 }
1469 then(self);
1470 Ok(())
1471 }
1472 Err(error) => {
1473 space.borrow_mut().rollback();
1475 let message = error.to_string();
1476 self.0.error.set(Some(message.clone()));
1477 Err(message)
1478 }
1479 }
1480 }
1481
1482 pub(crate) fn back_now(&self) {
1484 self.0.form.set(None);
1485 let previous = {
1486 let mut history = self.0.history.get();
1487 history.pop();
1488 let previous = history.last().cloned();
1489 self.0.history.set(history);
1490 previous
1491 };
1492 let target = previous.or_else(|| self.first_view());
1493 if let Some(id) = target {
1494 self.show_view_now(&id);
1495 }
1496 }
1497
1498 pub(crate) fn back(&self) {
1500 let session = self.clone();
1501 self.leaving(move || session.back_now());
1502 }
1503
1504 pub(crate) fn cancel(&self) {
1506 self.0.frame.space().borrow_mut().rollback();
1507 self.back_now();
1508 }
1509
1510 pub(crate) fn delete(&self, objects: &[Object]) -> Result<(), String> {
1512 let found: Vec<Violation> =
1513 objects.iter().flat_map(|object| self.violations_of(object, CONTEXT_DELETE)).collect();
1514 let refused: Vec<String> = found.iter().filter(|v| v.is_error()).map(|v| v.message.clone()).collect();
1515 if !refused.is_empty() {
1516 return Err(refused.join("; "));
1517 }
1518 let warnings: Vec<String> =
1519 found.iter().filter(|v| v.severity == Severity::Warning).map(|v| v.message.clone()).collect();
1520 if warnings.is_empty() {
1521 return self.delete_now(objects);
1522 }
1523 let (session, objects, words) = (self.clone(), objects.to_vec(), self.words());
1524 self.0.confirm.ask(
1525 words.delete_question.clone(),
1526 warnings.join("\n"),
1527 words.delete.clone(),
1528 move || {
1529 let _ = session.delete_now(&objects);
1531 },
1532 );
1533 Ok(())
1534 }
1535
1536 fn delete_now(&self, objects: &[Object]) -> Result<(), String> {
1537 let space = self.0.frame.space();
1538 let done = {
1539 let mut space = space.borrow_mut();
1540 let mut result = Ok(());
1541 for object in objects {
1542 let Some(entity) = self.entity(object.entity()) else { continue };
1543 let Some(field) = entity.key_field() else { continue };
1544 result = result.and_then(|()| space.delete(&entity.name, &object.get(&field.name)));
1545 }
1546 result.and_then(|()| space.commit())
1547 };
1548 match done {
1549 Ok(()) => {
1550 self.0.announcer.polite(self.access_words().deleted(objects.len()));
1551 self.touch();
1552 let leaving_form = self.0.form.peek().is_some();
1553 if leaving_form {
1554 self.back_now();
1555 } else {
1556 self.0.frame.set_selected(Vec::new());
1557 }
1558 Ok(())
1559 }
1560 Err(error) => {
1561 space.borrow_mut().rollback();
1562 let message = error.to_string();
1563 self.0.error.set(Some(message.clone()));
1564 Err(message)
1565 }
1566 }
1567 }
1568
1569 pub(crate) fn refresh(&self) {
1571 let session = self.clone();
1572 self.leaving(move || {
1573 session.0.frame.space().borrow_mut().refresh();
1574 session.touch();
1575 let form = session.0.form.peek().clone();
1576 if let Some(form) = form {
1577 if form.is_new {
1578 session.back_now();
1579 } else if let Some(field) = form.entity.key_field() {
1580 let key = form.original.get(&field.name);
1581 session.open_detail(&form.entity.name, &key);
1582 }
1583 }
1584 });
1585 }
1586
1587 pub(crate) fn first_view(&self) -> Option<String> {
1589 let model = self.model();
1590 model.get("NavigationItems")?.in_order().into_iter().find_map(|group| {
1591 if let Some(view) = group.text("View") {
1592 return self.can_open(None, &group.id, view).then(|| view.to_owned());
1593 }
1594 group.in_order().into_iter().find_map(|item| {
1595 let view = item.text("View")?;
1596 self.can_open(Some(&group.id), &item.id, view).then(|| view.to_owned())
1597 })
1598 })
1599 }
1600
1601 pub fn run_action(&self, action: &Action, input: ActionInput) {
1603 let Some(question) = action.confirmation_text().map(str::to_owned) else {
1604 self.execute_after_warnings(action, input);
1605 return;
1606 };
1607 let (session, kept) = (self.clone(), action.clone());
1608 let detail = action.confirmation_detail().unwrap_or(&self.words().delete_detail).to_owned();
1609 let answer = action.confirmation_answer().unwrap_or(action.caption()).to_owned();
1610 self.0.confirm.ask(question, detail, answer, move || {
1611 session.execute_after_warnings(&kept, input.clone());
1612 });
1613 }
1614
1615 fn execute_after_warnings(&self, action: &Action, input: ActionInput) {
1617 let selected = self.0.frame.selected();
1618 let records = match (self.form(), selected.is_empty()) {
1619 (Some(form), true) => vec![form.current()],
1620 _ => selected,
1621 };
1622 let warnings: Vec<String> = records
1623 .iter()
1624 .flat_map(|record| {
1625 action.contexts().iter().flat_map(move |context| self.violations_of(record, context))
1626 })
1627 .filter(|violation| violation.severity == Severity::Warning)
1628 .map(|violation| violation.message)
1629 .collect();
1630 if warnings.is_empty() {
1631 return self.execute(action.id(), input);
1632 }
1633 let (session, id, words) = (self.clone(), action.id().to_owned(), self.words());
1634 self.0.confirm.ask(
1635 words.warning_question.clone(),
1636 warnings.join("\n"),
1637 words.warning_answer.clone(),
1638 move || session.execute(&id, input.clone()),
1639 );
1640 }
1641
1642 pub(crate) fn after_background_work(&self) {
1647 self.reread_after_action();
1648 }
1649
1650 fn reread_after_action(&self) {
1651 let frame = &self.0.frame;
1652 let fresh: Vec<Object> = frame
1653 .selected()
1654 .into_iter()
1655 .filter_map(|record| {
1656 let entity = self.entity(record.entity())?;
1657 let key = record.get(&entity.key_field()?.name);
1658 frame.space().borrow().find(&entity.name, &key).ok().flatten()
1659 })
1660 .collect();
1661 frame.set_selected(fresh);
1662 self.touch();
1663 let form = self.0.form.peek().clone();
1664 if let Some(form) = form.filter(|form| !form.is_new && !form.is_dirty())
1665 && let Some(field) = form.entity.key_field()
1666 {
1667 self.open_detail(&form.entity.name, &form.original.get(&field.name));
1668 }
1669 }
1670
1671 fn execute(&self, id: &str, input: ActionInput) {
1672 match self.0.frame.execute(id, input) {
1673 Ok(ActionResult::OpenUrl(address)) => {
1674 let handler = self.0.on_url.borrow().clone();
1675 if let Some(handler) = handler {
1676 handler(address);
1677 }
1678 }
1679 Ok(ActionResult::RecordsChanged) => self.reread_after_action(),
1680 Ok(_) => {}
1681 Err(_) if *self.0.checking.peek() => {}
1683 Err(error) => self.0.error.set(Some(error.to_string())),
1684 }
1685 }
1686}