Skip to main content

things3_cloud/ui/views/
projects.rs

1use crate::common::ICONS;
2use crate::ids::ThingsId;
3use crate::store::Task;
4use crate::ui::components::empty_text::EmptyText;
5use crate::ui::components::id::Id;
6use crate::ui::components::tasks::{TaskList, TaskOptions};
7use iocraft::prelude::*;
8
9#[derive(Clone)]
10pub struct ProjectsAreaGroup {
11    pub area_uuid: ThingsId,
12    pub area_title: String,
13    pub projects: Vec<Task>,
14}
15
16#[derive(Default, Props)]
17pub struct ProjectsViewProps {
18    pub projects_count: usize,
19    pub no_area_projects: Vec<Task>,
20    pub area_groups: Vec<ProjectsAreaGroup>,
21    pub detailed: bool,
22    pub id_prefix_len: usize,
23}
24
25#[component]
26pub fn ProjectsView<'a>(props: &'a ProjectsViewProps) -> impl Into<AnyElement<'a>> {
27    if props.projects_count == 0 {
28        return element! { EmptyText(content: "No active projects.") }.into_any();
29    }
30
31    let options = TaskOptions {
32        detailed: props.detailed,
33        show_project: false,
34        show_area: false,
35        show_today_markers: true,
36        show_staged_today_marker: false,
37    };
38
39    let free_projects = if !props.no_area_projects.is_empty() {
40        element! {
41            View(flex_direction: FlexDirection::Column, padding_left: 2) {
42                TaskList(
43                    items: props.no_area_projects.iter().collect::<Vec<_>>(),
44                    id_prefix_len: props.id_prefix_len,
45                    options,
46                )
47            }
48        }
49        .into_any()
50    } else {
51        element!(Fragment).into_any()
52    };
53
54    let project_areas = props.area_groups.iter().map(|group| {
55        element! {
56            ProjectsAreaSection(group, id_prefix_len: props.id_prefix_len, options)
57        }
58    });
59
60    element! {
61        View(flex_direction: FlexDirection::Column) {
62            Text(
63                content: format!("● Projects  ({})", props.projects_count),
64                color: Color::Green,
65                weight: Weight::Bold,
66                wrap: TextWrap::NoWrap,
67            )
68            Text(content: "", wrap: TextWrap::NoWrap)
69            #(free_projects)
70            #(project_areas)
71        }
72    }
73    .into_any()
74}
75
76#[derive(Default, Props)]
77struct ProjectsAreaSectionProps<'a> {
78    pub group: Option<&'a ProjectsAreaGroup>,
79    pub id_prefix_len: usize,
80    pub options: TaskOptions,
81}
82
83#[component]
84fn ProjectsAreaSection<'a>(props: &ProjectsAreaSectionProps<'a>) -> impl Into<AnyElement<'a>> {
85    let Some(group) = props.group else {
86        return element!(Fragment).into_any();
87    };
88
89    element! {
90        View(flex_direction: FlexDirection::Column, padding_left: 2) {
91            Text(content: "", wrap: TextWrap::NoWrap)
92            View(flex_direction: FlexDirection::Row, gap: 1) {
93                Id(id: &group.area_uuid, length: props.id_prefix_len)
94                Text(content: ICONS.area, color: Color::DarkGrey)
95                Text(content: &group.area_title, wrap: TextWrap::NoWrap, weight: Weight::Bold)
96            }
97            View(flex_direction: FlexDirection::Column, padding_left: 2) {
98                TaskList(
99                    items: group.projects.iter().collect::<Vec<_>>(),
100                    id_prefix_len: props.id_prefix_len,
101                    options: props.options,
102                )
103            }
104        }
105    }
106    .into_any()
107}