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
16 changes: 16 additions & 0 deletions app/lib/linear_cli/cli.ex
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ defmodule LinearCli.CLI do
# too, so this port doesn't need to invent one.
@subcommand_aliases %{
"issue" => %{
"a" => "assign",
"c" => "create",
"new" => "create",
"add" => "create",
Expand Down Expand Up @@ -199,6 +200,7 @@ defmodule LinearCli.CLI do
do: run(&Commands.profile_clear/1, result, halt)

defp dispatch([:issue, :list], result, halt), do: run(&Commands.issue_list/1, result, halt)
defp dispatch([:issue, :assign], result, halt), do: run(&Commands.issue_assign/1, result, halt)
defp dispatch([:issue, :create], result, halt), do: run(&Commands.issue_create/1, result, halt)

defp dispatch([:issue, :develop], result, halt),
Expand Down Expand Up @@ -564,6 +566,20 @@ defmodule LinearCli.CLI do
]
]
],
assign: [
name: "assign",
about: "Assign an issue to a team member",
args: [
issue_id: [value_name: "ISSUE_ID", help: "The Issue (i.e. CRY-1)", required: true]
],
options: [
assignee: [
short: "-a",
long: "--assignee",
help: "Team member name to assign to (prompts if omitted)"
]
]
],
create: [
name: "create",
about:
Expand Down
64 changes: 64 additions & 0 deletions app/lib/linear_cli/cli/commands.ex
Original file line number Diff line number Diff line change
Expand Up @@ -543,4 +543,68 @@ defmodule LinearCli.CLI.Commands do
{:error, reason} -> {:error, reason}
end
end

@doc """
Assigns an issue to a team member.

With `--assignee`/`-a`, matches the given name against the issue's team's
members (case-insensitive exact, then unique prefix). Without it, prompts
interactively via `LinearCli.CLI.Prompt.select/2`.
"""
@spec issue_assign(Optimus.ParseResult.t()) :: :ok | {:error, term()}
def issue_assign(%{args: %{issue_id: issue_id}, options: options}) do
expanded_id = IssueHelpers.expand_issue_id(issue_id)

with {:ok, [issue]} <- Linear.issues(%{ids: [expanded_id]}),
{:ok, members} <- Linear.team_members(issue.team.id),
:ok <- guard_has_members(members, issue),
{:ok, target_member} <- resolve_target_member(members, options.assignee),
{:ok, updated} <- Linear.assign_issue(issue, target_member.id) do
Display.show(updated, %{output: options.output})

if options.output != "json",
do: Prompt.ok("#{updated.identifier} assigned to #{target_member.name}")

:ok
end
end

defp guard_has_members([], issue) do
{:error,
{:smells_bad, "No assignable members found for team #{issue.team.key || issue.team.id}"}}
end

defp guard_has_members(_members, _issue), do: :ok

defp resolve_target_member(members, nil) do
choices = Enum.sort_by(members, & &1.name) |> Enum.map(&{&1.name, &1})
{:ok, Prompt.select("Choose an assignee", choices)}
end

defp resolve_target_member(members, name) do
normalized = String.downcase(name)

members
|> Enum.filter(&(String.downcase(&1.name) == normalized))
|> use_prefix_member_matches_if_empty(members, normalized)
|> resolve_member_matches(members, name)
end

defp use_prefix_member_matches_if_empty([], members, name) do
Enum.filter(members, &String.starts_with?(String.downcase(&1.name), name))
end

defp use_prefix_member_matches_if_empty(matches, _members, _name), do: matches

defp resolve_member_matches([member], _members, _name), do: {:ok, member}

defp resolve_member_matches([], members, name) do
available = Enum.map_join(Enum.sort_by(members, & &1.name), ", ", & &1.name)
{:error, {:smells_bad, "Unknown assignee #{inspect(name)}. Available: #{available}"}}
end

defp resolve_member_matches(matches, _members, name) do
ambiguous = Enum.map_join(matches, ", ", & &1.name)
{:error, {:smells_bad, "Ambiguous assignee #{inspect(name)}: matches #{ambiguous}"}}
end
end
1 change: 1 addition & 0 deletions app/lib/linear_cli/linear.ex
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ defmodule LinearCli.Linear do
resources do
resource LinearCli.Linear.User do
define :me, action: :me, get?: true
define :team_members, action: :by_team, args: [:team_id]
end

resource LinearCli.Linear.Team do
Expand Down
28 changes: 28 additions & 0 deletions app/lib/linear_cli/linear/user.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ defmodule LinearCli.Linear.User do
get? true
manual LinearCli.Linear.User.Read.Me
end

read :by_team do
argument :team_id, :string, allow_nil?: false
manual LinearCli.Linear.User.Read.ByTeam
end
end

attributes do
Expand Down Expand Up @@ -55,3 +60,26 @@ defmodule LinearCli.Linear.User.Read.Me do
end
end
end

defmodule LinearCli.Linear.User.Read.ByTeam do
@moduledoc false
use Ash.Resource.ManualRead

alias LinearCli.Api
alias LinearCli.Linear.User

def read(query, _ecto_query, _opts, _context) do
team_id = query.arguments.team_id

document =
"query($id: String!) { team(id: $id) { members(first: 50) { nodes { #{User.base_fields()} } } } }"

with {:ok, %{"team" => team}} when is_map(team) <- Api.call(document, %{"id" => team_id}),
%{"members" => %{"nodes" => nodes}} <- team do
{:ok, Enum.map(nodes, &User.from_map/1)}
else
{:ok, _} -> {:ok, []}
error -> error
end
end
end
Loading