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:
+1
-1
@@ -473,7 +473,7 @@ impl App {
|
|||||||
};
|
};
|
||||||
self.db.insert_list(&list).ok();
|
self.db.insert_list(&list).ok();
|
||||||
self.db.push_sync(
|
self.db.push_sync(
|
||||||
SyncAction::Create,
|
SyncAction::CreateList,
|
||||||
&list.id,
|
&list.id,
|
||||||
&list.id,
|
&list.id,
|
||||||
&serde_json::to_string(&list).unwrap_or_default(),
|
&serde_json::to_string(&list).unwrap_or_default(),
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ pub enum SyncAction {
|
|||||||
Update,
|
Update,
|
||||||
Delete,
|
Delete,
|
||||||
Reorder,
|
Reorder,
|
||||||
|
CreateList,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
|||||||
+85
-88
@@ -196,47 +196,7 @@ impl ApiClient {
|
|||||||
let tasks = all_items
|
let tasks = all_items
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(i, item)| {
|
.map(|(i, item)| parse_task_value(item, list_id, i))
|
||||||
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,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
Ok(tasks)
|
Ok(tasks)
|
||||||
@@ -290,53 +250,13 @@ impl ApiClient {
|
|||||||
let tasks = all_items
|
let tasks = all_items
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(i, item)| {
|
.map(|(i, item)| parse_task_value(item, list_id, i))
|
||||||
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,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
Ok(tasks)
|
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 token = self.get_token().await?;
|
||||||
|
|
||||||
let mut body = serde_json::json!({
|
let mut body = serde_json::json!({
|
||||||
@@ -369,13 +289,48 @@ impl ApiClient {
|
|||||||
.map_err(|e| ApiError::Network(e.to_string()))?;
|
.map_err(|e| ApiError::Network(e.to_string()))?;
|
||||||
|
|
||||||
if !resp.status().is_success() {
|
if !resp.status().is_success() {
|
||||||
return Err(ApiError::Api(format!(
|
let status = resp.status();
|
||||||
"Create failed: {}",
|
let body_text = resp.text().await.unwrap_or_default();
|
||||||
resp.status()
|
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> {
|
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> {
|
fn parse_calendar_time(obj: &serde_json::Value) -> Option<chrono::NaiveDateTime> {
|
||||||
if let Some(dt) = obj["dateTime"].as_str() {
|
if let Some(dt) = obj["dateTime"].as_str() {
|
||||||
chrono::DateTime::parse_from_rfc3339(dt).ok().map(|d| d.naive_local())
|
chrono::DateTime::parse_from_rfc3339(dt).ok().map(|d| d.naive_local())
|
||||||
|
|||||||
@@ -281,6 +281,7 @@ impl Db {
|
|||||||
SyncAction::Update => "Update",
|
SyncAction::Update => "Update",
|
||||||
SyncAction::Delete => "Delete",
|
SyncAction::Delete => "Delete",
|
||||||
SyncAction::Reorder => "Reorder",
|
SyncAction::Reorder => "Reorder",
|
||||||
|
SyncAction::CreateList => "CreateList",
|
||||||
};
|
};
|
||||||
let conn = self.conn.lock().unwrap();
|
let conn = self.conn.lock().unwrap();
|
||||||
conn.execute(
|
conn.execute(
|
||||||
@@ -306,6 +307,47 @@ impl Db {
|
|||||||
count > 0
|
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> {
|
pub fn drain_sync(&self) -> Vec<SyncQueueItem> {
|
||||||
let conn = self.conn.lock().unwrap();
|
let conn = self.conn.lock().unwrap();
|
||||||
let items: Vec<SyncQueueItem> = {
|
let items: Vec<SyncQueueItem> = {
|
||||||
@@ -319,6 +361,7 @@ impl Db {
|
|||||||
"\"Update\"" | "Update" => SyncAction::Update,
|
"\"Update\"" | "Update" => SyncAction::Update,
|
||||||
"\"Delete\"" | "Delete" => SyncAction::Delete,
|
"\"Delete\"" | "Delete" => SyncAction::Delete,
|
||||||
"\"Reorder\"" | "Reorder" => SyncAction::Reorder,
|
"\"Reorder\"" | "Reorder" => SyncAction::Reorder,
|
||||||
|
"\"CreateList\"" | "CreateList" => SyncAction::CreateList,
|
||||||
_ => SyncAction::Update,
|
_ => SyncAction::Update,
|
||||||
};
|
};
|
||||||
Ok(SyncQueueItem {
|
Ok(SyncQueueItem {
|
||||||
|
|||||||
+51
-2
@@ -246,7 +246,7 @@ async fn push_sync(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let items = db.drain_sync();
|
let mut items = db.drain_sync();
|
||||||
let count = items.len();
|
let count = items.len();
|
||||||
|
|
||||||
if count == 0 {
|
if count == 0 {
|
||||||
@@ -257,7 +257,50 @@ async fn push_sync(
|
|||||||
*network_status.lock().await = NetworkStatus::Syncing;
|
*network_status.lock().await = NetworkStatus::Syncing;
|
||||||
|
|
||||||
let mut all_ok = true;
|
let mut all_ok = true;
|
||||||
|
|
||||||
|
// First pass: CreateList items (so list IDs are updated before task operations)
|
||||||
|
for i in 0..items.len() {
|
||||||
|
if items[i].action != SyncAction::CreateList {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let list: TaskList = serde_json::from_str(&items[i].payload).unwrap_or_else(|_| TaskList {
|
||||||
|
id: items[i].task_id.clone(),
|
||||||
|
title: String::new(),
|
||||||
|
});
|
||||||
|
match api.create_list(&list.title).await {
|
||||||
|
Ok(server_list) => {
|
||||||
|
if server_list.id != items[i].task_id {
|
||||||
|
let _ = db.update_list_id(&items[i].task_id, &server_list.id);
|
||||||
|
// Update list_id in remaining items of this batch
|
||||||
|
for j in (i + 1)..items.len() {
|
||||||
|
if items[j].list_id == items[i].task_id {
|
||||||
|
items[j].list_id = server_list.id.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!("[task_app] Sync failed (retry {}/{}): action=CreateList list={} error={}",
|
||||||
|
items[i].retries, MAX_SYNC_RETRIES, items[i].task_id, err);
|
||||||
|
if items[i].retries < MAX_SYNC_RETRIES {
|
||||||
|
let _ = db.push_sync_with_retry(
|
||||||
|
SyncAction::CreateList,
|
||||||
|
&items[i].task_id,
|
||||||
|
&items[i].list_id,
|
||||||
|
&items[i].payload,
|
||||||
|
items[i].retries + 1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
all_ok = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Second pass: everything else
|
||||||
for item in &items {
|
for item in &items {
|
||||||
|
if item.action == SyncAction::CreateList {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
let result = match item.action {
|
let result = match item.action {
|
||||||
SyncAction::Create => {
|
SyncAction::Create => {
|
||||||
let task = serde_json::from_str::<Task>(&item.payload).unwrap_or_else(|_| Task {
|
let task = serde_json::from_str::<Task>(&item.payload).unwrap_or_else(|_| Task {
|
||||||
@@ -271,7 +314,12 @@ async fn push_sync(
|
|||||||
created_at: None,
|
created_at: None,
|
||||||
updated_at: None,
|
updated_at: None,
|
||||||
});
|
});
|
||||||
api.create_task(&item.list_id, &task).await
|
api.create_task(&item.list_id, &task).await.map(|server_task| {
|
||||||
|
if server_task.id != item.task_id {
|
||||||
|
let _ = db.update_task_id(&item.task_id, &server_task.id);
|
||||||
|
let _ = db.update_sync_task_id(&item.task_id, &server_task.id);
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
SyncAction::Update => {
|
SyncAction::Update => {
|
||||||
let task = serde_json::from_str::<Task>(&item.payload).unwrap_or_else(|_| Task {
|
let task = serde_json::from_str::<Task>(&item.payload).unwrap_or_else(|_| Task {
|
||||||
@@ -293,6 +341,7 @@ async fn push_sync(
|
|||||||
SyncAction::Reorder => {
|
SyncAction::Reorder => {
|
||||||
api.move_task(&item.list_id, &item.task_id, None, None).await
|
api.move_task(&item.list_id, &item.task_id, None, None).await
|
||||||
}
|
}
|
||||||
|
_ => Ok(()),
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Err(err) = result {
|
if let Err(err) = result {
|
||||||
|
|||||||
Reference in New Issue
Block a user