fix: preserve Ticket lane order across pages
This commit is contained in:
@@ -1426,6 +1426,7 @@ pub struct SqliteTicketListProjection {
|
|||||||
|
|
||||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
pub struct SqliteTicketListCursor {
|
pub struct SqliteTicketListCursor {
|
||||||
|
pub state_rank: i64,
|
||||||
pub updated_at: Option<String>,
|
pub updated_at: Option<String>,
|
||||||
pub ticket_id: String,
|
pub ticket_id: String,
|
||||||
}
|
}
|
||||||
@@ -2412,6 +2413,7 @@ impl SqliteTicketBackend {
|
|||||||
.collect::<Vec<_>>(),
|
.collect::<Vec<_>>(),
|
||||||
)
|
)
|
||||||
.map_err(|error| TicketError::Sqlite(error.to_string()))?;
|
.map_err(|error| TicketError::Sqlite(error.to_string()))?;
|
||||||
|
let cursor_rank = query.after.as_ref().map(|cursor| cursor.state_rank);
|
||||||
let cursor_updated_at = query
|
let cursor_updated_at = query
|
||||||
.after
|
.after
|
||||||
.as_ref()
|
.as_ref()
|
||||||
@@ -2431,11 +2433,24 @@ impl SqliteTicketBackend {
|
|||||||
SELECT 1 FROM json_each(?2) AS state
|
SELECT 1 FROM json_each(?2) AS state
|
||||||
WHERE state.value = ticket.workflow_state
|
WHERE state.value = ticket.workflow_state
|
||||||
))
|
))
|
||||||
AND (?3 IS NULL OR COALESCE(ticket.updated_at, '') < COALESCE(?4, '')
|
AND (?3 IS NULL OR
|
||||||
OR (COALESCE(ticket.updated_at, '') = COALESCE(?4, '')
|
CASE ticket.workflow_state
|
||||||
AND ticket.ticket_id > ?3))
|
WHEN 'ready' THEN 0 WHEN 'planning' THEN 1
|
||||||
ORDER BY ticket.updated_at DESC, ticket.ticket_id ASC
|
WHEN 'inprogress' THEN 2 WHEN 'queued' THEN 3
|
||||||
LIMIT ?5",
|
WHEN 'done' THEN 4 WHEN 'closed' THEN 5 ELSE 6 END > ?4
|
||||||
|
OR (CASE ticket.workflow_state
|
||||||
|
WHEN 'ready' THEN 0 WHEN 'planning' THEN 1
|
||||||
|
WHEN 'inprogress' THEN 2 WHEN 'queued' THEN 3
|
||||||
|
WHEN 'done' THEN 4 WHEN 'closed' THEN 5 ELSE 6 END = ?4
|
||||||
|
AND (COALESCE(ticket.updated_at, '') < COALESCE(?5, '')
|
||||||
|
OR (COALESCE(ticket.updated_at, '') = COALESCE(?5, '')
|
||||||
|
AND ticket.ticket_id > ?3))))
|
||||||
|
ORDER BY CASE ticket.workflow_state
|
||||||
|
WHEN 'ready' THEN 0 WHEN 'planning' THEN 1
|
||||||
|
WHEN 'inprogress' THEN 2 WHEN 'queued' THEN 3
|
||||||
|
WHEN 'done' THEN 4 WHEN 'closed' THEN 5 ELSE 6 END ASC,
|
||||||
|
ticket.updated_at DESC, ticket.ticket_id ASC
|
||||||
|
LIMIT ?6",
|
||||||
)
|
)
|
||||||
.map_err(sqlite_err)?;
|
.map_err(sqlite_err)?;
|
||||||
let rows = statement
|
let rows = statement
|
||||||
@@ -2444,6 +2459,7 @@ impl SqliteTicketBackend {
|
|||||||
self.workspace_id,
|
self.workspace_id,
|
||||||
states,
|
states,
|
||||||
cursor_id,
|
cursor_id,
|
||||||
|
cursor_rank,
|
||||||
cursor_updated_at,
|
cursor_updated_at,
|
||||||
i64::try_from(fetch_limit).unwrap_or(i64::MAX)
|
i64::try_from(fetch_limit).unwrap_or(i64::MAX)
|
||||||
],
|
],
|
||||||
@@ -2458,6 +2474,14 @@ impl SqliteTicketBackend {
|
|||||||
let next = has_more.then(|| {
|
let next = has_more.then(|| {
|
||||||
let summary = summaries.last().expect("non-empty page with continuation");
|
let summary = summaries.last().expect("non-empty page with continuation");
|
||||||
SqliteTicketListCursor {
|
SqliteTicketListCursor {
|
||||||
|
state_rank: match summary.workflow_state {
|
||||||
|
TicketWorkflowState::Ready => 0,
|
||||||
|
TicketWorkflowState::Planning => 1,
|
||||||
|
TicketWorkflowState::InProgress => 2,
|
||||||
|
TicketWorkflowState::Queued => 3,
|
||||||
|
TicketWorkflowState::Done => 4,
|
||||||
|
TicketWorkflowState::Closed => 5,
|
||||||
|
},
|
||||||
updated_at: summary.updated_at.clone(),
|
updated_at: summary.updated_at.clone(),
|
||||||
ticket_id: summary.id.clone(),
|
ticket_id: summary.id.clone(),
|
||||||
}
|
}
|
||||||
@@ -6896,6 +6920,26 @@ state: planning
|
|||||||
ids.push(ticket.id);
|
ids.push(ticket.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Connection::open(&db_path)
|
||||||
|
.unwrap()
|
||||||
|
.execute(
|
||||||
|
"UPDATE typed_tickets SET updated_at='2026-08-12T05:00:00Z' WHERE workspace_id=?1 AND ticket_id=?2",
|
||||||
|
params!["workspace-test", ids[2]],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let combined_lane = backend
|
||||||
|
.list_workspace_projection_page(SqliteTicketListPageQuery {
|
||||||
|
states: vec![TicketWorkflowState::Ready, TicketWorkflowState::Planning],
|
||||||
|
limit: 1,
|
||||||
|
after: None,
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
combined_lane.items[0].summary.workflow_state,
|
||||||
|
TicketWorkflowState::Ready,
|
||||||
|
"lane pagination order must match the UI's state-primary order",
|
||||||
|
);
|
||||||
|
|
||||||
let first = backend
|
let first = backend
|
||||||
.list_workspace_projection_page(SqliteTicketListPageQuery {
|
.list_workspace_projection_page(SqliteTicketListPageQuery {
|
||||||
states: vec![TicketWorkflowState::Ready],
|
states: vec![TicketWorkflowState::Ready],
|
||||||
|
|||||||
@@ -782,7 +782,10 @@ impl TicketAuthority for SqliteWorkspaceAuthority {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
.collect::<Result<Vec<_>>>()?;
|
.collect::<Result<Vec<_>>>()?;
|
||||||
let fingerprint = format!("ticket-summary:v1:states={}", states.join(","));
|
let fingerprint = format!(
|
||||||
|
"ticket-summary:v2:sort=priority:states={}",
|
||||||
|
states.join(",")
|
||||||
|
);
|
||||||
let after = request
|
let after = request
|
||||||
.cursor
|
.cursor
|
||||||
.as_deref()
|
.as_deref()
|
||||||
@@ -809,7 +812,7 @@ impl TicketAuthority for SqliteWorkspaceAuthority {
|
|||||||
returned: items.len(),
|
returned: items.len(),
|
||||||
has_more: page.has_more,
|
has_more: page.has_more,
|
||||||
next_cursor,
|
next_cursor,
|
||||||
sort: "updated_desc".to_string(),
|
sort: "priority".to_string(),
|
||||||
source_limit: None,
|
source_limit: None,
|
||||||
source_truncated: false,
|
source_truncated: false,
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user