Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions crates/lineark/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ lineark projects create <NAME> --team KEY Create a project
[--description TEXT] [--lead NAME-OR-ID|me] Description, lead, dates
[--members NAME,...|me] Project members (comma-separated)
[--start-date DATE] [--target-date DATE] Priority, content, icon, color
[-p 0-4] [--content TEXT] ... See --help for all options
[-p PRIORITY] [--content TEXT] ... See --help for all options
lineark labels list [--team KEY] List labels (group, team, parent)
lineark labels create <NAME> Create a label
[--team KEY] [--color HEX] Team, color
Expand All @@ -96,14 +96,14 @@ lineark issues search <QUERY> [-l N] Full-text search
[--team KEY] [--assignee NAME-OR-ID|me] Filter by team, assignee, status
[--status NAME,...] [--show-done]
lineark issues create <TITLE> --team KEY Create an issue
[-p 0-4] [-e N] [--assignee NAME-OR-ID|me] Priority, estimate, assignee
[-p PRIORITY] [-e N] [--assignee NAME-OR-ID|me] Priority (0-4 or name), estimate
[--labels NAME,...] [-s NAME] ... Labels, status — see --help
lineark issues update <IDENTIFIER> Update an issue
[-s NAME] [-p 0-4] [-e N] Status, priority, estimate
[-s NAME] [-p PRIORITY] [-e N] Status, priority, estimate
[--assignee NAME-OR-ID|me] Assignee
[--clear-parent] [--project NAME-OR-ID] ... See --help for all options
lineark issues batch-update ID [ID ...] Batch update multiple issues
[-s NAME] [-p 0-4] [--assignee NAME-OR-ID|me] Status, priority, assignee
[-s NAME] [-p PRIORITY] [--assignee ...] Status, priority, assignee
lineark issues archive <IDENTIFIER> Archive an issue
lineark issues unarchive <IDENTIFIER> Unarchive an issue
lineark issues delete <IDENTIFIER> Delete (trash) an issue
Expand Down
37 changes: 37 additions & 0 deletions crates/lineark/src/commands/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -409,3 +409,40 @@ pub async fn resolve_cycle_id(
available.join(", ")
))
}

/// Parse a priority value from either a number (0-4) or a name.
///
/// Mapping defined by the Linear GraphQL schema on `IssueCreateInput.priority` and
/// `IssueUpdateInput.priority` (see `schema/schema.graphql`):
///
/// | Value | Schema label | `priorityLabel` from API |
/// |-------|-------------|--------------------------|
/// | 0 | No priority | No priority |
/// | 1 | Urgent | Urgent |
/// | 2 | High | High |
/// | 3 | Normal | Medium |
/// | 4 | Low | Low |
///
/// Note: the schema says "Normal" but the API's `priorityLabel` field returns "Medium"
/// for priority 3. We accept both `"medium"` and `"normal"` as input.
pub fn parse_priority(s: &str) -> Result<i64, String> {
let s = s.trim();
if let Ok(n) = s.parse::<i64>() {
if (0..=4).contains(&n) {
return Ok(n);
}
return Err(format!(
"invalid priority '{n}': valid values are 0-4 or none, urgent, high, medium, low"
));
}
match s.to_ascii_lowercase().as_str() {
"none" => Ok(0),
"urgent" => Ok(1),
"high" => Ok(2),
"medium" | "normal" => Ok(3),
"low" => Ok(4),
_ => Err(format!(
"invalid priority '{s}': valid values are 0-4 or none, urgent, high, medium, low"
)),
}
}
70 changes: 54 additions & 16 deletions crates/lineark/src/commands/issues.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ use serde::{Deserialize, Serialize};
use tabled::Tabled;

use super::helpers::{
resolve_cycle_id, resolve_issue_id, resolve_label_ids, resolve_project_id, resolve_team_id,
resolve_user_id_or_me,
parse_priority, resolve_cycle_id, resolve_issue_id, resolve_label_ids, resolve_project_id,
resolve_team_id, resolve_user_id_or_me,
};
use crate::output::{self, Format};

Expand Down Expand Up @@ -72,8 +72,8 @@ pub enum IssuesAction {
///
/// Examples:
/// lineark issues create "Fix the bug" --team ENG
/// lineark issues create "Add feature" --team ENG --priority 2 --description "Details here"
/// lineark issues create "Urgent fix" --team ENG --priority 1 --labels Bug,Frontend
/// lineark issues create "Add feature" --team ENG --priority high --description "Details here"
/// lineark issues create "Urgent fix" --team ENG -p urgent --labels Bug,Frontend
/// lineark issues create "My task" --team ENG --assignee me
Create {
/// Issue title.
Expand All @@ -87,8 +87,8 @@ pub enum IssuesAction {
/// Comma-separated label names.
#[arg(long, value_delimiter = ',')]
labels: Option<Vec<String>>,
/// Priority: 0=none, 1=urgent, 2=high, 3=medium, 4=low.
#[arg(short = 'p', long, value_parser = clap::value_parser!(i64).range(0..=4))]
/// Priority: 0-4 or none, urgent, high, medium, low.
#[arg(short = 'p', long, value_parser = parse_priority)]
priority: Option<i64>,
/// Estimate points (valid values depend on the team's estimation scale).
#[arg(short = 'e', long)]
Expand Down Expand Up @@ -145,7 +145,7 @@ pub enum IssuesAction {
/// Batch update multiple issues at once. Returns the updated issues.
///
/// Examples:
/// lineark issues batch-update ENG-1 ENG-2 --priority 2
/// lineark issues batch-update ENG-1 ENG-2 --priority high
/// lineark issues batch-update ENG-1 ENG-2 ENG-3 --status "In Progress"
/// lineark issues batch-update ENG-1 ENG-2 --assignee me --labels Bug --label-by adding
BatchUpdate {
Expand All @@ -155,8 +155,8 @@ pub enum IssuesAction {
/// New status name (resolved against the first issue's team workflow states).
#[arg(short = 's', long)]
status: Option<String>,
/// Priority: 0=none, 1=urgent, 2=high, 3=medium, 4=low.
#[arg(short = 'p', long, value_parser = clap::value_parser!(i64).range(0..=4))]
/// Priority: 0-4 or none, urgent, high, medium, low.
#[arg(short = 'p', long, value_parser = parse_priority)]
priority: Option<i64>,
/// Comma-separated label names. Behavior depends on --label-by.
#[arg(long, value_delimiter = ',')]
Expand All @@ -181,16 +181,16 @@ pub enum IssuesAction {
///
/// Examples:
/// lineark issues update ENG-123 --status "In Progress"
/// lineark issues update ENG-123 --priority 1 --assignee "John Doe"
/// lineark issues update ENG-123 --priority urgent --assignee "John Doe"
/// lineark issues update ENG-123 --labels Bug,Frontend --label-by adding
Update {
/// Issue identifier (e.g., ENG-123) or UUID.
identifier: String,
/// New status name (resolved against the issue's team workflow states).
#[arg(short = 's', long)]
status: Option<String>,
/// Priority: 0=none, 1=urgent, 2=high, 3=medium, 4=low.
#[arg(short = 'p', long, value_parser = clap::value_parser!(i64).range(0..=4))]
/// Priority: 0-4 or none, urgent, high, medium, low.
#[arg(short = 'p', long, value_parser = parse_priority)]
priority: Option<i64>,
/// Estimate points (valid values depend on the team's estimation scale).
#[arg(short = 'e', long)]
Expand Down Expand Up @@ -245,7 +245,7 @@ pub enum LabelMode {
struct IssueRow {
identifier: String,
title: String,
#[serde(rename = "priorityLabel")]
#[serde(rename = "priority")]
#[tabled(rename = "priority")]
priority_label: String,
#[tabled(rename = "status")]
Expand Down Expand Up @@ -337,7 +337,7 @@ pub struct IssueSummary {
pub id: Option<String>,
pub identifier: Option<String>,
pub title: Option<String>,
pub priority: Option<f64>,
#[serde(rename(serialize = "priority"))]
pub priority_label: Option<String>,
pub estimate: Option<f64>,
pub url: Option<String>,
Expand All @@ -359,7 +359,7 @@ pub struct SearchSummary {
pub id: Option<String>,
pub identifier: Option<String>,
pub title: Option<String>,
pub priority: Option<f64>,
#[serde(rename(serialize = "priority"))]
pub priority_label: Option<String>,
pub estimate: Option<f64>,
pub url: Option<String>,
Expand All @@ -385,7 +385,7 @@ pub struct IssueDetail {
pub identifier: Option<String>,
pub title: Option<String>,
pub description: Option<String>,
pub priority: Option<f64>,
#[serde(rename(serialize = "priority"))]
pub priority_label: Option<String>,
pub estimate: Option<f64>,
pub url: Option<String>,
Expand Down Expand Up @@ -1129,6 +1129,44 @@ async fn resolve_state_id(
mod tests {
use super::*;

#[test]
fn parse_priority_numeric_values() {
assert_eq!(parse_priority("0").unwrap(), 0);
assert_eq!(parse_priority("1").unwrap(), 1);
assert_eq!(parse_priority("4").unwrap(), 4);
}

#[test]
fn parse_priority_textual_values() {
assert_eq!(parse_priority("none").unwrap(), 0);
assert_eq!(parse_priority("urgent").unwrap(), 1);
assert_eq!(parse_priority("high").unwrap(), 2);
assert_eq!(parse_priority("medium").unwrap(), 3);
assert_eq!(parse_priority("normal").unwrap(), 3);
assert_eq!(parse_priority("low").unwrap(), 4);
}

#[test]
fn parse_priority_case_insensitive() {
assert_eq!(parse_priority("URGENT").unwrap(), 1);
assert_eq!(parse_priority("High").unwrap(), 2);
assert_eq!(parse_priority("LOW").unwrap(), 4);
}

#[test]
fn parse_priority_trims_whitespace() {
assert_eq!(parse_priority(" urgent ").unwrap(), 1);
assert_eq!(parse_priority(" 2 ").unwrap(), 2);
assert!(parse_priority(" ").is_err());
}

#[test]
fn parse_priority_rejects_invalid() {
assert!(parse_priority("bogus").is_err());
assert!(parse_priority("5").is_err());
assert!(parse_priority("-1").is_err());
}

#[test]
fn format_estimate_none_returns_empty() {
assert_eq!(format_estimate(None), "");
Expand Down
7 changes: 4 additions & 3 deletions crates/lineark/src/commands/projects.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ use serde::{Deserialize, Serialize};
use tabled::Tabled;

use super::helpers::{
resolve_project_id, resolve_team_ids, resolve_user_id_or_me, resolve_user_ids_or_me,
parse_priority, resolve_project_id, resolve_team_ids, resolve_user_id_or_me,
resolve_user_ids_or_me,
};
use crate::output::{self, Format};

Expand Down Expand Up @@ -68,8 +69,8 @@ pub enum ProjectsAction {
/// Planned target/completion date (YYYY-MM-DD).
#[arg(long)]
target_date: Option<String>,
/// Priority: 0=none, 1=urgent, 2=high, 3=medium, 4=low.
#[arg(short = 'p', long, value_parser = clap::value_parser!(i64).range(0..=4))]
/// Priority: 0-4 or none, urgent, high, medium, low.
#[arg(short = 'p', long, value_parser = parse_priority)]
priority: Option<i64>,
/// Markdown content for the project.
#[arg(long)]
Expand Down
4 changes: 2 additions & 2 deletions crates/lineark/src/commands/usage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,12 +87,12 @@ COMMANDS:
[--team KEY] [--assignee NAME-OR-ID|me] Filter by team, assignee, status
[--status NAME,...] [--show-done] Comma-separated status names
lineark issues create <TITLE> --team KEY Create an issue
[-p 0-4] [-e N] [--assignee NAME-OR-ID|me] 0=none 1=urgent 2=high 3=medium 4=low
[-p PRIORITY] [-e N] [--assignee NAME-OR-ID|me] 0-4 or none/urgent/high/medium/low
[--labels NAME,...] [-d TEXT] [-s NAME] Label names, status name
[--parent ID] [--project NAME-OR-ID] Parent issue, project, cycle
[--cycle NAME-OR-ID]
lineark issues update <IDENTIFIER> Update an issue
[-s NAME] [-p 0-4] [-e N] Status, priority, estimate
[-s NAME] [-p PRIORITY] [-e N] Status, priority, estimate
[--assignee NAME-OR-ID|me] Assignee
[--labels NAME,...] [--label-by adding|replacing|removing]
[--clear-labels] [-t TEXT] [-d TEXT] Title, description
Expand Down
26 changes: 26 additions & 0 deletions crates/lineark/tests/offline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,32 @@ fn issues_create_help_shows_flags() {
.stdout(predicate::str::contains("--labels"));
}

#[test]
fn issues_create_rejects_invalid_priority_name() {
lineark()
.args([
"issues",
"create",
"test",
"--team",
"FAKE",
"--priority",
"bogus",
])
.assert()
.failure()
.stderr(predicate::str::contains("invalid priority"));
}

#[test]
fn issues_create_accepts_textual_priority_in_help() {
lineark()
.args(["issues", "create", "--help"])
.assert()
.success()
.stdout(predicate::str::contains("none, urgent, high, medium, low"));
}

#[test]
fn issues_update_help_shows_flags() {
lineark()
Expand Down
Loading