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