Skip to content

Commit df13fd8

Browse files
feat(issue): include labels in Issue.full_fields and display (#210)
- Add `:labels` attribute to `LinearCli.Linear.Issue` (`{:array, :term}`). - Extend `full_fields/0` to request `labels { nodes { ... } }` using `Label.base_fields/0`, consistent with the existing `comments` pattern. - Extend `from_map/1` to parse `labels.nodes` from API responses, with a `|| []` fallback so base_fields responses (which omit labels) are handled gracefully. - Show a "Labels: ..." line in `Display.issue_full/1` when labels are present; suppress it when the list is empty via `Enum.reject`. - Update tests: assert `full_fields` query contains "labels", verify label parsing from API response, and cover Display rendering. - Update Ash domain ERD doc (CLAUDE.md maintenance convention). Co-authored-by: bougyman's bot <ruby-automation@users.noreply.github.com>
1 parent 1ae4fba commit df13fd8

5 files changed

Lines changed: 113 additions & 10 deletions

File tree

app/lib/linear_cli/cli/display.ex

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,12 +89,18 @@ defmodule LinearCli.CLI.Display do
8989
defp issue_full(issue) do
9090
header = issue_line(issue)
9191
sep = String.duplicate("-", String.length(header))
92+
labels = labels_line(issue.labels)
9293
description = render_markdown(issue.description)
9394
comments = Enum.map_join(issue.comments, "\n", &comment_block/1)
9495

95-
Enum.join([header, sep, description, comments], "\n")
96+
[header, sep, labels, description, comments]
97+
|> Enum.reject(&(&1 == ""))
98+
|> Enum.join("\n")
9699
end
97100

101+
defp labels_line([]), do: ""
102+
defp labels_line(labels), do: "Labels: #{Enum.map_join(labels, ", ", & &1.name)}"
103+
98104
defp comment_block(comment) do
99105
user = (comment.user && comment.user.name) || "unknown"
100106
"--- #{user} ---\n#{render_markdown(comment.body)}"

app/lib/linear_cli/linear/issue.ex

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ defmodule LinearCli.Linear.Issue do
7474
attribute :state, :term, public?: true
7575
attribute :team, :term, public?: true
7676
attribute :comments, {:array, :term}, public?: true, default: []
77+
attribute :labels, {:array, :term}, public?: true, default: []
7778
end
7879

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

99101
@doc false
@@ -112,6 +114,11 @@ defmodule LinearCli.Linear.Issue do
112114
Enum.map(
113115
get_in(map, ["comments", "nodes"]) || [],
114116
&LinearCli.Linear.Comment.from_map/1
117+
),
118+
labels:
119+
Enum.map(
120+
get_in(map, ["labels", "nodes"]) || [],
121+
&LinearCli.Linear.Label.from_map/1
115122
)
116123
)
117124
end

app/test/linear_cli/cli/display_test.exs

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ defmodule LinearCli.CLI.DisplayTest do
44
import ExUnit.CaptureIO
55

66
alias LinearCli.CLI.Display
7-
alias LinearCli.Linear.Issue
7+
alias LinearCli.Linear.{Issue, Label}
88

99
test "full issue output syntax-highlights fenced Elixir code" do
1010
issue = %Issue{
@@ -55,4 +55,37 @@ defmodule LinearCli.CLI.DisplayTest do
5555
assert output =~ theme.syntax.name_function <> "hello" <> theme.reset
5656
refute output =~ theme.code_text <> "class Greeter"
5757
end
58+
59+
test "full issue output includes a Labels line when labels are present" do
60+
issue = %Issue{
61+
id: "issue-2",
62+
identifier: "EXT-2",
63+
title: "Labelled issue",
64+
description: "Some work",
65+
comments: [],
66+
labels: [
67+
%Label{id: "l1", name: "Bug", description: nil, is_group: false},
68+
%Label{id: "l2", name: "Feature", description: nil, is_group: false}
69+
]
70+
}
71+
72+
output = capture_io(fn -> Display.show(issue, %{full: true}) end)
73+
74+
assert output =~ "Labels: Bug, Feature"
75+
end
76+
77+
test "full issue output omits the Labels line when no labels are present" do
78+
issue = %Issue{
79+
id: "issue-3",
80+
identifier: "EXT-3",
81+
title: "Unlabelled issue",
82+
description: "Some work",
83+
comments: [],
84+
labels: []
85+
}
86+
87+
output = capture_io(fn -> Display.show(issue, %{full: true}) end)
88+
89+
refute output =~ "Labels:"
90+
end
5891
end

app/test/linear_cli/linear/issue_test.exs

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ defmodule LinearCli.Linear.IssueTest do
106106
assert id == "CRY-2"
107107
assert query =~ "issue(id: $id)"
108108
assert query =~ "comments"
109+
assert query =~ "labels"
109110

110111
Req.Test.json(conn, %{
111112
"data" => %{
@@ -117,14 +118,58 @@ defmodule LinearCli.Linear.IssueTest do
117118
"description" => nil,
118119
"assignee" => nil,
119120
"team" => %{"id" => "t1", "key" => "ENG", "name" => "Engineering"},
120-
"comments" => %{"nodes" => []}
121+
"comments" => %{"nodes" => []},
122+
"labels" => %{"nodes" => []}
121123
}
122124
}
123125
})
124126
end)
125127

126128
assert {:ok, [issue]} = Linear.issues(%{ids: ["cry-2"]})
127129
assert issue.identifier == "CRY-2"
130+
assert issue.labels == []
131+
end
132+
133+
test "issues/1 with ids parses labels from the full-detail response" do
134+
Req.Test.stub(LinearCli.Api, fn conn ->
135+
{:ok, body, conn} = Plug.Conn.read_body(conn)
136+
%{"variables" => %{"id" => _id}} = Jason.decode!(body)
137+
138+
Req.Test.json(conn, %{
139+
"data" => %{
140+
"issue" => %{
141+
"id" => "i2",
142+
"identifier" => "CRY-2",
143+
"title" => "Ship it",
144+
"branchName" => "cry-2-ship-it",
145+
"description" => nil,
146+
"assignee" => nil,
147+
"team" => %{"id" => "t1", "key" => "ENG", "name" => "Engineering"},
148+
"comments" => %{"nodes" => []},
149+
"labels" => %{
150+
"nodes" => [
151+
%{
152+
"id" => "lbl-1",
153+
"name" => "Bug",
154+
"description" => nil,
155+
"isGroup" => false
156+
},
157+
%{
158+
"id" => "lbl-2",
159+
"name" => "Feature",
160+
"description" => "A new feature",
161+
"isGroup" => false
162+
}
163+
]
164+
}
165+
}
166+
}
167+
})
168+
end)
169+
170+
assert {:ok, [issue]} = Linear.issues(%{ids: ["cry-2"]})
171+
assert length(issue.labels) == 2
172+
assert Enum.map(issue.labels, & &1.name) == ["Bug", "Feature"]
128173
end
129174

130175
test "issues/1 parses the issue's current state when present in the response" do

documents/ash-domain-erd.adoc

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ erDiagram
7272
WorkflowState state
7373
Team team
7474
Comment[] comments
75+
Label[] labels
7576
}
7677
Label {
7778
string id PK
@@ -103,6 +104,7 @@ erDiagram
103104
Issue }o--|| WorkflowState : "state [nested]"
104105
Issue }o--|| Team : "team [nested]"
105106
Issue ||--o{ Comment : "comments [nested]"
107+
Issue ||--o{ Label : "labels [nested]"
106108
User }o--o{ Team : "teams [nested]"
107109
Comment }o--|| User : "user/author [nested]"
108110
Project }o--o{ Team : "teams [nested]"
@@ -140,7 +142,7 @@ Eight resources are registered in `LinearCli.Linear`
140142

141143
| `LinearCli.Linear.Issue`
142144
| `id` (`:string`)
143-
| `identifier`, `title`, `branch_name`, `description`, `assignee` (`:term`), `state` (`:term`), `team` (`:term`), `comments` (`{:array, :term}`)
145+
| `identifier`, `title`, `branch_name`, `description`, `assignee` (`:term`), `state` (`:term`), `team` (`:term`), `comments` (`{:array, :term}`), `labels` (`{:array, :term}`)
144146

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

201+
| `Issue`
202+
| `Label`
203+
| one-to-many
204+
| `labels` (`{:array, :term}`)
205+
| GraphQL/nested-data — populated by `Issue.from_map/1` from `issue.labels.nodes` (full fragment only)
206+
199207
| `User`
200208
| `Team`
201209
| many-to-many
@@ -224,9 +232,13 @@ data, not from declared Ash relationships.
224232

225233
=== Notes on unidirectional associations
226234

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

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

0 commit comments

Comments
 (0)