Rail and drawer: only the 5 tags most recently used or viewed (#5931)
CI & Build / Build now, or wait for Android? (push) Successful in 2s
Android / Build, or is the channel already serving this? (push) Successful in 2s
CI & Build / Python lint (push) Successful in 3s
CI & Build / Web typecheck and unit tests (push) Successful in 12s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 2s
CI & Build / Python tests (push) Successful in 14s
Android / Core and FFI clippy and tests (push) Successful in 1m9s
CI & Build / integration (push) Successful in 2m10s
CI & Build / Build & push image (push) Skipped
Desktop (Tauri) / Web tests, clippy, Rust tests and rustfmt (push) Successful in 3m25s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 3m20s
Android / Kotlin + Rust (APK) (push) Failing after 6m3s
Android / Build the server image (push) Skipped
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 4m5s
Desktop (Tauri) / Update manifest (push) Successful in 4s
CI & Build / Build now, or wait for Android? (push) Successful in 2s
Android / Build, or is the channel already serving this? (push) Successful in 2s
CI & Build / Python lint (push) Successful in 3s
CI & Build / Web typecheck and unit tests (push) Successful in 12s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 2s
CI & Build / Python tests (push) Successful in 14s
Android / Core and FFI clippy and tests (push) Successful in 1m9s
CI & Build / integration (push) Successful in 2m10s
CI & Build / Build & push image (push) Skipped
Desktop (Tauri) / Web tests, clippy, Rust tests and rustfmt (push) Successful in 3m25s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 3m20s
Android / Kotlin + Rust (APK) (push) Failing after 6m3s
Android / Build the server image (push) Skipped
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 4m5s
Desktop (Tauri) / Update manifest (push) Successful in 4s
A long tag list pushed the drawer's Archive, Trash and Sync off screen on Android, where Tags still sat above them (#5915 only reordered the web rail). Tags now come last on Android too, and both clients list the five tags most recently used or viewed, by name, with "All tags (N)" opening the rest in place. Used: the newest untrashed note carrying the tag. GET /api/labels and the core's list_labels return it as last_used, and each client also folds in the notes it holds, so a tag applied a moment ago counts before the next listing. Viewed: opening the tag's lens, remembered on the device (per user on the web). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -118,6 +118,9 @@ pub struct Label {
|
||||
// preserves the count it already holds (matches the REST single-label responses).
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub count: Option<i64>,
|
||||
/// The newest `updated_at` among the untrashed notes carrying it, or None when
|
||||
/// nothing does. The drawer keeps the few most recent tags in reach (#5931).
|
||||
pub last_used: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
|
||||
@@ -992,6 +992,8 @@ pub fn restore_revision(conn: &Connection, id: &str, rev_id: &str) -> rusqlite::
|
||||
/// A label with how many live notes carry it; `label_row` reads it.
|
||||
const LABEL_SELECT: &str = "SELECT l.id, l.name, l.color,
|
||||
(SELECT COUNT(*) FROM note_labels nl JOIN notes n ON n.id = nl.note_id
|
||||
WHERE nl.label_id = l.id AND n.trashed = 0),
|
||||
(SELECT MAX(n.updated_at) FROM note_labels nl JOIN notes n ON n.id = nl.note_id
|
||||
WHERE nl.label_id = l.id AND n.trashed = 0)
|
||||
FROM labels l";
|
||||
|
||||
@@ -1001,6 +1003,7 @@ fn label_row(r: &rusqlite::Row<'_>) -> rusqlite::Result<Label> {
|
||||
name: r.get(1)?,
|
||||
color: r.get(2)?,
|
||||
count: Some(r.get(3)?),
|
||||
last_used: r.get(4)?,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1799,6 +1802,38 @@ mod tests {
|
||||
assert_eq!(list_labels(&conn).expect("labels")[0].count, Some(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_label_is_as_recent_as_its_newest_untrashed_note() {
|
||||
let conn = db();
|
||||
let used = create_label(&conn, "errands").expect("label");
|
||||
create_label(&conn, "someday").expect("label");
|
||||
let old = create_label(&conn, "old").expect("label");
|
||||
let kept = note(&conn, "milk");
|
||||
let binned = note(&conn, "stale");
|
||||
set_labels(&conn, &kept.id, std::slice::from_ref(&used.id)).expect("labels");
|
||||
set_labels(&conn, &binned.id, std::slice::from_ref(&old.id)).expect("labels");
|
||||
trash(&conn, &binned.id).expect("trash");
|
||||
|
||||
let kept = get_note(&conn, &kept.id).expect("get");
|
||||
let by_name = |name: &str| {
|
||||
list_labels(&conn)
|
||||
.expect("labels")
|
||||
.into_iter()
|
||||
.find(|l| l.name == name)
|
||||
.expect("listed")
|
||||
};
|
||||
assert_eq!(
|
||||
by_name("errands").last_used.as_deref(),
|
||||
kept.updated_at.as_deref()
|
||||
);
|
||||
assert_eq!(by_name("someday").last_used, None, "carried by nothing");
|
||||
assert_eq!(
|
||||
by_name("old").last_used,
|
||||
None,
|
||||
"a trashed note does not count"
|
||||
);
|
||||
}
|
||||
|
||||
fn tombstones(conn: &Connection) -> Vec<(String, String)> {
|
||||
let mut stmt = conn
|
||||
.prepare("SELECT entity, id FROM pending_deletes ORDER BY entity, id")
|
||||
|
||||
Reference in New Issue
Block a user