things3_cloud/ui/views/
project.rs1use crate::store::{Task, ThingsStore};
2use crate::ui::components::deadline_badge::DeadlineBadge;
3use crate::ui::components::details_container::DetailsContainer;
4use crate::ui::components::progress_badge::ProgressBadge;
5use crate::ui::components::tags_badge::TagsBadge;
6use crate::ui::components::tasks::{TaskList, TaskOptions};
7use iocraft::prelude::*;
8use std::sync::Arc;
9
10#[derive(Clone)]
11pub struct ProjectHeadingGroup<'a> {
12 pub title: String,
13 pub items: Vec<&'a Task>,
14}
15
16#[derive(Default, Props)]
17pub struct ProjectViewProps<'a> {
18 pub project: Option<&'a Task>,
19 pub ungrouped: Vec<&'a Task>,
20 pub heading_groups: Vec<ProjectHeadingGroup<'a>>,
21 pub detailed: bool,
22 pub no_color: bool,
23}
24
25#[component]
26pub fn ProjectView<'a>(hooks: Hooks, props: &ProjectViewProps<'a>) -> impl Into<AnyElement<'a>> {
27 let store = hooks.use_context::<Arc<ThingsStore>>().clone();
28 let Some(project) = props.project else {
29 return element! { Text(content: "") }.into_any();
30 };
31 let _ = props.no_color;
32
33 let mut all_uuids = props
34 .ungrouped
35 .iter()
36 .map(|t| t.uuid.clone())
37 .collect::<Vec<_>>();
38 for group in &props.heading_groups {
39 all_uuids.extend(group.items.iter().map(|t| t.uuid.clone()));
40 }
41 let id_prefix_len = store.unique_prefix_length(&all_uuids);
42
43 let options = TaskOptions {
44 detailed: props.detailed,
45 show_project: false,
46 show_area: false,
47 show_today_markers: true,
48 show_staged_today_marker: false,
49 };
50
51 let note_lines = project
52 .notes
53 .as_deref()
54 .unwrap_or("")
55 .lines()
56 .map(|line| {
57 element! {
58 Text(content: line, wrap: TextWrap::NoWrap, color: Color::DarkGrey)
59 }
60 .into_any()
61 })
62 .collect::<Vec<_>>();
63
64 element! {
65 View(flex_direction: FlexDirection::Column) {
66 View(flex_direction: FlexDirection::Row, gap: 1) {
67 ProgressBadge(
68 project: project,
69 title: Some(project.title.clone()),
70 show_count: true,
71 color: Color::Magenta,
72 weight: Weight::Bold,
73 )
74 DeadlineBadge(deadline: project.deadline)
75 TagsBadge(tags: project.tags.clone())
76 }
77
78 #(if !note_lines.is_empty() {
79 Some(element! {
80 View(flex_direction: FlexDirection::Column, padding_left: 2) {
81 DetailsContainer {
82 #(note_lines)
83 }
84 }
85 })
86 } else { None })
87
88 #(if props.ungrouped.is_empty() && props.heading_groups.is_empty() {
89 Some(element! {
90 Text(content: " No tasks.", wrap: TextWrap::NoWrap, color: Color::DarkGrey)
91 })
92 } else { None })
93
94 #(if !props.ungrouped.is_empty() {
95 Some(element! {
96 View(flex_direction: FlexDirection::Column) {
97 Text(content: "", wrap: TextWrap::NoWrap)
98 View(flex_direction: FlexDirection::Column, padding_left: 2) {
99 TaskList(items: props.ungrouped.clone(), id_prefix_len, options)
100 }
101 }
102 })
103 } else { None })
104
105 #(props.heading_groups.iter().map(|group| element! {
106 View(flex_direction: FlexDirection::Column) {
107 Text(content: "", wrap: TextWrap::NoWrap)
108 Text(content: format!(" {}", group.title), wrap: TextWrap::NoWrap, weight: Weight::Bold)
109 View(flex_direction: FlexDirection::Column, padding_left: 4) {
110 TaskList(items: group.items.clone(), id_prefix_len, options)
111 }
112 }
113 }))
114 }
115 }
116 .into_any()
117}