The GraphQL half: what a board is, and what is on it.
Projects v2 has no REST endpoint worth the name, so every question here is a
GraphQL one, asked through the gh CLI rather than over HTTP directly. That
is a deliberate trade: gh already holds the token, already refreshes it,
already knows about enterprise hosts and GH_TOKEN, and a tool that is run
from a checkout is being run by someone who has gh set up. The cost is a
process per request, which against a network round trip is nothing.
Impure out here, pure in there
projects/1 and items/1 shell out; decode_projects/1 and decode_items/1
are pure functions over the decoded JSON. The decoders are where the shape of
a project item actually lives — a card that points at an issue, at a pull
request, or at nothing — so that is what the tests exercise, on captured
payloads, with no token and no network.
What is fetched
The cards come with their bodies, in the same pass that fetches the rest. It is more bytes than a list needs — but opening the detail popup is then instant and works with the network gone, which is worth more on a board of a few hundred cards than the bytes are.
Errors
Nothing raises. Every call answers {:ok, value} or {:error, message}, the
message being something to put in front of a person: gh not installed, not
logged in, a repository that is not there. Atui.Fetch will turn a crash into
an error too, but an error with a sentence in it is worth more than one with
an exception in it.
Summary
Functions
A board's title and the columns of its kanban field.
Turns the board payload into a board map.
Turns one page of the items payload into Gitpro.Item structs.
Turns the projectsV2 payload into project maps, open boards first.
Every card on the board, following the cursor until there are no more.
The Projects v2 boards repo is linked to, open ones first.
Runs a GraphQL query through gh and answers its data.
Types
Functions
A board's title and the columns of its kanban field.
"The kanban board" is a single-select field by convention, and by convention it is called Status — that is what the board view groups by, and what the filter's column flags are. A board that renamed it still works: the first single-select field is taken when there is no Status.
Turns the board payload into a board map.
column_field is the name of the field the columns came from, and it is
carried rather than assumed: a card has a value for every field that has one,
so reading its column means knowing which of them to read.
@spec decode_items(map(), String.t() | nil) :: [Gitpro.Item.t()]
Turns one page of the items payload into Gitpro.Item structs.
Cards whose content came back empty are dropped: that is what a card pointing at an issue in a repository the token cannot see looks like, and a row with no title in it is worse than no row.
Turns the projectsV2 payload into project maps, open boards first.
@spec items(String.t(), String.t() | nil) :: {:ok, [Gitpro.Item.t()]} | {:error, String.t()}
Every card on the board, following the cursor until there are no more.
A board of a few hundred cards is several round trips, so this is the call
that belongs in an Atui.Fetch rather than in a view callback.
@spec projects(Gitpro.Git.repo()) :: {:ok, [project()]} | {:error, String.t()}
The Projects v2 boards repo is linked to, open ones first.
Order is the reason this sorts at all: a repository collects closed boards over the years and the one somebody wants is almost never among them, so an open board is always offered before a closed one. Within each, the board's own order is kept.
Runs a GraphQL query through gh and answers its data.
GraphQL answers 200 with an errors array rather than a status code, so the
exit status is not enough to tell a working query from a broken one; both are
checked, and either turns into a message.