Skip to main content

things3_cloud/ui/views/json/
common.rs

1use chrono::{DateTime, Utc};
2use serde::Serialize;
3
4use crate::{
5    ids::ThingsId,
6    store::{Area, Tag, Task, ThingsStore},
7    wire::task::{TaskStart, TaskStatus, TaskType},
8};
9
10#[derive(Debug, Serialize)]
11pub struct ResolvedTaskJson {
12    #[serde(flatten)]
13    core: TaskCoreJson,
14    #[serde(flatten)]
15    links: TaskLinksJson,
16    dates: TaskDatesJson,
17    notes: Option<String>,
18    checklist: Vec<ResolvedChecklistItemJson>,
19    recurrence: TaskRecurrenceJson,
20    flags: TaskFlagsJson,
21    indexes: TaskIndexesJson,
22    #[serde(skip_serializing_if = "Option::is_none")]
23    progress: Option<TaskProgressJson>,
24}
25
26#[derive(Debug, Serialize)]
27pub struct ResolvedAreaJson {
28    pub id: String,
29    pub short_id: String,
30    pub title: String,
31    pub index: i32,
32    pub tags: Vec<TagRefJson>,
33}
34
35#[derive(Debug, Serialize)]
36pub struct ResolvedTagJson {
37    pub id: String,
38    pub short_id: String,
39    pub title: String,
40    pub shortcut: Option<String>,
41    pub parent: Option<LinkRefJson>,
42    pub index: i32,
43}
44
45#[derive(Debug, Serialize)]
46struct TaskProgressJson {
47    done: i32,
48    total: i32,
49}
50
51#[derive(Debug, Serialize)]
52pub struct TaskCoreJson {
53    pub id: String,
54    pub short_id: String,
55    pub title: String,
56    pub status: JsonTaskStatus,
57    pub r#type: JsonItemType,
58    pub start: TaskStartJson,
59}
60
61#[derive(Debug, Serialize)]
62pub struct TaskLinksJson {
63    pub project: Option<LinkRefJson>,
64    pub area: Option<LinkRefJson>,
65    pub heading: Option<LinkRefJson>,
66    pub tags: Vec<TagRefJson>,
67}
68
69#[derive(Debug, Serialize)]
70pub struct TaskStartJson {
71    pub bucket: JsonStartBucket,
72    pub scheduled_at: Option<String>,
73    pub today_index_reference: Option<i64>,
74    pub evening: bool,
75}
76
77#[derive(Debug, Serialize)]
78pub struct TaskDatesJson {
79    pub deadline_at: Option<String>,
80    pub created_at: Option<String>,
81    pub modified_at: Option<String>,
82    pub completed_at: Option<String>,
83}
84
85#[derive(Debug, Serialize)]
86pub struct TaskRecurrenceJson {
87    pub is_template: bool,
88    pub is_instance: bool,
89    pub rule: Option<serde_json::Value>,
90    pub template_ids: Vec<String>,
91}
92
93#[derive(Debug, Serialize)]
94pub struct TaskFlagsJson {
95    pub trashed: bool,
96    pub is_new: bool,
97    pub instance_creation_paused: bool,
98    pub leaves_tombstone: bool,
99}
100
101#[derive(Debug, Serialize)]
102pub struct TaskIndexesJson {
103    pub sort_index: i32,
104    pub today_sort_index: i32,
105}
106
107#[derive(Debug, Serialize)]
108pub struct LinkRefJson {
109    pub id: String,
110    pub short_id: String,
111    pub title: String,
112}
113
114#[derive(Debug, Serialize)]
115pub struct TagRefJson {
116    pub id: String,
117    pub short_id: String,
118    pub title: String,
119    pub shortcut: Option<String>,
120}
121
122#[derive(Debug, Serialize)]
123pub struct ResolvedChecklistItemJson {
124    pub id: String,
125    pub short_id: String,
126    pub title: String,
127    pub status: JsonTaskStatus,
128    pub index: i32,
129}
130
131#[derive(Debug, Clone, Copy, Serialize)]
132#[serde(rename_all = "snake_case")]
133pub enum JsonTaskStatus {
134    Incomplete,
135    Completed,
136    Canceled,
137}
138
139#[derive(Debug, Clone, Copy, Serialize)]
140#[serde(rename_all = "snake_case")]
141pub enum JsonStartBucket {
142    Inbox,
143    Anytime,
144    Someday,
145}
146
147#[derive(Debug, Clone, Copy, Serialize)]
148#[serde(rename_all = "snake_case")]
149pub enum JsonItemType {
150    Todo,
151    Project,
152}
153
154pub fn link_ref(id: &str, title: String, store: &ThingsStore) -> LinkRefJson {
155    LinkRefJson {
156        id: id.to_string(),
157        short_id: store.short_id(id),
158        title,
159    }
160}
161
162pub fn resolve_heading_title(id: &ThingsId, store: &ThingsStore) -> String {
163    store
164        .tasks_by_uuid
165        .get(id)
166        .map(|task| task.title.clone())
167        .unwrap_or_else(|| id.to_string())
168}
169
170pub fn task_status_json(status: TaskStatus) -> JsonTaskStatus {
171    match status {
172        TaskStatus::Incomplete => JsonTaskStatus::Incomplete,
173        TaskStatus::Completed => JsonTaskStatus::Completed,
174        TaskStatus::Canceled => JsonTaskStatus::Canceled,
175        TaskStatus::Unknown(_) => JsonTaskStatus::Incomplete,
176    }
177}
178
179pub fn task_start_json(start: TaskStart) -> JsonStartBucket {
180    match start {
181        TaskStart::Inbox => JsonStartBucket::Inbox,
182        TaskStart::Anytime => JsonStartBucket::Anytime,
183        TaskStart::Someday => JsonStartBucket::Someday,
184        TaskStart::Unknown(_) => JsonStartBucket::Inbox,
185    }
186}
187
188pub fn task_type_json(item_type: TaskType) -> JsonItemType {
189    match item_type {
190        TaskType::Project => JsonItemType::Project,
191        TaskType::Todo | TaskType::Heading | TaskType::Unknown(_) => JsonItemType::Todo,
192    }
193}
194
195pub fn build_tasks_json(
196    tasks: &[Task],
197    store: &ThingsStore,
198    today: &DateTime<Utc>,
199) -> Vec<ResolvedTaskJson> {
200    tasks
201        .iter()
202        .map(|task| task_to_json(task, store, today))
203        .collect()
204}
205
206pub fn build_area_json(area: &Area, store: &ThingsStore) -> ResolvedAreaJson {
207    area_to_json(area, store)
208}
209
210pub fn build_tags_json(tags: &[Tag], store: &ThingsStore) -> Vec<ResolvedTagJson> {
211    tags.iter().map(|tag| tag_to_json(tag, store)).collect()
212}
213
214fn task_to_json(task: &Task, store: &ThingsStore, today: &DateTime<Utc>) -> ResolvedTaskJson {
215    ResolvedTaskJson {
216        core: TaskCoreJson {
217            id: task.uuid.to_string(),
218            short_id: store.short_id(&task.uuid),
219            title: task.title.clone(),
220            status: task_status_json(task.status),
221            r#type: task_type_json(task.item_type),
222            start: TaskStartJson {
223                bucket: task_start_json(task.start),
224                scheduled_at: task.start_date.map(|d| d.to_rfc3339()),
225                today_index_reference: task.today_index_reference,
226                evening: task.evening,
227            },
228        },
229        links: TaskLinksJson {
230            project: store
231                .effective_project_uuid(task)
232                .map(|id| link_ref(&id.to_string(), store.resolve_project_title(&id), store)),
233            area: store
234                .effective_area_uuid(task)
235                .map(|id| link_ref(&id.to_string(), store.resolve_area_title(&id), store)),
236            heading: task
237                .action_group
238                .as_ref()
239                .map(|id| link_ref(&id.to_string(), resolve_heading_title(id, store), store)),
240            tags: task
241                .tags
242                .iter()
243                .map(|tag_id| {
244                    let tag = store.tags_by_uuid.get(tag_id);
245                    TagRefJson {
246                        id: tag_id.to_string(),
247                        short_id: store.short_id(tag_id),
248                        title: tag.map(|t| t.title.clone()).unwrap_or_default(),
249                        shortcut: tag.and_then(|t| t.shortcut.clone()),
250                    }
251                })
252                .collect(),
253        },
254        dates: TaskDatesJson {
255            deadline_at: task.deadline.map(|d| d.to_rfc3339()),
256            created_at: task.creation_date.map(|d| d.to_rfc3339()),
257            modified_at: task.modification_date.map(|d| d.to_rfc3339()),
258            completed_at: task.stop_date.map(|d| d.to_rfc3339()),
259        },
260        notes: task.notes.clone(),
261        checklist: task
262            .checklist_items
263            .iter()
264            .map(|item| ResolvedChecklistItemJson {
265                id: item.uuid.to_string(),
266                short_id: store.short_id(&item.uuid),
267                title: item.title.clone(),
268                status: task_status_json(item.status),
269                index: item.index,
270            })
271            .collect(),
272        recurrence: TaskRecurrenceJson {
273            is_template: task.is_recurrence_template(),
274            is_instance: task.is_recurrence_instance(),
275            rule: task
276                .recurrence_rule
277                .as_ref()
278                .map(|rule| serde_json::to_value(rule).unwrap_or(serde_json::Value::Null)),
279            template_ids: task
280                .recurrence_templates
281                .iter()
282                .map(ToString::to_string)
283                .collect(),
284        },
285        flags: TaskFlagsJson {
286            trashed: task.trashed,
287            is_new: task.is_staged_for_today(today),
288            instance_creation_paused: task.instance_creation_paused,
289            leaves_tombstone: task.leaves_tombstone,
290        },
291        indexes: TaskIndexesJson {
292            sort_index: task.index,
293            today_sort_index: task.today_index,
294        },
295        progress: if task.is_project() {
296            let progress = store.project_progress(&task.uuid);
297            Some(TaskProgressJson {
298                done: progress.done,
299                total: progress.total,
300            })
301        } else {
302            None
303        },
304    }
305}
306
307fn area_to_json(area: &Area, store: &ThingsStore) -> ResolvedAreaJson {
308    ResolvedAreaJson {
309        id: area.uuid.to_string(),
310        short_id: store.short_id(&area.uuid),
311        title: area.title.clone(),
312        index: area.index,
313        tags: area
314            .tags
315            .iter()
316            .map(|tag_id| {
317                let tag = store.tags_by_uuid.get(tag_id);
318                TagRefJson {
319                    id: tag_id.to_string(),
320                    short_id: store.short_id(tag_id),
321                    title: tag.map(|t| t.title.clone()).unwrap_or_default(),
322                    shortcut: tag.and_then(|t| t.shortcut.clone()),
323                }
324            })
325            .collect(),
326    }
327}
328
329fn tag_to_json(tag: &Tag, store: &ThingsStore) -> ResolvedTagJson {
330    ResolvedTagJson {
331        id: tag.uuid.to_string(),
332        short_id: store.short_id(&tag.uuid),
333        title: tag.title.clone(),
334        shortcut: tag.shortcut.clone(),
335        parent: tag.parent_uuid.as_ref().map(|id| {
336            link_ref(
337                &id.to_string(),
338                store.resolve_tag_title(id.to_string()),
339                store,
340            )
341        }),
342        index: tag.index,
343    }
344}