Display labels in CLI and web table views

Amolith created

Change summary

src/cmd/list.rs                |  5 ++++-
src/cmd/next.rs                | 15 +++++++++++++--
src/cmd/webui/project/mod.rs   | 12 ++++++++++++
src/cmd/webui/project/views.rs |  2 ++
src/cmd/webui/task/mod.rs      |  1 +
templates/macros.html          |  2 ++
templates/project.html         |  2 ++
templates/task.html            |  2 ++
8 files changed, 38 insertions(+), 3 deletions(-)

Detailed changes

src/cmd/list.rs 🔗

@@ -58,7 +58,9 @@ pub fn run(root: &Path, opts: Opts) -> Result<()> {
         let use_color = stdout_use_color();
         let mut table = Table::new();
         table.load_preset(NOTHING);
-        table.set_header(vec!["ID", "STATUS", "TYPE", "PRIORITY", "EFFORT", "TITLE"]);
+        table.set_header(vec![
+            "ID", "STATUS", "TYPE", "PRIORITY", "EFFORT", "LABELS", "TITLE",
+        ]);
         for t in &tasks {
             table.add_row(vec![
                 cell_bold(&t.id, use_color),
@@ -66,6 +68,7 @@ pub fn run(root: &Path, opts: Opts) -> Result<()> {
                 Cell::new(&t.task_type),
                 cell_priority(t.priority.as_str(), t.priority, use_color),
                 cell_effort(t.effort.as_str(), t.effort, use_color),
+                Cell::new(t.labels.join(", ")),
                 Cell::new(&t.title),
             ]);
         }

src/cmd/next.rs 🔗

@@ -1,7 +1,7 @@
 use anyhow::{bail, Result};
 use comfy_table::presets::NOTHING;
 use comfy_table::{Cell, Table};
-use std::collections::HashSet;
+use std::collections::{HashMap, HashSet};
 use std::path::Path;
 
 use crate::color::{cell_bold, stdout_use_color};
@@ -60,6 +60,12 @@ pub fn run(root: &Path, mode_str: &str, verbose: bool, limit: usize, json: bool)
         limit,
     );
 
+    // Build a lookup from task ID to labels for display.
+    let labels_by_id: HashMap<&str, &[String]> = all
+        .iter()
+        .map(|t| (t.id.as_str(), t.labels.as_slice()))
+        .collect();
+
     if scored.is_empty() {
         if json {
             println!("[]");
@@ -94,14 +100,19 @@ pub fn run(root: &Path, mode_str: &str, verbose: bool, limit: usize, json: bool)
         let use_color = stdout_use_color();
         let mut table = Table::new();
         table.load_preset(NOTHING);
-        table.set_header(vec!["#", "ID", "SCORE", "TITLE"]);
+        table.set_header(vec!["#", "ID", "SCORE", "LABELS", "TITLE"]);
 
         for (i, s) in scored.iter().enumerate() {
             let short = TaskId::display_id(&s.id);
+            let labels = labels_by_id
+                .get(s.id.as_str())
+                .map(|ls| ls.join(", "))
+                .unwrap_or_default();
             table.add_row(vec![
                 Cell::new(i + 1),
                 cell_bold(&short, use_color),
                 Cell::new(format!("{:.2}", s.score)),
+                Cell::new(labels),
                 Cell::new(&s.title),
             ]);
         }

src/cmd/webui/project/mod.rs 🔗

@@ -284,6 +284,7 @@ fn build_section(
                 priority: t.priority.as_str().to_string(),
                 effort: t.effort.as_str().to_string(),
                 title: t.title.clone(),
+                labels: t.labels.clone(),
                 created_at_display: friendly_date(&t.created_at),
                 created_at: t.created_at.clone(),
             }
@@ -393,6 +394,12 @@ pub(in crate::cmd::webui) async fn project_handler(
             5,
         );
 
+        // Build a lookup from task ID to labels for the Next Up display.
+        let labels_by_id: std::collections::HashMap<&str, &[String]> = tasks
+            .iter()
+            .map(|t| (t.id.as_str(), t.labels.as_slice()))
+            .collect();
+
         let next_up: Vec<ScoredEntry> = scored
             .into_iter()
             .map(|s| {
@@ -415,6 +422,10 @@ pub(in crate::cmd::webui) async fn project_handler(
                     "Unblocks: {} {} ({} directly)",
                     s.total_unblocked, task_word, s.direct_unblocked
                 );
+                let labels = labels_by_id
+                    .get(s.id.as_str())
+                    .map(|ls| ls.to_vec())
+                    .unwrap_or_default();
                 ScoredEntry {
                     short_id: TaskId::display_id(&s.id),
                     id: s.id,
@@ -424,6 +435,7 @@ pub(in crate::cmd::webui) async fn project_handler(
                     status_display: friendly_status("open"),
                     equation,
                     unblocks_display,
+                    labels,
                 }
             })
             .collect();

src/cmd/webui/project/views.rs 🔗

@@ -14,6 +14,7 @@ pub(in crate::cmd::webui) struct ScoredEntry {
     pub(in crate::cmd::webui) equation: String,
     /// Human-friendly unblocks summary for the score tooltip.
     pub(in crate::cmd::webui) unblocks_display: String,
+    pub(in crate::cmd::webui) labels: Vec<String>,
 }
 
 /// Minimal view-model for a task row in the project task table.
@@ -26,6 +27,7 @@ pub(in crate::cmd::webui) struct TaskRow {
     pub(in crate::cmd::webui) priority: String,
     pub(in crate::cmd::webui) effort: String,
     pub(in crate::cmd::webui) title: String,
+    pub(in crate::cmd::webui) labels: Vec<String>,
     pub(in crate::cmd::webui) created_at: String,
     pub(in crate::cmd::webui) created_at_display: String,
 }

src/cmd/webui/task/mod.rs 🔗

@@ -62,6 +62,7 @@ pub(in crate::cmd::webui) async fn task_handler(
                     priority: t.priority.as_str().to_string(),
                     effort: t.effort.as_str().to_string(),
                     title: t.title.clone(),
+                    labels: t.labels.clone(),
                     created_at_display: friendly_date(&t.created_at),
                     created_at: t.created_at.clone(),
                 }

templates/macros.html 🔗

@@ -68,6 +68,7 @@
           <th scope="col"><a href="{{ section.sort_ctx.column_href("priority") }}">Priority{{ section.sort_ctx.arrow("priority") }}</a></th>
           <th scope="col"><a href="{{ section.sort_ctx.column_href("effort") }}">Effort{{ section.sort_ctx.arrow("effort") }}</a></th>
           <th scope="col"><a href="{{ section.sort_ctx.column_href("title") }}">Title{{ section.sort_ctx.arrow("title") }}</a></th>
+          <th scope="col">Labels</th>
           <th scope="col"><a href="{{ section.sort_ctx.column_href("created") }}">Created{{ section.sort_ctx.arrow("created") }}</a></th>
           <th scope="col">Change status</th>
         </tr>
@@ -80,6 +81,7 @@
           <td>{{ t.priority }}</td>
           <td>{{ t.effort }}</td>
           <td>{{ t.title }}</td>
+          <td>{% for l in t.labels %}{% if !loop.first %}, {% endif %}{{ l }}{% endfor %}</td>
           <td><time datetime="{{ t.created_at }}">{{ t.created_at_display }}</time></td>
           <td>
             <ot-dropdown>

templates/project.html 🔗

@@ -74,6 +74,7 @@
           <th scope="col">ID</th>
           <th scope="col">Score</th>
           <th scope="col">Title</th>
+          <th scope="col">Labels</th>
           <th scope="col">Change status</th>
         </tr>
       </thead>
@@ -84,6 +85,7 @@
           <td><a href="/projects/{{ project_name }}/tasks/{{ s.id }}"><code>{{ s.short_id }}</code></a></td>
           <td title="{{ s.equation }}&#10;{{ s.unblocks_display }}">{{ s.score }}</td>
           <td>{{ s.title }}</td>
+          <td>{% for l in s.labels %}{% if !loop.first %}, {% endif %}{{ l }}{% endfor %}</td>
           <td>
             <ot-dropdown>
               <button popovertarget="next-status-{{ s.short_id }}" class="outline small" aria-label="Change status of {{ s.short_id }}, currently {{ s.status_display }}">

templates/task.html 🔗

@@ -170,6 +170,7 @@
           <th scope="col">Priority</th>
           <th scope="col">Effort</th>
           <th scope="col">Title</th>
+          <th scope="col">Labels</th>
           <th scope="col">Created</th>
         </tr>
       </thead>
@@ -182,6 +183,7 @@
           <td>{{ t.priority }}</td>
           <td>{{ t.effort }}</td>
           <td>{{ t.title }}</td>
+          <td>{% for l in t.labels %}{% if !loop.first %}, {% endif %}{{ l }}{% endfor %}</td>
           <td><time datetime="{{ t.created_at }}">{{ t.created_at_display }}</time></td>
         </tr>
         {% endfor %}