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: 7 additions & 0 deletions app/lib/linear_cli/cli.ex
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,9 @@ defmodule LinearCli.CLI do
help: "Project name, URL, ID, or search term",
required: true
]
],
options: [
team: [short: "-t", long: "--team", help: "Scope project search to this team"]
]
],
unfavorite: [
Expand All @@ -467,6 +470,9 @@ defmodule LinearCli.CLI do
help: "Project name, URL, ID, or search term",
required: true
]
],
options: [
team: [short: "-t", long: "--team", help: "Scope project search to this team"]
]
],
update: [
Expand All @@ -480,6 +486,7 @@ defmodule LinearCli.CLI do
]
],
options: [
team: [short: "-t", long: "--team", help: "Scope project search to this team"],
body: [short: "-b", long: "--body", help: "The update's content (markdown)"],
health: [
long: "--health",
Expand Down
64 changes: 41 additions & 23 deletions app/lib/linear_cli/cli/commands.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ defmodule LinearCli.CLI.Commands do
result. Ported from vendor/ruby-linear-cli/lib/linear/commands/**.
"""

alias LinearCli.CLI.{Display, IssueHelpers, Projects, Prompt}
alias LinearCli.CLI.{Display, IssueHelpers, Projects, Prompt, WhatFor}
alias LinearCli.{Favorites, Git, Linear, Profiles}

@doc "Ported from commands/whoami.rb."
Expand Down Expand Up @@ -89,13 +89,15 @@ defmodule LinearCli.CLI.Commands do

@doc """
New in this port - Ruby has no equivalent. Favorites a project
(`LinearCli.Favorites`), resolved the same way `project update`'s
`PROJECT` is - against every project in the workspace, prompting if
ambiguous. Once any project is favorited, `project list` defaults to
showing just favorites (`--all` overrides).
(`LinearCli.Favorites`), resolved against the active team's projects,
prompting if ambiguous. Team is resolved via `--team`, the active
profile, or an interactive prompt. Once any project is favorited,
`project list` defaults to showing just favorites (`--all` overrides).
"""
def project_favorite(%{args: %{project: search}}) do
with {:ok, projects} <- Linear.projects(),
def project_favorite(%{args: %{project: search}, options: options}) do
team = WhatFor.team_for(options.team || Profiles.default_team())

with {:ok, projects} <- Linear.projects_by_team(team.id),
project when not is_nil(project) <- Projects.project_for(projects, search) do
Favorites.add("project", project.id)
Prompt.ok("Favorited project #{project.name}")
Expand All @@ -107,8 +109,10 @@ defmodule LinearCli.CLI.Commands do
end

@doc "New in this port - Ruby has no equivalent. Un-favorites a project."
def project_unfavorite(%{args: %{project: search}}) do
with {:ok, projects} <- Linear.projects(),
def project_unfavorite(%{args: %{project: search}, options: options}) do
team = WhatFor.team_for(options.team || Profiles.default_team())

with {:ok, projects} <- Linear.projects_by_team(team.id),
project when not is_nil(project) <- Projects.project_for(projects, search) do
Favorites.remove("project", project.id)
Prompt.ok("Un-favorited project #{project.name}")
Expand Down Expand Up @@ -137,11 +141,14 @@ defmodule LinearCli.CLI.Commands do
New in this port - Ruby has no equivalent. Posts a status update
(Linear's own "Project Update" feature - a journal-style status post,
not an edit to the project's own fields) via the projectUpdateCreate
mutation. `PROJECT` is resolved the same way issue list's `--project`
is - against every project in the workspace, prompting if ambiguous.
mutation. `PROJECT` is resolved against the active team's projects,
prompting if ambiguous. Team is resolved via `--team`, the active
profile, or an interactive prompt.
"""
def project_update(%{args: %{project: search}, options: options}) do
with {:ok, projects} <- Linear.projects(),
team = WhatFor.team_for(options.team || Profiles.default_team())

with {:ok, projects} <- Linear.projects_by_team(team.id),
project when not is_nil(project) <- Projects.project_for(projects, search),
{:ok, update} <-
Linear.post_project_update(project.id, options.body, %{health: options.health}) do
Expand Down Expand Up @@ -223,14 +230,15 @@ defmodule LinearCli.CLI.Commands do
@doc """
Ported from commands/issue/list.rb + operations/issue/list.rb.

`--project`/`-p` is resolved the same way Ruby's `CLI::Projects#project_for`
does - against every project in the workspace (`Project.all`, not
team-scoped), prompting interactively when the search is ambiguous or
omitted-but-requested (`-p -`). Only resolved at all when `--project` was
actually given (or `LinearCli.Profiles.default_project/0` supplies one) -
unlike `issue create`/`issue update`, a bare `issue list` with no active
profile applies no project filter and never prompts. `--team`/`--project`
passed explicitly always win over the active profile.
`--project`/`-p` resolution is team-scoped when `--team` is given (or
the active profile supplies a team) - it searches that team's projects via
`projects_by_team`. Without a team context it falls back to all workspace
projects (`Project.all`). Prompts interactively when the search is
ambiguous or omitted-but-requested (`-p -`). Only resolved at all when
`--project` was actually given (or `LinearCli.Profiles.default_project/0`
supplies one) - unlike `issue create`/`issue update`, a bare `issue list`
with no active profile applies no project filter and never prompts.
`--team`/`--project` passed explicitly always win over the active profile.
"""
def issue_list(%{flags: flags, options: options, unknown: ids}) do
no_profile = Map.get(flags, :no_profile, false)
Expand All @@ -239,7 +247,7 @@ defmodule LinearCli.CLI.Commands do
project_source =
options.project || unless no_profile, do: Profiles.default_project()

with {:ok, project_id} <- resolve_project_id(project_source) do
with {:ok, project_id} <- resolve_project_id(project_source, team_key) do
input = %{
ids: Enum.map(ids, &IssueHelpers.expand_issue_id/1),
mine: !flags.no_mine,
Expand All @@ -257,9 +265,19 @@ defmodule LinearCli.CLI.Commands do
end
end

defp resolve_project_id(nil), do: {:ok, nil}
defp resolve_project_id(nil, _team_key), do: {:ok, nil}

defp resolve_project_id(search, team_key) when is_binary(team_key) do
with {:ok, team} <- Linear.find_team(team_key),
{:ok, projects} <- Linear.projects_by_team(team.id) do
case Projects.project_for(projects, search) do
nil -> {:ok, nil}
project -> {:ok, project.id}
end
end
end

defp resolve_project_id(search) do
defp resolve_project_id(search, _team_key) do
with {:ok, projects} <- Linear.projects() do
case Projects.project_for(projects, search) do
nil -> {:ok, nil}
Expand Down
24 changes: 24 additions & 0 deletions app/test/linear_cli/cli/favorites_commands_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,30 @@ defmodule LinearCli.CLI.FavoritesCommandsTest do
}
})

query =~ "team(id: $teamId)" ->
Req.Test.json(conn, %{
"data" => %{
"team" => %{
"projects" => %{
"nodes" => [
%{
"id" => "p1",
"name" => "Manhattan",
"slugId" => "abc",
"url" => "https://linear.app/x/project/manhattan-abc"
},
%{
"id" => "p2",
"name" => "Platform Cleanup",
"slugId" => "def",
"url" => "https://linear.app/x/project/platform-cleanup-def"
}
]
}
}
}
})

query =~ "projects(" ->
Req.Test.json(conn, %{
"data" => %{
Expand Down
44 changes: 44 additions & 0 deletions app/test/linear_cli/cli/issue_commands_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,50 @@ defmodule LinearCli.CLI.IssueCommandsTest do
assert_received {:filter, %{"project" => %{"id" => %{"eq" => "p1"}}}}
end

test "--project with --team resolves against team-scoped projects only" do
test_pid = self()

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

cond do
String.contains?(query, "projects(first: $first") ->
raise "--project with --team must not query all-workspace projects"

String.contains?(query, "team(id: $id)") ->
Req.Test.json(conn, %{"data" => %{"team" => team_map()}})

String.contains?(query, "projects(first: 100)") ->
Req.Test.json(conn, team_projects([project_map("p1", "Manhattan Rollout")]))

String.contains?(query, "issues(filter") ->
send(test_pid, {:filter, decoded["variables"]["filter"]})
Req.Test.json(conn, issues_response([issue_map()]))

true ->
raise "no stub matched query: #{query}"
end
end)

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

assert output =~ "CRY-1"
assert_received {:filter, %{"project" => %{"id" => %{"eq" => "p1"}}}}
end

test "bare issue list applies no project filter and never queries projects at all" do
Req.Test.stub(LinearCli.Api, fn conn ->
{:ok, body, conn} = Plug.Conn.read_body(conn)
Expand Down
14 changes: 10 additions & 4 deletions app/test/linear_cli/cli/profile_defaults_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -153,8 +153,11 @@ defmodule LinearCli.CLI.ProfileDefaultsTest do
query = decoded["query"]

cond do
String.contains?(query, "projects(first: $first") ->
Req.Test.json(conn, all_projects([project_map("p1", "Manhattan Rollout")]))
String.contains?(query, "team(id: $id)") ->
Req.Test.json(conn, %{"data" => %{"team" => team_map("CRY")}})

String.contains?(query, "projects(first: 100)") ->
Req.Test.json(conn, team_projects([project_map("p1", "Manhattan Rollout")]))

String.contains?(query, "issues(filter") ->
send(test_pid, {:filter, decoded["variables"]["filter"]})
Expand Down Expand Up @@ -191,8 +194,11 @@ defmodule LinearCli.CLI.ProfileDefaultsTest do
query = decoded["query"]

cond do
String.contains?(query, "projects(first: $first") ->
Req.Test.json(conn, all_projects([project_map("p2", "Platform Cleanup")]))
String.contains?(query, "team(id: $id)") ->
Req.Test.json(conn, %{"data" => %{"team" => team_map("ENG")}})

String.contains?(query, "projects(first: 100)") ->
Req.Test.json(conn, team_projects([project_map("p2", "Platform Cleanup")]))

String.contains?(query, "issues(filter") ->
send(test_pid, {:filter, decoded["variables"]["filter"]})
Expand Down
32 changes: 22 additions & 10 deletions app/test/linear_cli/cli_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -115,22 +115,34 @@ defmodule LinearCli.CLITest do
query = decoded["query"]

cond do
String.contains?(query, "projects(first: $first") ->
String.contains?(query, "viewer") ->
Req.Test.json(conn, %{
"data" => %{
"projects" => %{
"edges" => [
%{
"node" => %{
"viewer" => %{
"id" => "u1",
"name" => "Ada",
"email" => "ada@example.com",
"teams" => %{
"nodes" => [%{"id" => "t1", "key" => "ENG", "name" => "Engineering"}]
}
}
}
})

String.contains?(query, "team(id: $teamId)") ->
Req.Test.json(conn, %{
"data" => %{
"team" => %{
"projects" => %{
"nodes" => [
%{
"id" => "p1",
"name" => "Manhattan",
"slugId" => "abc",
"url" => "https://linear.app/x/project/manhattan-abc"
},
"cursor" => "p1"
}
],
"pageInfo" => %{"hasNextPage" => false}
}
]
}
}
}
})
Expand Down