Skip to main content

things3_cloud/store/
entities.rs

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