Skip to main content

things3_cloud/store/
entities.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3
4use crate::{
5    ids::ThingsId,
6    wire::{
7        area::{AreaPatch, AreaProps},
8        checklist::{ChecklistItemPatch, ChecklistItemProps},
9        notes::TaskNotes,
10        recurrence::RecurrenceRule,
11        tags::{TagPatch, TagProps},
12        task::{TaskPatch, TaskProps, TaskStart, TaskStatus, TaskType},
13        wire_object::{EntityType, Properties},
14    },
15};
16
17#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
18pub struct StateObject {
19    pub entity_type: Option<EntityType>,
20    pub properties: StateProperties,
21}
22
23#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
24pub enum StateProperties {
25    Task(TaskStateProps),
26    ChecklistItem(ChecklistItemStateProps),
27    Area(AreaStateProps),
28    Tag(TagStateProps),
29    Other,
30}
31
32#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
33pub struct TaskStateProps {
34    pub title: String,
35    pub notes: Option<String>,
36    pub item_type: TaskType,
37    pub status: TaskStatus,
38    pub stop_date: Option<f64>,
39    pub start_location: TaskStart,
40    pub scheduled_date: Option<f64>,
41    pub today_index_reference: Option<i64>,
42    pub deadline: Option<f64>,
43    pub parent_project_ids: Vec<ThingsId>,
44    pub area_ids: Vec<ThingsId>,
45    pub action_group_ids: Vec<ThingsId>,
46    pub tag_ids: Vec<ThingsId>,
47    pub sort_index: i32,
48    pub today_sort_index: i32,
49    pub recurrence_rule: Option<RecurrenceRule>,
50    pub recurrence_template_ids: Vec<ThingsId>,
51    pub instance_creation_paused: bool,
52    pub evening_bit: i32,
53    pub leaves_tombstone: bool,
54    pub trashed: bool,
55    pub creation_date: Option<f64>,
56    pub modification_date: Option<f64>,
57}
58
59#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
60pub struct ChecklistItemStateProps {
61    pub title: String,
62    pub status: TaskStatus,
63    pub task_ids: Vec<ThingsId>,
64    pub sort_index: i32,
65}
66
67#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
68pub struct AreaStateProps {
69    pub title: String,
70    pub tag_ids: Vec<ThingsId>,
71    pub sort_index: i32,
72}
73
74#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
75pub struct TagStateProps {
76    pub title: String,
77    pub shortcut: Option<String>,
78    pub sort_index: i32,
79    pub parent_ids: Vec<ThingsId>,
80}
81
82#[derive(Debug, Clone, PartialEq, Eq)]
83pub struct Tag {
84    pub uuid: ThingsId,
85    pub title: String,
86    pub shortcut: Option<String>,
87    pub index: i32,
88    pub parent_uuid: Option<ThingsId>,
89}
90
91#[derive(Debug, Clone, PartialEq, Eq)]
92pub struct Area {
93    pub uuid: ThingsId,
94    pub title: String,
95    pub tags: Vec<ThingsId>,
96    pub index: i32,
97}
98
99#[derive(Debug, Clone, PartialEq)]
100pub struct ChecklistItem {
101    pub uuid: ThingsId,
102    pub title: String,
103    pub task_uuid: ThingsId,
104    pub status: TaskStatus,
105    pub index: i32,
106}
107
108impl ChecklistItem {
109    pub fn is_incomplete(&self) -> bool {
110        self.status == TaskStatus::Incomplete
111    }
112
113    pub fn is_completed(&self) -> bool {
114        self.status == TaskStatus::Completed
115    }
116
117    pub fn is_canceled(&self) -> bool {
118        self.status == TaskStatus::Canceled
119    }
120}
121
122#[derive(Debug, Clone, PartialEq, Eq, Default)]
123pub struct ProjectProgress {
124    pub total: i32,
125    pub done: i32,
126}
127
128#[derive(Debug, Clone, PartialEq, Default)]
129pub struct Task {
130    pub uuid: ThingsId,
131    pub title: String,
132    pub status: TaskStatus,
133    pub start: TaskStart,
134    pub item_type: TaskType,
135    pub entity: String,
136    pub notes: Option<String>,
137    pub project: Option<ThingsId>,
138    pub area: Option<ThingsId>,
139    pub action_group: Option<ThingsId>,
140    pub tags: Vec<ThingsId>,
141    pub trashed: bool,
142    pub deadline: Option<DateTime<Utc>>,
143    pub start_date: Option<DateTime<Utc>>,
144    pub stop_date: Option<DateTime<Utc>>,
145    pub creation_date: Option<DateTime<Utc>>,
146    pub modification_date: Option<DateTime<Utc>>,
147    pub index: i32,
148    pub today_index: i32,
149    pub today_index_reference: Option<i64>,
150    pub leaves_tombstone: bool,
151    pub instance_creation_paused: bool,
152    pub evening: bool,
153    pub recurrence_rule: Option<RecurrenceRule>,
154    pub recurrence_templates: Vec<ThingsId>,
155    pub checklist_items: Vec<ChecklistItem>,
156}
157
158impl Task {
159    pub fn is_incomplete(&self) -> bool {
160        self.status == TaskStatus::Incomplete
161    }
162
163    pub fn is_completed(&self) -> bool {
164        self.status == TaskStatus::Completed
165    }
166
167    pub fn is_canceled(&self) -> bool {
168        self.status == TaskStatus::Canceled
169    }
170
171    pub fn is_todo(&self) -> bool {
172        self.item_type == TaskType::Todo
173    }
174
175    pub fn is_project(&self) -> bool {
176        self.item_type == TaskType::Project
177    }
178
179    pub fn is_heading(&self) -> bool {
180        self.item_type == TaskType::Heading
181    }
182
183    pub fn in_someday(&self) -> bool {
184        self.start == TaskStart::Someday && self.start_date.is_none()
185    }
186
187    pub fn is_today(&self, today: &DateTime<Utc>) -> bool {
188        let Some(start_date) = self.start_date else {
189            return false;
190        };
191        if self.start != TaskStart::Anytime && self.start != TaskStart::Someday {
192            return false;
193        }
194        start_date <= *today
195    }
196
197    pub fn is_staged_for_today(&self, today: &DateTime<Utc>) -> bool {
198        let Some(start_date) = self.start_date else {
199            return false;
200        };
201        self.start == TaskStart::Someday && start_date <= *today
202    }
203
204    pub fn is_recurrence_template(&self) -> bool {
205        self.recurrence_rule.is_some() && self.recurrence_templates.is_empty()
206    }
207
208    pub fn is_recurrence_instance(&self) -> bool {
209        self.recurrence_rule.is_none() && !self.recurrence_templates.is_empty()
210    }
211}
212
213fn i64_to_f64_opt(value: Option<i64>) -> Option<f64> {
214    value.map(|v| v as f64)
215}
216
217fn parse_notes_from_wire(notes: &Option<TaskNotes>) -> Option<String> {
218    notes.as_ref().and_then(TaskNotes::to_plain_text)
219}
220
221impl From<TaskProps> for TaskStateProps {
222    fn from(props: TaskProps) -> Self {
223        Self {
224            title: props.title,
225            notes: parse_notes_from_wire(&props.notes),
226            item_type: props.item_type,
227            status: props.status,
228            stop_date: props.stop_date,
229            start_location: props.start_location,
230            scheduled_date: i64_to_f64_opt(props.scheduled_date),
231            today_index_reference: props.today_index_reference,
232            deadline: i64_to_f64_opt(props.deadline),
233            parent_project_ids: props.parent_project_ids,
234            area_ids: props.area_ids,
235            action_group_ids: props.action_group_ids,
236            tag_ids: props.tag_ids,
237            sort_index: props.sort_index,
238            today_sort_index: props.today_sort_index,
239            recurrence_rule: props.recurrence_rule,
240            recurrence_template_ids: props.recurrence_template_ids,
241            instance_creation_paused: props.instance_creation_paused,
242            evening_bit: props.evening_bit,
243            leaves_tombstone: props.leaves_tombstone,
244            trashed: props.trashed,
245            creation_date: props.creation_date,
246            modification_date: props.modification_date,
247        }
248    }
249}
250
251impl From<ChecklistItemProps> for ChecklistItemStateProps {
252    fn from(props: ChecklistItemProps) -> Self {
253        Self {
254            title: props.title,
255            status: props.status,
256            task_ids: props.task_ids,
257            sort_index: props.sort_index,
258        }
259    }
260}
261
262impl From<AreaProps> for AreaStateProps {
263    fn from(props: AreaProps) -> Self {
264        Self {
265            title: props.title,
266            tag_ids: props.tag_ids,
267            sort_index: props.sort_index,
268        }
269    }
270}
271
272impl From<TagProps> for TagStateProps {
273    fn from(props: TagProps) -> Self {
274        Self {
275            title: props.title,
276            shortcut: props.shortcut,
277            sort_index: props.sort_index,
278            parent_ids: props.parent_ids,
279        }
280    }
281}
282
283impl From<ChecklistItemPatch> for ChecklistItemStateProps {
284    fn from(patch: ChecklistItemPatch) -> Self {
285        let mut item = Self::default();
286        if let Some(title) = patch.title {
287            item.title = title;
288        }
289        if let Some(status) = patch.status {
290            item.status = status;
291        }
292        if let Some(task_ids) = patch.task_ids {
293            item.task_ids = task_ids;
294        }
295        if let Some(sort_index) = patch.sort_index {
296            item.sort_index = sort_index;
297        }
298        item
299    }
300}
301
302impl From<AreaPatch> for AreaStateProps {
303    fn from(patch: AreaPatch) -> Self {
304        let mut area = Self::default();
305        if let Some(title) = patch.title {
306            area.title = title;
307        }
308        if let Some(tag_ids) = patch.tag_ids {
309            area.tag_ids = tag_ids;
310        }
311        if let Some(sort_index) = patch.sort_index {
312            area.sort_index = sort_index;
313        }
314        area
315    }
316}
317
318impl From<TagPatch> for TagStateProps {
319    fn from(patch: TagPatch) -> Self {
320        let mut tag = Self::default();
321        if let Some(title) = patch.title {
322            tag.title = title;
323        }
324        if let Some(parent_ids) = patch.parent_ids {
325            tag.parent_ids = parent_ids;
326        }
327        if let Some(shortcut) = patch.shortcut {
328            tag.shortcut = shortcut;
329        }
330        if let Some(sort_index) = patch.sort_index {
331            tag.sort_index = sort_index;
332        }
333        tag
334    }
335}
336
337impl From<TaskPatch> for TaskStateProps {
338    fn from(patch: TaskPatch) -> Self {
339        let mut task = Self::default();
340        if let Some(title) = patch.title {
341            task.title = title;
342        }
343        if let Some(notes) = patch.notes {
344            task.notes = notes.to_plain_text();
345        }
346        if let Some(start_location) = patch.start_location {
347            task.start_location = start_location;
348        }
349        if let Some(scheduled_date) = patch.scheduled_date {
350            task.scheduled_date = scheduled_date.map(|v| v as f64);
351        }
352        if let Some(today_index_reference) = patch.today_index_reference {
353            task.today_index_reference = today_index_reference;
354        }
355        if let Some(parent_project_ids) = patch.parent_project_ids {
356            task.parent_project_ids = parent_project_ids;
357        }
358        if let Some(area_ids) = patch.area_ids {
359            task.area_ids = area_ids;
360        }
361        if let Some(action_group_ids) = patch.action_group_ids {
362            task.action_group_ids = action_group_ids;
363        }
364        if let Some(tag_ids) = patch.tag_ids {
365            task.tag_ids = tag_ids;
366        }
367        if let Some(evening_bit) = patch.evening_bit {
368            task.evening_bit = evening_bit;
369        }
370        if let Some(modification_date) = patch.modification_date {
371            task.modification_date = Some(modification_date);
372        }
373        if let Some(item_type) = patch.item_type {
374            task.item_type = item_type;
375        }
376        if let Some(status) = patch.status {
377            task.status = status;
378        }
379        if let Some(stop_date) = patch.stop_date {
380            task.stop_date = stop_date;
381        }
382        if let Some(deadline) = patch.deadline {
383            task.deadline = deadline;
384        }
385        if let Some(sort_index) = patch.sort_index {
386            task.sort_index = sort_index;
387        }
388        if let Some(today_sort_index) = patch.today_sort_index {
389            task.today_sort_index = today_sort_index;
390        }
391        if let Some(recurrence_rule) = patch.recurrence_rule {
392            task.recurrence_rule = recurrence_rule;
393        }
394        if let Some(recurrence_template_ids) = patch.recurrence_template_ids {
395            task.recurrence_template_ids = recurrence_template_ids;
396        }
397        if let Some(instance_creation_paused) = patch.instance_creation_paused {
398            task.instance_creation_paused = instance_creation_paused;
399        }
400        if let Some(leaves_tombstone) = patch.leaves_tombstone {
401            task.leaves_tombstone = leaves_tombstone;
402        }
403        if let Some(trashed) = patch.trashed {
404            task.trashed = trashed;
405        }
406        if let Some(creation_date) = patch.creation_date {
407            task.creation_date = creation_date;
408        }
409        task
410    }
411}
412
413impl From<Properties> for StateProperties {
414    fn from(payload: Properties) -> Self {
415        use Properties::*;
416        use StateProperties::*;
417
418        match payload {
419            TaskCreate(props) => Task(props.into()),
420            TaskUpdate(patch) => Task(patch.into()),
421            ChecklistCreate(props) => ChecklistItem(props.into()),
422            ChecklistUpdate(patch) => ChecklistItem(patch.into()),
423            AreaCreate(props) => Area(props.into()),
424            AreaUpdate(patch) => Area(patch.into()),
425            TagCreate(props) => Tag(props.into()),
426            TagUpdate(patch) => Tag(patch.into()),
427            TombstoneCreate(_) | CommandCreate(_) | Ignored(_) | Unknown(_) | Delete => Other,
428        }
429    }
430}