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: 8 additions & 0 deletions app/lib/linear_cli/cli.ex
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,8 @@ defmodule LinearCli.CLI do

defp parse_statuses(value), do: {:ok, split_filter_values(value)}

defp parse_labels(value), do: {:ok, split_filter_values(value)}

defp split_filter_values(value) do
value
|> String.split(",", trim: true)
Expand Down Expand Up @@ -683,6 +685,12 @@ defmodule LinearCli.CLI do
long: "--status",
help: "Filter by friendly workflow status name(s) (comma-separated)",
parser: &parse_statuses/1
],
labels: [
short: "-l",
long: "--labels",
help: "Filter by label name(s) (comma-separated, OR match)",
parser: &parse_labels/1
]
]
],
Expand Down
3 changes: 2 additions & 1 deletion app/lib/linear_cli/cli/commands.ex
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,8 @@ defmodule LinearCli.CLI.Commands do
project_id: project_id,
all: Map.get(flags, :all, false),
state: Map.get(options, :state) || [],
status: Map.get(options, :status) || []
status: Map.get(options, :status) || [],
labels: Map.get(options, :labels) || []
}

with {:ok, issues} <- Linear.issues(input) do
Expand Down
13 changes: 13 additions & 0 deletions app/lib/linear_cli/linear/issue.ex
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ defmodule LinearCli.Linear.Issue do
argument :all, :boolean, default: false
argument :state, {:array, :string}, default: []
argument :status, {:array, :string}, default: []
argument :labels, {:array, :string}, default: []
manual LinearCli.Linear.Issue.Read.List
end

Expand Down Expand Up @@ -201,6 +202,7 @@ defmodule LinearCli.Linear.Issue.Read.List do
|> maybe_put_team_filter(args)
|> maybe_put_project_filter(args)
|> maybe_put_state_filter(args)
|> maybe_put_label_filter(args)
end

@completed_types ~w(completed)
Expand Down Expand Up @@ -281,6 +283,17 @@ defmodule LinearCli.Linear.Issue.Read.List do
names = Enum.map(statuses, &%{"name" => %{"eqIgnoreCase" => &1}})
Map.put(filter, "or", names)
end

defp maybe_put_label_filter(filter, %{labels: []}), do: filter

defp maybe_put_label_filter(filter, %{labels: [label]}) do
Map.put(filter, "labels", %{"some" => %{"name" => %{"eqIgnoreCase" => label}}})
end

defp maybe_put_label_filter(filter, %{labels: labels}) do
or_clauses = Enum.map(labels, &%{"name" => %{"eqIgnoreCase" => &1}})
Map.put(filter, "labels", %{"some" => %{"or" => or_clauses}})
end
end

defmodule LinearCli.Linear.Issue.Create do
Expand Down
96 changes: 96 additions & 0 deletions app/test/linear_cli/cli/issue_commands_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,102 @@ defmodule LinearCli.CLI.IssueCommandsTest do
assert_received {:halted, 1}
end

test "--labels filters by a single label name (case-insensitive)" do
test_pid = self()

Req.Test.stub(LinearCli.Api, fn conn ->
{:ok, body, conn} = Plug.Conn.read_body(conn)
decoded = Jason.decode!(body)
send(test_pid, {:filter, decoded["variables"]["filter"]})
Req.Test.json(conn, issues_response([issue_map()]))
end)

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

assert_received {:filter, filter}

assert filter["labels"] == %{
"some" => %{"name" => %{"eqIgnoreCase" => "Incident-followup"}}
}
end

test "--labels accepts comma-separated names and matches issues with any of them (OR)" do
test_pid = self()

Req.Test.stub(LinearCli.Api, fn conn ->
{:ok, body, conn} = Plug.Conn.read_body(conn)
decoded = Jason.decode!(body)
send(test_pid, {:filter, decoded["variables"]["filter"]})
Req.Test.json(conn, issues_response([issue_map()]))
end)

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

assert_received {:filter, filter}

assert filter["labels"] == %{
"some" => %{
"or" => [
%{"name" => %{"eqIgnoreCase" => "Bug"}},
%{"name" => %{"eqIgnoreCase" => "Feature"}}
]
}
}
end

test "--labels with an unknown label name returns empty result, not a crash" do
Req.Test.stub(LinearCli.Api, fn conn ->
Req.Test.json(conn, issues_response([]))
end)

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

assert output == "" or is_binary(output)
end

test "--labels composes with --team" do
test_pid = self()

Req.Test.stub(LinearCli.Api, fn conn ->
{:ok, body, conn} = Plug.Conn.read_body(conn)
decoded = Jason.decode!(body)

if String.contains?(decoded["query"] || "", "teams(") do
Req.Test.json(conn, %{
"data" => %{
"teams" => %{
"edges" => [
%{
"node" => %{"id" => "t1", "key" => "ENG", "name" => "Engineering"},
"cursor" => "c1"
}
],
"pageInfo" => %{"hasNextPage" => false, "endCursor" => "c1"}
}
}
})
else
send(test_pid, {:filter, decoded["variables"]["filter"]})
Req.Test.json(conn, issues_response([issue_map()]))
end
end)

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

assert_received {:filter, filter}
assert Map.has_key?(filter, "team")
assert filter["labels"] == %{"some" => %{"name" => %{"eqIgnoreCase" => "Bug"}}}
end

test "compact listing includes workflow state name in brackets" do
Req.Test.stub(LinearCli.Api, fn conn ->
{:ok, body, conn} = Plug.Conn.read_body(conn)
Expand Down
92 changes: 92 additions & 0 deletions app/test/linear_cli/linear/issue_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,42 @@ defmodule LinearCli.Linear.IssueTest do
assert {:ok, []} = Linear.issues(%{unassigned: true})
end

test "issues/1 with labels sends the correct label filter to the API" do
Req.Test.stub(LinearCli.Api, fn conn ->
{:ok, body, conn} = Plug.Conn.read_body(conn)
%{"variables" => %{"filter" => filter}} = Jason.decode!(body)
assert filter["labels"] == %{"some" => %{"name" => %{"eqIgnoreCase" => "Bug"}}}

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

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

test "issues/1 with multiple labels sends OR filter to the API" do
Req.Test.stub(LinearCli.Api, fn conn ->
{:ok, body, conn} = Plug.Conn.read_body(conn)
%{"variables" => %{"filter" => filter}} = Jason.decode!(body)

assert filter["labels"] == %{
"some" => %{
"or" => [
%{"name" => %{"eqIgnoreCase" => "Bug"}},
%{"name" => %{"eqIgnoreCase" => "Feature"}}
]
}
}

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

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

test "issues/1 with ids fetches each by id via the full-detail query" do
Req.Test.stub(LinearCli.Api, fn conn ->
{:ok, body, conn} = Plug.Conn.read_body(conn)
Expand Down Expand Up @@ -468,4 +504,60 @@ defmodule LinearCli.Linear.IssueTest do
Linear.update_issue_description(issue, "Updated body")
end
end

describe "issues/1 label filtering" do
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)
%{"variables" => %{"filter" => filter}} = Jason.decode!(body)

assert filter["labels"] == %{
"some" => %{"name" => %{"eqIgnoreCase" => "Bug"}}
}

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

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

test "issues/1 with labels: [multiple] sends some/or filter for OR semantics" do
Req.Test.stub(LinearCli.Api, fn conn ->
{:ok, body, conn} = Plug.Conn.read_body(conn)
%{"variables" => %{"filter" => filter}} = Jason.decode!(body)

assert filter["labels"] == %{
"some" => %{
"or" => [
%{"name" => %{"eqIgnoreCase" => "Bug"}},
%{"name" => %{"eqIgnoreCase" => "Feature"}}
]
}
}

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

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

test "issues/1 with labels: [] sends no labels filter key" do
Req.Test.stub(LinearCli.Api, fn conn ->
{:ok, body, conn} = Plug.Conn.read_body(conn)
%{"variables" => %{"filter" => filter}} = Jason.decode!(body)

refute Map.has_key?(filter, "labels")

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

assert {:ok, []} = Linear.issues(%{labels: [], mine: false})
end
end
end
5 changes: 3 additions & 2 deletions documents/ash-domain-erd.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -313,8 +313,9 @@ manual-implementation module, and the Linear GraphQL operation it calls.
| read
| `Linear.Issue.Read.List`
| `issues(filter:, first:, after:)` paginated (base), with `state` filtering
workflow-state types and `status` filtering friendly workflow-state names; or
`issue(id:)` per id (full, fanned concurrently)
workflow-state types, `status` filtering friendly workflow-state names, and
`labels` filtering by label name (case-insensitive OR match via
`IssueLabelCollectionFilter.some`); or `issue(id:)` per id (full, fanned concurrently)

| `Issue`
| `create_issue`
Expand Down
Loading