Fix sync: CreateList action + server ID mapping

- Added SyncAction::CreateList variant
- create_task returns server Task, added create_list API
- Sync engine processes CreateList first, updates IDs in batch
- After Create/CreateList success, local IDs updated to server IDs
This commit is contained in:
Ruben Rosario
2026-06-21 18:14:24 +01:00
parent 3379cbd057
commit a35eab35af
5 changed files with 181 additions and 91 deletions
+85 -88
View File
@@ -196,47 +196,7 @@ impl ApiClient {
let tasks = all_items
.iter()
.enumerate()
.map(|(i, item)| {
let due_str = item["due"].as_str().and_then(|s| {
chrono::NaiveDateTime::parse_from_str(
&s.replace("T", " ")
.replace("Z", "")
.chars()
.take(16)
.collect::<String>(),
"%Y-%m-%d %H:%M",
)
.ok()
});
let updated = item["updated"].as_str().and_then(|s| {
chrono::NaiveDateTime::parse_from_str(
&s.replace("T", " ")
.replace("Z", "")
.chars()
.take(19)
.collect::<String>(),
"%Y-%m-%d %H:%M:%S",
)
.ok()
});
Task {
id: item["id"].as_str().unwrap_or("").to_string(),
list_id: list_id.to_string(),
title: item["title"].as_str().unwrap_or("").to_string(),
notes: item["notes"].as_str().map(|s| s.to_string()),
status: if item["status"].as_str() == Some("completed") {
TaskStatus::Completed
} else {
TaskStatus::NeedsAction
},
due: due_str,
position: i as i64,
created_at: None,
updated_at: updated,
}
})
.map(|(i, item)| parse_task_value(item, list_id, i))
.collect();
Ok(tasks)
@@ -290,53 +250,13 @@ impl ApiClient {
let tasks = all_items
.iter()
.enumerate()
.map(|(i, item)| {
let due_str = item["due"].as_str().and_then(|s| {
chrono::NaiveDateTime::parse_from_str(
&s.replace("T", " ")
.replace("Z", "")
.chars()
.take(16)
.collect::<String>(),
"%Y-%m-%d %H:%M",
)
.ok()
});
let updated = item["updated"].as_str().and_then(|s| {
chrono::NaiveDateTime::parse_from_str(
&s.replace("T", " ")
.replace("Z", "")
.chars()
.take(19)
.collect::<String>(),
"%Y-%m-%d %H:%M:%S",
)
.ok()
});
Task {
id: item["id"].as_str().unwrap_or("").to_string(),
list_id: list_id.to_string(),
title: item["title"].as_str().unwrap_or("").to_string(),
notes: item["notes"].as_str().map(|s| s.to_string()),
status: if item["status"].as_str() == Some("completed") {
TaskStatus::Completed
} else {
TaskStatus::NeedsAction
},
due: due_str,
position: i as i64,
created_at: None,
updated_at: updated,
}
})
.map(|(i, item)| parse_task_value(item, list_id, i))
.collect();
Ok(tasks)
}
pub async fn create_task(&self, list_id: &str, task: &Task) -> Result<(), ApiError> {
pub async fn create_task(&self, list_id: &str, task: &Task) -> Result<Task, ApiError> {
let token = self.get_token().await?;
let mut body = serde_json::json!({
@@ -369,13 +289,48 @@ impl ApiClient {
.map_err(|e| ApiError::Network(e.to_string()))?;
if !resp.status().is_success() {
return Err(ApiError::Api(format!(
"Create failed: {}",
resp.status()
)));
let status = resp.status();
let body_text = resp.text().await.unwrap_or_default();
return Err(ApiError::Api(format!("Create failed: {} - {}", status, body_text)));
}
Ok(())
let data: serde_json::Value = resp
.json()
.await
.map_err(|e| ApiError::Api(format!("Create parse error: {}", e)))?;
Ok(parse_task_value(&data, list_id, 0))
}
pub async fn create_list(&self, title: &str) -> Result<TaskList, ApiError> {
let token = self.get_token().await?;
let body = serde_json::json!({ "title": title });
let resp = self
.client
.post("https://tasks.googleapis.com/tasks/v1/users/@me/lists")
.bearer_auth(&token)
.json(&body)
.send()
.await
.map_err(|e| ApiError::Network(e.to_string()))?;
if !resp.status().is_success() {
let status = resp.status();
let body_text = resp.text().await.unwrap_or_default();
return Err(ApiError::Api(format!("Create list failed: {} - {}", status, body_text)));
}
let data: serde_json::Value = resp
.json()
.await
.map_err(|e| ApiError::Api(format!("Create list parse error: {}", e)))?;
Ok(TaskList {
id: data["id"].as_str().unwrap_or("").to_string(),
title: data["title"].as_str().unwrap_or("").to_string(),
})
}
pub async fn update_task(&self, list_id: &str, task: &Task) -> Result<(), ApiError> {
@@ -530,6 +485,48 @@ impl ApiClient {
}
}
fn parse_task_value(item: &serde_json::Value, list_id: &str, position: usize) -> Task {
let due_str = item["due"].as_str().and_then(|s| {
chrono::NaiveDateTime::parse_from_str(
&s.replace("T", " ")
.replace("Z", "")
.chars()
.take(16)
.collect::<String>(),
"%Y-%m-%d %H:%M",
)
.ok()
});
let updated = item["updated"].as_str().and_then(|s| {
chrono::NaiveDateTime::parse_from_str(
&s.replace("T", " ")
.replace("Z", "")
.chars()
.take(19)
.collect::<String>(),
"%Y-%m-%d %H:%M:%S",
)
.ok()
});
Task {
id: item["id"].as_str().unwrap_or("").to_string(),
list_id: list_id.to_string(),
title: item["title"].as_str().unwrap_or("").to_string(),
notes: item["notes"].as_str().map(|s| s.to_string()),
status: if item["status"].as_str() == Some("completed") {
TaskStatus::Completed
} else {
TaskStatus::NeedsAction
},
due: due_str,
position: position as i64,
created_at: None,
updated_at: updated,
}
}
fn parse_calendar_time(obj: &serde_json::Value) -> Option<chrono::NaiveDateTime> {
if let Some(dt) = obj["dateTime"].as_str() {
chrono::DateTime::parse_from_rfc3339(dt).ok().map(|d| d.naive_local())
+43
View File
@@ -281,6 +281,7 @@ impl Db {
SyncAction::Update => "Update",
SyncAction::Delete => "Delete",
SyncAction::Reorder => "Reorder",
SyncAction::CreateList => "CreateList",
};
let conn = self.conn.lock().unwrap();
conn.execute(
@@ -306,6 +307,47 @@ impl Db {
count > 0
}
pub fn update_task_id(&self, old_id: &str, new_id: &str) -> SqlResult<()> {
let conn = self.conn.lock().unwrap();
conn.execute(
"UPDATE tasks SET id = ?1 WHERE id = ?2",
params![new_id, old_id],
)?;
Ok(())
}
pub fn update_sync_task_id(&self, old_id: &str, new_id: &str) -> SqlResult<()> {
let conn = self.conn.lock().unwrap();
conn.execute(
"UPDATE sync_queue SET task_id = ?1 WHERE task_id = ?2",
params![new_id, old_id],
)?;
Ok(())
}
pub fn update_list_id(&self, old_id: &str, new_id: &str) -> SqlResult<()> {
let conn = self.conn.lock().unwrap();
conn.execute(
"UPDATE task_lists SET id = ?1 WHERE id = ?2",
params![new_id, old_id],
)?;
conn.execute(
"UPDATE tasks SET list_id = ?1 WHERE list_id = ?2",
params![new_id, old_id],
)?;
Ok(())
}
#[allow(dead_code)]
pub fn update_sync_list_id(&self, old_id: &str, new_id: &str) -> SqlResult<()> {
let conn = self.conn.lock().unwrap();
conn.execute(
"UPDATE sync_queue SET list_id = ?1 WHERE list_id = ?2",
params![new_id, old_id],
)?;
Ok(())
}
pub fn drain_sync(&self) -> Vec<SyncQueueItem> {
let conn = self.conn.lock().unwrap();
let items: Vec<SyncQueueItem> = {
@@ -319,6 +361,7 @@ impl Db {
"\"Update\"" | "Update" => SyncAction::Update,
"\"Delete\"" | "Delete" => SyncAction::Delete,
"\"Reorder\"" | "Reorder" => SyncAction::Reorder,
"\"CreateList\"" | "CreateList" => SyncAction::CreateList,
_ => SyncAction::Update,
};
Ok(SyncQueueItem {