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: 7 additions & 1 deletion app/lib/linear_cli/cli/display.ex
Original file line number Diff line number Diff line change
Expand Up @@ -89,12 +89,18 @@ defmodule LinearCli.CLI.Display do
defp issue_full(issue) do
header = issue_line(issue)
sep = String.duplicate("-", String.length(header))
labels = labels_line(issue.labels)
description = render_markdown(issue.description)
comments = Enum.map_join(issue.comments, "\n", &comment_block/1)

Enum.join([header, sep, description, comments], "\n")
[header, sep, labels, description, comments]
|> Enum.reject(&(&1 == ""))
|> Enum.join("\n")
end

defp labels_line([]), do: ""
defp labels_line(labels), do: "Labels: #{Enum.map_join(labels, ", ", & &1.name)}"

defp comment_block(comment) do
user = (comment.user && comment.user.name) || "unknown"
"--- #{user} ---\n#{render_markdown(comment.body)}"
Expand Down
9 changes: 8 additions & 1 deletion app/lib/linear_cli/linear/issue.ex
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ defmodule LinearCli.Linear.Issue do
attribute :state, :term, public?: true
attribute :team, :term, public?: true
attribute :comments, {:array, :term}, public?: true, default: []
attribute :labels, {:array, :term}, public?: true, default: []
end

@issue_fields "id identifier title branchName description url createdAt updatedAt"
Expand All @@ -93,7 +94,8 @@ defmodule LinearCli.Linear.Issue do
"state { #{@state_fields} } " <>
"assignee { #{LinearCli.Linear.User.fields_with_teams()} } " <>
"team { #{LinearCli.Linear.Team.full_fields()} } " <>
"comments { nodes { #{LinearCli.Linear.Comment.base_fields()} } }"
"comments { nodes { #{LinearCli.Linear.Comment.base_fields()} } } " <>
"labels { nodes { #{LinearCli.Linear.Label.base_fields()} } }"
end

@doc false
Expand All @@ -112,6 +114,11 @@ defmodule LinearCli.Linear.Issue do
Enum.map(
get_in(map, ["comments", "nodes"]) || [],
&LinearCli.Linear.Comment.from_map/1
),
labels:
Enum.map(
get_in(map, ["labels", "nodes"]) || [],
&LinearCli.Linear.Label.from_map/1
)
)
end
Expand Down
35 changes: 34 additions & 1 deletion app/test/linear_cli/cli/display_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ defmodule LinearCli.CLI.DisplayTest do
import ExUnit.CaptureIO

alias LinearCli.CLI.Display
alias LinearCli.Linear.Issue
alias LinearCli.Linear.{Issue, Label}

test "full issue output syntax-highlights fenced Elixir code" do
issue = %Issue{
Expand Down Expand Up @@ -55,4 +55,37 @@ defmodule LinearCli.CLI.DisplayTest do
assert output =~ theme.syntax.name_function <> "hello" <> theme.reset
refute output =~ theme.code_text <> "class Greeter"
end

test "full issue output includes a Labels line when labels are present" do
issue = %Issue{
id: "issue-2",
identifier: "EXT-2",
title: "Labelled issue",
description: "Some work",
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, %{full: true}) end)

assert output =~ "Labels: Bug, Feature"
end

test "full issue output omits the Labels line when no labels are present" do
issue = %Issue{
id: "issue-3",
identifier: "EXT-3",
title: "Unlabelled issue",
description: "Some work",
comments: [],
labels: []
}

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

refute output =~ "Labels:"
end
end
47 changes: 46 additions & 1 deletion app/test/linear_cli/linear/issue_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ defmodule LinearCli.Linear.IssueTest do
assert id == "CRY-2"
assert query =~ "issue(id: $id)"
assert query =~ "comments"
assert query =~ "labels"

Req.Test.json(conn, %{
"data" => %{
Expand All @@ -117,14 +118,58 @@ defmodule LinearCli.Linear.IssueTest do
"description" => nil,
"assignee" => nil,
"team" => %{"id" => "t1", "key" => "ENG", "name" => "Engineering"},
"comments" => %{"nodes" => []}
"comments" => %{"nodes" => []},
"labels" => %{"nodes" => []}
}
}
})
end)

assert {:ok, [issue]} = Linear.issues(%{ids: ["cry-2"]})
assert issue.identifier == "CRY-2"
assert issue.labels == []
end

test "issues/1 with ids parses labels from the full-detail response" do
Req.Test.stub(LinearCli.Api, fn conn ->
{:ok, body, conn} = Plug.Conn.read_body(conn)
%{"variables" => %{"id" => _id}} = Jason.decode!(body)

Req.Test.json(conn, %{
"data" => %{
"issue" => %{
"id" => "i2",
"identifier" => "CRY-2",
"title" => "Ship it",
"branchName" => "cry-2-ship-it",
"description" => nil,
"assignee" => nil,
"team" => %{"id" => "t1", "key" => "ENG", "name" => "Engineering"},
"comments" => %{"nodes" => []},
"labels" => %{
"nodes" => [
%{
"id" => "lbl-1",
"name" => "Bug",
"description" => nil,
"isGroup" => false
},
%{
"id" => "lbl-2",
"name" => "Feature",
"description" => "A new feature",
"isGroup" => false
}
]
}
}
}
})
end)

assert {:ok, [issue]} = Linear.issues(%{ids: ["cry-2"]})
assert length(issue.labels) == 2
assert Enum.map(issue.labels, & &1.name) == ["Bug", "Feature"]
end

test "issues/1 parses the issue's current state when present in the response" do
Expand Down
24 changes: 18 additions & 6 deletions documents/ash-domain-erd.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ erDiagram
WorkflowState state
Team team
Comment[] comments
Label[] labels
}
Label {
string id PK
Expand Down Expand Up @@ -103,6 +104,7 @@ erDiagram
Issue }o--|| WorkflowState : "state [nested]"
Issue }o--|| Team : "team [nested]"
Issue ||--o{ Comment : "comments [nested]"
Issue ||--o{ Label : "labels [nested]"
User }o--o{ Team : "teams [nested]"
Comment }o--|| User : "user/author [nested]"
Project }o--o{ Team : "teams [nested]"
Expand Down Expand Up @@ -140,7 +142,7 @@ Eight resources are registered in `LinearCli.Linear`

| `LinearCli.Linear.Issue`
| `id` (`:string`)
| `identifier`, `title`, `branch_name`, `description`, `assignee` (`:term`), `state` (`:term`), `team` (`:term`), `comments` (`{:array, :term}`)
| `identifier`, `title`, `branch_name`, `description`, `assignee` (`:term`), `state` (`:term`), `team` (`:term`), `comments` (`{:array, :term}`), `labels` (`{:array, :term}`)

| `LinearCli.Linear.Label`
| `id` (`:string`)
Expand Down Expand Up @@ -196,6 +198,12 @@ data, not from declared Ash relationships.
| `comments` (`{:array, :term}`)
| GraphQL/nested-data — populated by `Issue.from_map/1` from `issue.comments.nodes` (full fragment only)

| `Issue`
| `Label`
| one-to-many
| `labels` (`{:array, :term}`)
| GraphQL/nested-data — populated by `Issue.from_map/1` from `issue.labels.nodes` (full fragment only)

| `User`
| `Team`
| many-to-many
Expand Down Expand Up @@ -224,9 +232,13 @@ data, not from declared Ash relationships.

=== Notes on unidirectional associations

* `Label` and `WorkflowState` have no stored association attributes.
Their `team_id` appears only as a query argument passed to their read
actions; there is no `:team` field on those structs.
* `Label` is stored as an association attribute on `Issue` (`labels`, `{:array, :term}`) —
populated from `issue.labels.nodes` in `full_fields` responses only.
However, `Label` itself has no stored back-reference to its team; the
`team_id` is passed as an action argument to `Label.Read.ByTeam` only.
* `WorkflowState` has no stored association attributes.
Its `team_id` appears only as a query argument passed to its read
actions; there is no `:team` field on that struct.
* `ProjectUpdate` has no stored association attributes.
Its `project_id` appears only as a required create argument.
* When `Team.Read.Find` fetches a team by id, the response includes
Expand Down Expand Up @@ -401,8 +413,8 @@ All four issue-update actions (`:assign`, `:attach_to_project`, `:close`,
`:set_status`) delegate to this shared runner rather than each building
their own `issueUpdate` mutation. `run/2` calls the mutation, receives
the updated issue map, and decodes it via `Issue.from_map/1` using
`Issue.full_fields/0` (the full fragment including assignee, team, and
comments).
`Issue.full_fields/0` (the full fragment including assignee, team,
comments, and labels).

The mutation GraphQL document is built at call time (not a module
attribute) because `Issue.full_fields/0` references `User`, `Team`, and
Expand Down
Loading