Skip to main content

things3_cloud/ui/views/
projects.rs

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