Skip to main content

ravenrs_app_ui/
list_editors.rs

1//! Other ways to show a list than a grid: a chart, a month, a timeline of resources and a tree.
2//!
3//! A view picks one by `EditorType` in the model, or a way to see the list by its own. What each
4//! one needs comes from a node under the view, `ChartSettings`, `CalendarSettings`,
5//! `SchedulerSettings` or `TreeSettings`, as text a person can change like any other setting, and
6//! what is not said follows from the kinds of the fields.
7
8use std::collections::{BTreeMap, BTreeSet};
9use std::rc::Rc;
10
11use ravenrs_app::{
12    CONTEXT_SAVE, EntityInfo, FieldInfo, FieldKind, Object, Operation, Query, Value, Violation,
13};
14use ravenrs_criteria::DateTime;
15use ravenrs_reactive::{Binding, State};
16use ravenrs_widgets::{
17    Alert, AnyView, BuildCx, CalendarEvent, Chart, ChartKind, EventCalendar, NodeId, Resource, Scheduler,
18    SchedulerChange, SchedulerEvent, TimeAxis, TimePoint, Tone, TreeNode, TreeView, View,
19};
20
21use crate::ListEditorRequest;
22use crate::Session;
23use crate::values::{date_of, display};
24
25/// The name of the editor that shows a list as a chart.
26pub const CHART_EDITOR: &str = "Chart";
27/// The name of the editor that shows a list on the days of a month.
28pub const CALENDAR_EDITOR: &str = "Calendar";
29/// The name of the editor that shows a list on a timeline, a row per resource.
30pub const SCHEDULER_EDITOR: &str = "Scheduler";
31/// The name of the editor that shows a list as a tree.
32pub const TREE_EDITOR: &str = "Tree";
33
34/// How long a record with no end shows, in minutes.
35const NO_END_MINUTES: i64 = 60;
36const CHART_HEIGHT: f32 = 420.0;
37const TREE_HEIGHT: f32 = 520.0;
38const TONES: [Tone; 5] = [Tone::Accent, Tone::Success, Tone::Warning, Tone::Danger, Tone::Neutral];
39
40/// Puts them where a view can pick them, by name.
41pub(crate) fn register(session: &Session) {
42    session.register_list_editor(CHART_EDITOR, |request| Box::new(Shown::new(request, chart)) as AnyView);
43    session
44        .register_list_editor(CALENDAR_EDITOR, |request| Box::new(Shown::new(request, calendar)) as AnyView);
45    session.register_list_editor(SCHEDULER_EDITOR, |request| {
46        Box::new(Shown::new(request, scheduler)) as AnyView
47    });
48    session.register_list_editor(TREE_EDITOR, |request| Box::new(Shown::new(request, tree)) as AnyView);
49}
50
51type Build = fn(&Data, &mut BuildCx<'_>) -> Result<NodeId, String>;
52
53/// What every one of them works from.
54struct Data {
55    session: Session,
56    view: String,
57    entity: EntityInfo,
58    columns: Vec<(FieldInfo, String)>,
59    objects: Rc<Vec<Object>>,
60    on_open: crate::OpenHandler,
61    on_select: Rc<dyn Fn(Vec<Object>)>,
62    /// Settings given directly, in place of those in the model.
63    given: BTreeMap<String, String>,
64}
65
66/// A view that says what is missing instead of showing something wrong.
67struct Shown {
68    data: Data,
69    build: Build,
70}
71
72#[cfg_attr(feature = "live", ravenrs_live::expose)]
73impl Shown {
74    fn new(request: &ListEditorRequest, build: Build) -> Self {
75        Self {
76            data: Data {
77                session: request.session.clone(),
78                view: request.view.id.clone(),
79                entity: request.entity.clone(),
80                columns: request.columns.clone(),
81                objects: request.objects.clone(),
82                on_open: request.on_open.clone(),
83                on_select: request.on_select.clone(),
84                given: BTreeMap::new(),
85            },
86            build,
87        }
88    }
89}
90
91impl View for Shown {
92    fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
93        match (self.build)(&self.data, cx) {
94            Ok(node) => node,
95            Err(why) => {
96                let title = self.data.entity.caption.clone();
97                cx.child(&Alert::new(title, why).tone(Tone::Warning))
98            }
99        }
100    }
101}
102
103#[cfg_attr(feature = "live", ravenrs_live::expose)]
104impl Data {
105    fn setting(&self, node: &str, name: &str) -> Option<String> {
106        if let Some(given) = self.given.get(name) {
107            return Some(given.clone());
108        }
109        let model = self.session.model();
110        model.at(&["Views", &self.view, node]).and_then(|settings| settings.text(name)).map(str::to_owned)
111    }
112
113    /// The field a setting names, or, if it says nothing, the first that fits.
114    fn field(
115        &self,
116        node: &str,
117        name: &str,
118        fits: impl Fn(&FieldInfo) -> bool,
119    ) -> Result<Option<&FieldInfo>, String> {
120        match self.setting(node, name) {
121            Some(named) => match self.entity.field_named(&named) {
122                Some(field) => Ok(Some(field)),
123                None => Err(self.words(&format!("{node}/{name}: {named}"))),
124            },
125            None => Ok(self.columns.iter().map(|(field, _)| field).find(|field| fits(field))),
126        }
127    }
128
129    fn words(&self, what: &str) -> String {
130        format!("{} {what}", self.session.words().list_editor_needs)
131    }
132
133    fn key_of(&self, object: &Object) -> Value {
134        self.entity.key_field().map_or(Value::Null, |key| object.get(&key.name))
135    }
136
137    /// What names the record, for whoever shows one.
138    fn title_of(&self, node: &str, object: &Object) -> String {
139        let field = self
140            .setting(node, "Title")
141            .and_then(|name| self.entity.field_named(&name))
142            .or_else(|| self.entity.default_property());
143        field.map_or_else(
144            || ravenrs_app::key_text(&self.key_of(object)).unwrap_or_default(),
145            |field| display(field, &object.get(&field.name)),
146        )
147    }
148
149    /// What names each record of the entity a reference points to.
150    fn names_of(&self, target: &str) -> BTreeMap<String, String> {
151        let Some(entity) = self.session.entity(target) else { return BTreeMap::new() };
152        let (Some(key), Some(name)) = (entity.key_field(), entity.default_property()) else {
153            return BTreeMap::new();
154        };
155        let found = self.session.frame().space().borrow().query(target, &Query::all());
156        found
157            .unwrap_or_default()
158            .iter()
159            .map(|object| (display(key, &object.get(&key.name)), display(name, &object.get(&name.name))))
160            .collect()
161    }
162
163    fn tone_of(&self, node: &str, object: &Object) -> Tone {
164        let field = self.setting(node, "Tone").and_then(|name| self.entity.field_named(&name).cloned());
165        let Some(field) = field else { return Tone::Accent };
166        let FieldKind::Enum(options) = &field.kind else { return Tone::Accent };
167        let value = object.get(&field.name);
168        options
169            .iter()
170            .position(|option| value == Value::Text(option.code.clone()))
171            .map_or(Tone::Neutral, |at| TONES[at % TONES.len()])
172    }
173
174    fn is_done(&self, node: &str, object: &Object) -> bool {
175        self.setting(node, "Done").is_some_and(|name| object.get(&name) == Value::Bool(true))
176    }
177
178    fn can(&self, operation: Operation, record: Option<&Object>) -> bool {
179        self.session.frame().security().is_none_or(|security| {
180            security.with(|permissions| permissions.can(operation, &self.entity.name, record))
181        })
182    }
183
184    /// Writes a record changed on the screen: its rules first, then in a commit of its own.
185    fn store(&self, object: Object) {
186        let session = &self.session;
187        let errors: Vec<String> = session
188            .violations_of(&object, CONTEXT_SAVE)
189            .into_iter()
190            .filter(Violation::is_error)
191            .map(|violation| violation.message)
192            .collect();
193        if !errors.is_empty() {
194            session.set_error(Some(errors.join("; ")));
195            session.records_changed();
196            return;
197        }
198        let space = session.frame().space();
199        let mut space = space.borrow_mut();
200        let result = space.save(object).and_then(|()| space.commit());
201        if let Err(error) = result {
202            space.rollback();
203            session.set_error(Some(error.to_string()));
204        } else {
205            session.set_error(None);
206        }
207        drop(space);
208        session.records_changed();
209    }
210}
211
212fn is_date(field: &FieldInfo) -> bool {
213    matches!(field.kind, FieldKind::Date | FieldKind::DateTime)
214}
215
216fn minutes_of(value: &Value) -> Option<TimePoint> {
217    match value {
218        Value::Date(when) => Some(TimePoint::from_minutes(when.days * 1440 + i64::from(when.seconds) / 60)),
219        _ => None,
220    }
221}
222
223fn value_of(time: TimePoint) -> Value {
224    let minutes = time.minutes();
225    Value::Date(DateTime { days: minutes.div_euclid(1440), seconds: (minutes.rem_euclid(1440) * 60) as u32 })
226}
227
228/// How the values of one bar or point come together.
229#[derive(Clone, Copy, PartialEq)]
230enum Summary {
231    Sum,
232    Count,
233    Average,
234    Min,
235    Max,
236}
237
238#[cfg_attr(feature = "live", ravenrs_live::expose)]
239impl Summary {
240    fn named(name: &str) -> Self {
241        match name {
242            "Count" => Self::Count,
243            "Average" => Self::Average,
244            "Min" => Self::Min,
245            "Max" => Self::Max,
246            _ => Self::Sum,
247        }
248    }
249
250    fn of(self, values: &[f64]) -> f64 {
251        match self {
252            Self::Sum => values.iter().sum(),
253            Self::Count => values.len() as f64,
254            Self::Average if values.is_empty() => 0.0,
255            Self::Average => values.iter().sum::<f64>() / values.len() as f64,
256            Self::Min => values.iter().copied().reduce(f64::min).unwrap_or(0.0),
257            Self::Max => values.iter().copied().reduce(f64::max).unwrap_or(0.0),
258        }
259    }
260}
261
262fn is_number(field: &FieldInfo) -> bool {
263    matches!(field.kind, FieldKind::Integer | FieldKind::Decimal(_) | FieldKind::Money)
264}
265
266/// A date as what it is grouped by. The text sorts as time goes.
267fn date_group(value: &Value, interval: &str) -> Option<String> {
268    let date = date_of(value)?;
269    let (year, month, _) = (date.year(), date.month(), date.day());
270    Some(match interval {
271        "Year" => format!("{year:04}"),
272        "Month" => format!("{year:04}-{month:02}"),
273        _ => date.to_string(),
274    })
275}
276
277fn chart(data: &Data, cx: &mut BuildCx<'_>) -> Result<NodeId, String> {
278    let view = chart_of(data, CHART_HEIGHT)?;
279    Ok(cx.child(&view))
280}
281
282fn chart_of(data: &Data, height: f32) -> Result<Chart, String> {
283    const NODE: &str = "ChartSettings";
284    let argument = data
285        .field(NODE, "Argument", |field| !is_number(field))?
286        .ok_or_else(|| data.words("ChartSettings/Argument"))?;
287    let captions: BTreeMap<&str, &str> =
288        data.columns.iter().map(|(field, caption)| (field.name.as_str(), caption.as_str())).collect();
289    // `cost:Sum,hours:Average`, or every number shown, added up, or how many there are.
290    let series: Vec<(Option<&FieldInfo>, Summary, String)> = match data.setting(NODE, "Series") {
291        Some(text) => text
292            .split(',')
293            .map(str::trim)
294            .filter(|part| !part.is_empty())
295            .map(|part| {
296                let (name, summary) = part.split_once(':').unwrap_or((part, "Sum"));
297                let field = data
298                    .entity
299                    .field_named(name)
300                    .ok_or_else(|| data.words(&format!("{NODE}/Series: {name}")))?;
301                let caption = captions.get(name).map_or(field.caption.as_str(), |caption| caption);
302                Ok((Some(field), Summary::named(summary), caption.to_owned()))
303            })
304            .collect::<Result<_, String>>()?,
305        None => data
306            .columns
307            .iter()
308            .filter(|(field, _)| is_number(field))
309            .map(|(field, caption)| (Some(field), Summary::Sum, caption.clone()))
310            .collect(),
311    };
312    let series = if series.is_empty() {
313        vec![(None, Summary::Count, data.session.words().chart_count.clone())]
314    } else {
315        series
316    };
317    let names = match &argument.kind {
318        FieldKind::Reference(target) => data.names_of(target),
319        _ => BTreeMap::new(),
320    };
321    let interval = data.setting(NODE, "Interval").unwrap_or_else(|| "Day".to_owned());
322    let label_of = |object: &Object| {
323        let value = object.get(&argument.name);
324        if is_date(argument) {
325            return date_group(&value, &interval).unwrap_or_default();
326        }
327        let shown = display(argument, &value);
328        names.get(&shown).cloned().unwrap_or(shown)
329    };
330    // In the order they come, except for dates, which go as time does.
331    let mut order: Vec<String> = Vec::new();
332    let mut groups: BTreeMap<String, Vec<&Object>> = BTreeMap::new();
333    for object in data.objects.iter() {
334        let label = label_of(object);
335        if !groups.contains_key(&label) {
336            order.push(label.clone());
337        }
338        groups.entry(label).or_default().push(object);
339    }
340    if is_date(argument) {
341        order.sort();
342    }
343    let money =
344        series.iter().all(|(field, ..)| field.is_some_and(|field| matches!(field.kind, FieldKind::Money)));
345    let mut view = Chart::new(kind_named(data.setting(NODE, "Kind").as_deref()))
346        .categories(order.clone())
347        .height(height)
348        .show_value_axis()
349        .alt(data.entity.caption.clone());
350    for (field, summary, caption) in &series {
351        let values = order.iter().map(|label| {
352            let rows = &groups[label];
353            let numbers: Vec<f64> = match field {
354                Some(field) => rows
355                    .iter()
356                    .filter_map(|object| match object.get(&field.name) {
357                        Value::Number(number) => Some(number),
358                        _ => None,
359                    })
360                    .collect(),
361                None => vec![1.0; rows.len()],
362            };
363            let value = summary.of(&numbers);
364            // Money is kept in cents and read in whole units.
365            let in_cents = field.is_some_and(|field| matches!(field.kind, FieldKind::Money));
366            if in_cents && *summary != Summary::Count { value / 100.0 } else { value }
367        });
368        view = view.series(caption.clone(), values.collect::<Vec<_>>());
369    }
370    if money {
371        let words = data.session.words();
372        let (prefix, thousands, decimal) = (words.money_prefix.clone(), words.thousands, words.decimal);
373        view = view.format(move |value| {
374            ravenrs_widgets::grid::NumberFormat {
375                prefix: prefix.clone(),
376                decimals: 2,
377                thousands,
378                decimal,
379                ..ravenrs_widgets::grid::NumberFormat::default()
380            }
381            .format(value)
382        });
383    }
384    Ok(view)
385}
386
387fn given_data(
388    session: &Session,
389    entity: &EntityInfo,
390    objects: Rc<Vec<Object>>,
391    given: BTreeMap<String, String>,
392) -> Data {
393    Data {
394        session: session.clone(),
395        view: String::new(),
396        columns: entity.fields.iter().filter(|f| f.in_list).map(|f| (f.clone(), f.caption.clone())).collect(),
397        entity: entity.clone(),
398        objects,
399        on_open: Rc::new(|_| {}),
400        on_select: Rc::new(|_| {}),
401        given,
402    }
403}
404
405/// A chart of these records, from settings given directly, as a panel of its own wants it: the
406/// same names [`CHART_EDITOR`] reads from the model.
407pub fn chart_view(
408    session: &Session,
409    entity: &EntityInfo,
410    objects: Rc<Vec<Object>>,
411    settings: BTreeMap<String, String>,
412    height: f32,
413) -> Result<AnyView, String> {
414    let view = chart_of(&given_data(session, entity, objects, settings), height)?;
415    Ok(Box::new(view))
416}
417
418/// What these records come to, for one `field:Sum`, in words: money as money, and a count of the
419/// records with no field.
420pub fn summary_text(
421    session: &Session,
422    entity: &EntityInfo,
423    objects: &[Object],
424    series: &str,
425) -> Result<String, String> {
426    let words = session.words();
427    let (name, summary) = series.split_once(':').unwrap_or((series, "Sum"));
428    let (name, summary) = (name.trim(), Summary::named(summary.trim()));
429    if name.is_empty() {
430        return Ok(objects.len().to_string());
431    }
432    let Some(field) = entity.field_named(name) else {
433        return Err(format!("{} {series}", words.list_editor_needs));
434    };
435    let numbers: Vec<f64> = objects
436        .iter()
437        .filter_map(|object| match object.get(name) {
438            Value::Number(number) => Some(number),
439            _ => None,
440        })
441        .collect();
442    let value = summary.of(&numbers);
443    let money = matches!(field.kind, FieldKind::Money) && summary != Summary::Count;
444    let format = ravenrs_widgets::grid::NumberFormat {
445        prefix: if money { words.money_prefix.clone() } else { String::new() },
446        decimals: if money || (summary == Summary::Average) { 2 } else { 0 },
447        thousands: words.thousands,
448        decimal: words.decimal,
449        ..ravenrs_widgets::grid::NumberFormat::default()
450    };
451    Ok(format.format(if money { value / 100.0 } else { value }))
452}
453
454fn kind_named(name: Option<&str>) -> ChartKind {
455    match name {
456        Some("Line") => ChartKind::Line,
457        Some("Area") => ChartKind::Area,
458        Some("Pie") => ChartKind::Pie,
459        Some("Doughnut") => ChartKind::Doughnut,
460        _ => ChartKind::Bar,
461    }
462}
463
464fn calendar(data: &Data, cx: &mut BuildCx<'_>) -> Result<NodeId, String> {
465    const NODE: &str = "CalendarSettings";
466    let start =
467        data.field(NODE, "Start", is_date)?.ok_or_else(|| data.words("CalendarSettings/Start"))?.name.clone();
468    let today = data.session.today();
469    let month: State<_> = cx.use_state(|| today.first_of_month());
470    let day: State<Option<_>> = cx.use_state(|| None);
471    let events = data.objects.iter().enumerate().filter_map(|(at, object)| {
472        let date = date_of(&object.get(&start))?;
473        Some(
474            CalendarEvent::new(at as u64, date, data.title_of(NODE, object))
475                .tone(data.tone_of(NODE, object))
476                .done(data.is_done(NODE, object)),
477        )
478    });
479    let (objects, on_open, key) =
480        (data.objects.clone(), data.on_open.clone(), data.entity.key_field().cloned());
481    let view = EventCalendar::new(month.binding(), day.binding())
482        .today(today)
483        .events(events.collect::<Vec<_>>())
484        .on_event(move |at| {
485            if let (Some(object), Some(key)) = (objects.get(at as usize), &key) {
486                on_open(&object.get(&key.name));
487            }
488        });
489    Ok(cx.child(&view))
490}
491
492fn scheduler(data: &Data, cx: &mut BuildCx<'_>) -> Result<NodeId, String> {
493    const NODE: &str = "SchedulerSettings";
494    let start = data
495        .field(NODE, "Start", is_date)?
496        .ok_or_else(|| data.words("SchedulerSettings/Start"))?
497        .name
498        .clone();
499    let end = data
500        .field(NODE, "End", |field| is_date(field) && field.name != start)?
501        .map(|field| field.name.clone());
502    let resource =
503        data.field(NODE, "Resource", |field| matches!(field.kind, FieldKind::Reference(_)))?.cloned();
504    // A row for each record the reference can point to, and one for what points nowhere.
505    let names: Vec<(String, String)> = match resource.as_ref().map(|field| &field.kind) {
506        Some(FieldKind::Reference(target)) => data.names_of(target).into_iter().collect(),
507        _ => Vec::new(),
508    };
509    let row_of = |object: &Object| {
510        let resource = resource.as_ref()?;
511        let key = display(resource, &object.get(&resource.name));
512        names.iter().position(|(known, _)| *known == key).map(|at| at as u64 + 1)
513    };
514    let mut rows: Vec<Resource> =
515        names.iter().enumerate().map(|(at, (_, name))| Resource::new(at as u64 + 1, name.clone())).collect();
516    if resource.is_none() || data.objects.iter().any(|object| row_of(object).is_none()) {
517        let caption = if resource.is_none() {
518            data.entity.caption.clone()
519        } else {
520            data.session.words().no_resource.clone()
521        };
522        rows.insert(0, Resource::new(0, caption));
523    }
524    let writes = data.can(Operation::Write, None);
525    let events: Vec<SchedulerEvent> = data
526        .objects
527        .iter()
528        .enumerate()
529        .filter_map(|(at, object)| {
530            let from = minutes_of(&object.get(&start))?;
531            let to = end
532                .as_ref()
533                .and_then(|end| minutes_of(&object.get(end)))
534                .filter(|to| *to > from)
535                .unwrap_or_else(|| from.plus_minutes(NO_END_MINUTES));
536            Some(
537                SchedulerEvent::new(
538                    at as u64,
539                    row_of(object).unwrap_or(0),
540                    from,
541                    to,
542                    data.title_of(NODE, object),
543                )
544                .tone(data.tone_of(NODE, object))
545                .done(data.is_done(NODE, object))
546                .locked(!data.can(Operation::Write, Some(object))),
547            )
548        })
549        .collect();
550    let today = data.session.today();
551    let axis: State<TimeAxis> = cx.use_state(|| TimeAxis::days(today.first_of_month(), 31));
552    let selected: State<Option<u64>> = cx.use_state(|| None);
553    let (read, write) = (selected.clone(), selected.clone());
554    let (objects, on_select) = (data.objects.clone(), data.on_select.clone());
555    let selection = Binding::new(
556        move || read.get(),
557        move |id: Option<u64>| {
558            write.set(id);
559            on_select(id.and_then(|id| objects.get(id as usize).cloned()).into_iter().collect());
560        },
561    );
562    let shared = Rc::new(Keep {
563        data: Data {
564            session: data.session.clone(),
565            view: data.view.clone(),
566            entity: data.entity.clone(),
567            columns: Vec::new(),
568            objects: data.objects.clone(),
569            on_open: data.on_open.clone(),
570            on_select: data.on_select.clone(),
571            given: BTreeMap::new(),
572        },
573        start,
574        end,
575        resource,
576        keys: names.into_iter().map(|(key, _)| key).collect(),
577    });
578    let (changing, opening) = (shared.clone(), shared.clone());
579    let mut view = Scheduler::new(axis.binding())
580        .resources(rows)
581        .events(events)
582        .selection(selection)
583        .read_only(!writes)
584        .on_change(move |change| changing.moved(change))
585        .on_open(move |at| opening.opened(at));
586    if data.can(Operation::Create, None) {
587        let creating = shared;
588        view = view.on_create(move |change| creating.created(change));
589    }
590    Ok(cx.child(&view))
591}
592
593/// What the timeline needs once something happens on it.
594struct Keep {
595    data: Data,
596    start: String,
597    end: Option<String>,
598    resource: Option<FieldInfo>,
599    keys: Vec<String>,
600}
601
602#[cfg_attr(feature = "live", ravenrs_live::expose)]
603impl Keep {
604    /// The value of the reference for a row, as the kind of the key it points to.
605    fn resource_value(&self, row: u64) -> Value {
606        let Some(key) = row.checked_sub(1).and_then(|at| self.keys.get(at as usize)) else {
607            return Value::Null;
608        };
609        let Some(FieldKind::Reference(target)) = self.resource.as_ref().map(|field| &field.kind) else {
610            return Value::Null;
611        };
612        let numbered = self
613            .data
614            .session
615            .entity(target)
616            .and_then(|entity| entity.key_field().map(|key| matches!(key.kind, FieldKind::Integer)));
617        match (numbered, key.parse::<f64>()) {
618            (Some(true), Ok(number)) => Value::Number(number),
619            _ => Value::Text(key.clone()),
620        }
621    }
622
623    fn moved(&self, change: SchedulerChange) {
624        let Some(mut object) = change.id.and_then(|id| self.data.objects.get(id as usize).cloned()) else {
625            return;
626        };
627        object.set(&self.start, value_of(change.start));
628        if let Some(end) = &self.end {
629            object.set(end, value_of(change.end));
630        }
631        if let Some(resource) = &self.resource {
632            object.set(&resource.name, self.resource_value(change.resource));
633        }
634        self.data.store(object);
635    }
636
637    fn opened(&self, at: u64) {
638        if let Some(object) = self.data.objects.get(at as usize) {
639            (self.data.on_open)(&self.data.key_of(object));
640        }
641    }
642
643    fn created(&self, change: SchedulerChange) {
644        let session = &self.data.session;
645        session.open_new(&self.data.entity.name);
646        session.set_form_value(&self.start, value_of(change.start));
647        if let Some(end) = &self.end {
648            session.set_form_value(end, value_of(change.end));
649        }
650        if let Some(resource) = &self.resource {
651            session.set_form_value(&resource.name, self.resource_value(change.resource));
652        }
653    }
654}
655
656fn tree(data: &Data, cx: &mut BuildCx<'_>) -> Result<NodeId, String> {
657    const NODE: &str = "TreeSettings";
658    let entity_name = data.entity.name.clone();
659    let parent =
660        data.setting(NODE, "Parent")
661            .and_then(|name| data.entity.field_named(&name))
662            .or_else(|| {
663                data.entity.fields.iter().find(
664                    |field| matches!(&field.kind, FieldKind::Reference(target) if *target == entity_name),
665                )
666            })
667            .ok_or_else(|| data.words("TreeSettings/Parent"))?;
668    let Some(key_field) = data.entity.key_field() else {
669        return Err(data.words("TreeSettings/Key"));
670    };
671    let key_of = |object: &Object| display(key_field, &object.get(&key_field.name));
672    let parent_of = |object: &Object| display(parent, &object.get(&parent.name));
673    let known: BTreeSet<String> = data.objects.iter().map(key_of).collect();
674    let mut children: BTreeMap<String, Vec<&Object>> = BTreeMap::new();
675    let mut roots: Vec<&Object> = Vec::new();
676    for object in data.objects.iter() {
677        let above = parent_of(object);
678        // One whose parent cannot be seen starts its own branch.
679        if above.is_empty() || !known.contains(&above) || above == key_of(object) {
680            roots.push(object);
681        } else {
682            children.entry(above).or_default().push(object);
683        }
684    }
685    fn node_of(
686        data: &Data,
687        object: &Object,
688        children: &BTreeMap<String, Vec<&Object>>,
689        key_of: &dyn Fn(&Object) -> String,
690        seen: &mut BTreeSet<String>,
691    ) -> TreeNode {
692        let key = key_of(object);
693        let title = data.title_of("TreeSettings", object);
694        seen.insert(key.clone());
695        let mut node = TreeNode::new(key.clone(), title);
696        for child in children.get(&key).map_or(&[][..], Vec::as_slice) {
697            if !seen.contains(&key_of(child)) {
698                node = node.child(node_of(data, child, children, key_of, seen));
699            }
700        }
701        node
702    }
703    let mut seen = BTreeSet::new();
704    let mut nodes: Vec<TreeNode> =
705        roots.iter().map(|object| node_of(data, object, &children, &key_of, &mut seen)).collect();
706    // What only points at itself in a circle has no root, and still has to show.
707    for object in data.objects.iter() {
708        if !seen.contains(&key_of(object)) {
709            nodes.push(node_of(data, object, &children, &key_of, &mut seen));
710        }
711    }
712    let parents: BTreeSet<String> = children.keys().cloned().collect();
713    let expanded: State<BTreeSet<String>> = cx.use_state(|| parents);
714    let selected: State<Option<String>> = cx.use_state(|| None);
715    let by_key: BTreeMap<String, Object> =
716        data.objects.iter().map(|object| (key_of(object), object.clone())).collect();
717    let by_key = Rc::new(by_key);
718    let (read, write, choosing, on_select) =
719        (selected.clone(), selected.clone(), by_key.clone(), data.on_select.clone());
720    let selection = Binding::new(
721        move || read.get(),
722        move |key: Option<String>| {
723            on_select(key.as_ref().and_then(|key| choosing.get(key).cloned()).into_iter().collect());
724            write.set(key);
725        },
726    );
727    let (on_open, key_name) = (data.on_open.clone(), key_field.name.clone());
728    let view = TreeView::new(nodes)
729        .selected(selection)
730        .expanded(expanded.binding())
731        .height(TREE_HEIGHT)
732        .label(data.entity.caption.clone())
733        .on_open(move |key| {
734            if let Some(object) = by_key.get(key) {
735                on_open(&object.get(&key_name));
736            }
737        });
738    Ok(cx.child(&view))
739}