1use 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
25pub const CHART_EDITOR: &str = "Chart";
27pub const CALENDAR_EDITOR: &str = "Calendar";
29pub const SCHEDULER_EDITOR: &str = "Scheduler";
31pub const TREE_EDITOR: &str = "Tree";
33
34const 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
40pub(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
53struct 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 given: BTreeMap<String, String>,
64}
65
66struct 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 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 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 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 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#[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
266fn 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 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 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 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
405pub 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
418pub 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 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
593struct 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 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 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 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}