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
7 changes: 6 additions & 1 deletion app/lib/linear_cli/cli/commands.ex
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,12 @@ defmodule LinearCli.CLI.Commands do
}

with {:ok, issues} <- Linear.issues(input) do
Display.show(issues, %{output: options.output, full: flags.full})
Display.show(issues, %{
output: options.output,
full: flags.full,
labels: input.labels != []
})

:ok
end
end
Expand Down
14 changes: 10 additions & 4 deletions app/lib/linear_cli/cli/display.ex
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@ defmodule LinearCli.CLI.Display do
issue_full(issue)
end

defp format(%Issue{} = issue, _opts) do
issue_line(issue)
defp format(%Issue{} = issue, opts) do
issue_line(issue, opts)
end

defp user_line(user, opts) do
Expand All @@ -80,10 +80,16 @@ defmodule LinearCli.CLI.Display do
end
end

defp issue_line(issue) do
defp issue_line(issue, opts \\ %{}) do
state = if issue.state, do: "[#{issue.state.name}] ", else: ""
basic = "#{String.pad_trailing(issue.identifier || "", 12)} #{state}#{issue.title}"
if issue.assignee, do: "#{basic} (#{issue.assignee.name})", else: basic
line = if issue.assignee, do: "#{basic} (#{issue.assignee.name})", else: basic

if Map.get(opts, :labels) && issue.labels != [] do
"#{line} [#{Enum.map_join(issue.labels, ", ", & &1.name)}]"
else
line
end
end

defp issue_full(issue) do
Expand Down
26 changes: 23 additions & 3 deletions app/lib/linear_cli/linear/issue.ex
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,11 @@ defmodule LinearCli.Linear.Issue do
"team { #{LinearCli.Linear.Team.base_fields()} }"
end

@doc "GraphQL field selection for base_fields plus labels — used when listing issues filtered by label."
def list_fields_with_labels do
"#{base_fields()} labels { nodes { #{LinearCli.Linear.Label.base_fields()} } }"
end

@doc "GraphQL field selection for a fully detailed issue, incl. comments (Ruby: Issue.full_fragment)."
def full_fields do
"#{@issue_fields} " <>
Expand Down Expand Up @@ -137,7 +142,7 @@ defmodule LinearCli.Linear.Issue.Read.List do
if args.ids != [] do
find_by_ids(args.ids)
else
list_all(build_filter(args))
list_all(build_filter(args), args.labels != [])
end
end

Expand All @@ -161,6 +166,19 @@ defmodule LinearCli.Linear.Issue.Read.List do
"""
end

# Same paginated query but includes labels in each node — used when the caller
# filtered by --labels so we can show labels in the compact output.
defp list_document_with_labels do
"""
query($filter: IssueFilter, $first: Int!, $after: String) {
issues(filter: $filter, first: $first, after: $after) {
edges { node { #{Issue.list_fields_with_labels()} } cursor }
pageInfo { hasNextPage endCursor }
}
}
"""
end

# Ruby: params[:ids].map { |id| Issue.find(id) } - fully serial. Each id is
# an independent GraphQL call, so fan them out concurrently instead. Capped
# at 20 in flight to avoid hammering the API on a very long id list; ordered
Expand All @@ -187,9 +205,11 @@ defmodule LinearCli.Linear.Issue.Read.List do
end
end

defp list_all(filter) do
defp list_all(filter, include_labels) do
document = if include_labels, do: list_document_with_labels(), else: list_document()

Paginate.all(
list_document(),
document,
"issues",
fn after_cursor -> %{"filter" => filter, "first" => 50, "after" => after_cursor} end,
&Issue.from_map/1
Expand Down
53 changes: 53 additions & 0 deletions app/test/linear_cli/cli/display_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -88,4 +88,57 @@ defmodule LinearCli.CLI.DisplayTest do

refute output =~ "Labels:"
end

test "compact listing appends labels in brackets when labels opt is true" do
issue = %Issue{
id: "issue-4",
identifier: "EXT-4",
title: "Labelled compact",
description: nil,
comments: [],
labels: [
%Label{id: "l1", name: "Bug", description: nil, is_group: false},
%Label{id: "l2", name: "Feature", description: nil, is_group: false}
]
}

output = capture_io(fn -> Display.show(issue, %{labels: true}) end)

assert output =~ "EXT-4"
assert output =~ "[Bug, Feature]"
end

test "compact listing omits label brackets when labels opt is false" do
issue = %Issue{
id: "issue-5",
identifier: "EXT-5",
title: "Unlabelled compact",
description: nil,
comments: [],
labels: [
%Label{id: "l1", name: "Bug", description: nil, is_group: false}
]
}

output = capture_io(fn -> Display.show(issue, %{labels: false}) end)

assert output =~ "EXT-5"
refute output =~ "[Bug]"
end

test "compact listing omits label brackets when issue has no labels even if labels opt is true" do
issue = %Issue{
id: "issue-6",
identifier: "EXT-6",
title: "No labels",
description: nil,
comments: [],
labels: []
}

output = capture_io(fn -> Display.show(issue, %{labels: true}) end)

assert output =~ "EXT-6"
refute output =~ "["
end
end
90 changes: 90 additions & 0 deletions app/test/linear_cli/cli/issue_commands_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -606,6 +606,96 @@ defmodule LinearCli.CLI.IssueCommandsTest do
assert output =~ "[Done]"
assert output =~ "Fix the thing"
end

test "-l shows label names in compact output" do
labeled_issue =
issue_map(%{
"labels" => %{
"nodes" => [
%{"id" => "l1", "name" => "Bug", "description" => nil, "isGroup" => false}
]
}
})

Req.Test.stub(LinearCli.Api, fn conn ->
Req.Test.json(conn, issues_response([labeled_issue]))
end)

output =
capture_io(fn ->
assert :ok = LinearCli.CLI.main(["issue", "list", "-l", "Bug"])
end)

assert output =~ "CRY-1"
assert output =~ "[Bug]"
end

test "--labels shows label names in compact output" do
labeled_issue =
issue_map(%{
"labels" => %{
"nodes" => [
%{"id" => "l1", "name" => "Bug", "description" => nil, "isGroup" => false}
]
}
})

Req.Test.stub(LinearCli.Api, fn conn ->
Req.Test.json(conn, issues_response([labeled_issue]))
end)

output =
capture_io(fn ->
assert :ok = LinearCli.CLI.main(["issue", "list", "--labels", "Bug"])
end)

assert output =~ "CRY-1"
assert output =~ "[Bug]"
end

test "--labels with multiple labels shows all label names in compact output" do
labeled_issue =
issue_map(%{
"labels" => %{
"nodes" => [
%{"id" => "l1", "name" => "Bug", "description" => nil, "isGroup" => false},
%{"id" => "l2", "name" => "Feature", "description" => nil, "isGroup" => false}
]
}
})

Req.Test.stub(LinearCli.Api, fn conn ->
Req.Test.json(conn, issues_response([labeled_issue]))
end)

output =
capture_io(fn ->
assert :ok = LinearCli.CLI.main(["issue", "list", "--labels", "Bug,Feature"])
end)

assert output =~ "CRY-1"
assert output =~ "[Bug, Feature]"
end

test "compact listing without --labels does not show label brackets" do
labeled_issue =
issue_map(%{
"labels" => %{
"nodes" => [
%{"id" => "l1", "name" => "Bug", "description" => nil, "isGroup" => false}
]
}
})

Req.Test.stub(LinearCli.Api, fn conn ->
Req.Test.json(conn, issues_response([labeled_issue]))
end)

output = capture_io(fn -> assert :ok = LinearCli.CLI.main(["issue", "list"]) end)

assert output =~ "CRY-1"
refute output =~ "[Bug]"
end
end

describe "issue view" do
Expand Down
66 changes: 66 additions & 0 deletions app/test/linear_cli/linear/issue_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,72 @@ defmodule LinearCli.Linear.IssueTest do
end

describe "issues/1 label filtering" do
test "issues/1 with labels requests labels fields in the GraphQL query" do
Req.Test.stub(LinearCli.Api, fn conn ->
{:ok, body, conn} = Plug.Conn.read_body(conn)
%{"query" => query} = Jason.decode!(body)

assert String.contains?(query, "labels {")

Req.Test.json(conn, %{
"data" => %{"issues" => %{"edges" => [], "pageInfo" => %{"hasNextPage" => false}}}
})
end)

assert {:ok, []} = Linear.issues(%{labels: ["Bug"]})
end

test "issues/1 without labels does not request labels fields in the GraphQL query" do
Req.Test.stub(LinearCli.Api, fn conn ->
{:ok, body, conn} = Plug.Conn.read_body(conn)
%{"query" => query} = Jason.decode!(body)

refute String.contains?(query, "labels {")

Req.Test.json(conn, %{
"data" => %{"issues" => %{"edges" => [], "pageInfo" => %{"hasNextPage" => false}}}
})
end)

assert {:ok, []} = Linear.issues(%{labels: [], mine: false})
end

test "issues/1 with labels parses label names from the response" do
Req.Test.stub(LinearCli.Api, fn conn ->
Req.Test.json(conn, %{
"data" => %{
"issues" => %{
"edges" => [
%{
"node" => %{
"id" => "i1",
"identifier" => "CRY-1",
"title" => "Bug fix",
"branchName" => "cry-1-bug-fix",
"description" => nil,
"assignee" => nil,
"state" => nil,
"team" => %{"id" => "t1", "key" => "ENG", "name" => "Engineering"},
"labels" => %{
"nodes" => [
%{"id" => "l1", "name" => "Bug", "description" => nil, "isGroup" => false}
]
}
},
"cursor" => "c1"
}
],
"pageInfo" => %{"hasNextPage" => false}
}
}
})
end)

assert {:ok, [issue]} = Linear.issues(%{labels: ["Bug"]})
assert [label] = issue.labels
assert label.name == "Bug"
end

test "issues/1 with labels: [single] sends some/name/eqIgnoreCase filter" do
Req.Test.stub(LinearCli.Api, fn conn ->
{:ok, body, conn} = Plug.Conn.read_body(conn)
Expand Down