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
15 changes: 12 additions & 3 deletions src/commands/issue/issue-mine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,12 @@ import {
getProjectOptionsByName,
getTeamIdByKey,
getTeamKey,
isIssueBlocked,
selectOption,
} from "../../utils/linear.ts"
import { openTeamAssigneeView } from "../../utils/actions.ts"
import { pipeToUserPager, shouldUsePager } from "../../utils/pager.ts"
import { header, muted } from "../../utils/styling.ts"
import { header, muted, warning } from "../../utils/styling.ts"
import { shouldShowSpinner } from "../../utils/hyperlink.ts"
import {
handleError,
Expand Down Expand Up @@ -291,6 +292,7 @@ export const mineCommand = new Command()
? Deno.consoleSize()
: { columns: 120 }
const PRIORITY_WIDTH = 3
const BLOCKED_WIDTH = 1
const ID_WIDTH = Math.max(
2, // minimum width for "ID" header
...issues.map((issue) => issue.identifier.length),
Expand Down Expand Up @@ -323,6 +325,7 @@ export const mineCommand = new Command()

type TableRow = {
priorityStr: string
blockedStr: string
identifier: string
title: string
labels: string
Expand Down Expand Up @@ -394,8 +397,11 @@ export const mineCommand = new Command()
)
const statePadded = stateColored + " ".repeat(stateRemainingSpace)

const blockedStr = isIssueBlocked(issue) ? warning("⊘") : " "

return {
priorityStr,
blockedStr,
identifier: issue.identifier,
title: issue.title,
labels,
Expand All @@ -405,7 +411,8 @@ export const mineCommand = new Command()
}
})

const fixed = PRIORITY_WIDTH + ID_WIDTH + UPDATED_WIDTH + SPACE_WIDTH +
const fixed = PRIORITY_WIDTH + BLOCKED_WIDTH + ID_WIDTH +
UPDATED_WIDTH + SPACE_WIDTH +
LABEL_WIDTH + ESTIMATE_WIDTH + STATE_WIDTH + SPACE_WIDTH
const PADDING = 1
const maxTitleWidth = Math.max(
Expand All @@ -418,6 +425,7 @@ export const mineCommand = new Command()
padDisplay("ID", ID_WIDTH),
padDisplay("TITLE", titleWidth),
padDisplay("LABELS", LABEL_WIDTH),
padDisplay("B", BLOCKED_WIDTH),
padDisplay("E", ESTIMATE_WIDTH),
padDisplay("STATE", STATE_WIDTH),
padDisplay(updatedHeader, UPDATED_WIDTH),
Expand All @@ -432,6 +440,7 @@ export const mineCommand = new Command()
for (const row of tableData) {
const {
priorityStr,
blockedStr,
identifier,
title,
labels,
Expand All @@ -446,7 +455,7 @@ export const mineCommand = new Command()

const issueLine = `${padDisplay(priorityStr, PRIORITY_WIDTH)} ${
padDisplay(identifier, ID_WIDTH)
} ${truncTitle} ${labels} ${
} ${truncTitle} ${labels} ${padDisplay(blockedStr, BLOCKED_WIDTH)} ${
padDisplay(estimate?.toString() || "-", ESTIMATE_WIDTH)
} ${state} ${muted(padDisplay(timeAgo, UPDATED_WIDTH))}`
outputLines.push(issueLine)
Expand Down
14 changes: 13 additions & 1 deletion src/commands/issue/issue-query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,13 @@ import {
getProjectOptionsByName,
getTeamIdByKey,
getTeamKey,
isIssueBlocked,
searchIssuesByTerm,
selectOption,
} from "../../utils/linear.ts"
import { pipeToUserPager, shouldUsePager } from "../../utils/pager.ts"
import { shouldShowSpinner } from "../../utils/hyperlink.ts"
import { header, muted } from "../../utils/styling.ts"
import { header, muted, warning } from "../../utils/styling.ts"
import {
handleError,
NotFoundError,
Expand Down Expand Up @@ -423,6 +424,12 @@ interface DisplayableIssue {
assignee?: { initials: string } | null
team?: { key: string }
labels: { nodes: Array<{ name: string; color: string }> }
inverseRelations?: {
nodes: Array<{
type: string
issue?: { state?: { type?: string | null } | null } | null
}>
} | null
}

function formatIssueTable(
Expand All @@ -435,6 +442,7 @@ function formatIssueTable(
: { columns: 120 }

const priorityWidth = 3
const blockedWidth = 1
const idWidth = Math.max(2, ...issues.map((i) => i.identifier.length))
const teamWidth = showTeamColumn
? Math.max(
Expand Down Expand Up @@ -468,6 +476,7 @@ function formatIssueTable(
idWidth,
...(showTeamColumn ? [teamWidth] : []),
labelWidth,
blockedWidth,
estimateWidth,
...(showAssigneeColumn ? [assigneeWidth] : []),
stateWidth,
Expand All @@ -485,6 +494,7 @@ function formatIssueTable(
...(showTeamColumn ? [padDisplay("TEAM", teamWidth)] : []),
padDisplay("TITLE", titleWidth),
padDisplay("LABELS", labelWidth),
padDisplay("B", blockedWidth),
padDisplay("E", estimateWidth),
...(showAssigneeColumn ? [padDisplay("A", assigneeWidth)] : []),
padDisplay("STATE", stateWidth),
Expand All @@ -508,12 +518,14 @@ function formatIssueTable(
const timeAgo = muted(
padDisplay(getTimeAgo(new Date(issue.updatedAt)), updatedWidth),
)
const blockedCell = isIssueBlocked(issue) ? warning("⊘") : " "
const cells = [
padDisplay(getPriorityDisplay(issue.priority), priorityWidth),
padDisplay(issue.identifier, idWidth),
...(showTeamColumn ? [padDisplay(issue.team?.key ?? "", teamWidth)] : []),
title,
formatLabels(issue.labels.nodes, labelWidth),
padDisplay(blockedCell, blockedWidth),
padDisplay(issue.estimate?.toString() || "-", estimateWidth),
...(showAssigneeColumn
? [
Expand Down
60 changes: 60 additions & 0 deletions src/utils/linear.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,26 @@ export function parseDateFilter(value: string, flagName: string): string {
return parsed.toISOString()
}

type InverseRelationNode = {
type: string
issue?: { state?: { type?: string | null } | null } | null
}

export function isIssueBlocked(issue: {
inverseRelations?: { nodes: ReadonlyArray<InverseRelationNode> } | null
}): boolean {
const nodes = issue.inverseRelations?.nodes
if (!nodes) return false
for (const rel of nodes) {
if (rel.type !== "blocks") continue
const blockerStateType = rel.issue?.state?.type
if (blockerStateType !== "completed" && blockerStateType !== "canceled") {
return true
}
}
return false
}

export function formatIssueIdentifier(providedId: string): string {
return normalizeIssueIdentifier(providedId) ?? providedId.toUpperCase()
}
Expand Down Expand Up @@ -596,6 +616,7 @@ export async function fetchIssuesForState(
id
name
color
type
}
labels {
nodes {
Expand All @@ -604,6 +625,19 @@ export async function fetchIssuesForState(
color
}
}
inverseRelations(first: 100) {
nodes {
id
type
issue {
id
identifier
state {
type
}
}
}
}
updatedAt
}
pageInfo {
Expand Down Expand Up @@ -732,6 +766,19 @@ const queryIssuesQuery = gql(/* GraphQL */ `
color
}
}
inverseRelations(first: 100) {
nodes {
id
type
issue {
id
identifier
state {
type
}
}
}
}
}
pageInfo {
hasNextPage
Expand Down Expand Up @@ -971,6 +1018,19 @@ const searchIssuesQuery = gql(/* GraphQL */ `
color
}
}
inverseRelations(first: 100) {
nodes {
id
type
issue {
id
identifier
state {
type
}
}
}
}
metadata
}
pageInfo {
Expand Down
6 changes: 6 additions & 0 deletions test/commands/issue/__snapshots__/issue-query.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@ stdout:
"color": "#eb5757"
}
]
},
"inverseRelations": {
"nodes": []
}
}
],
Expand Down Expand Up @@ -144,6 +147,9 @@ stdout:
}
]
},
"inverseRelations": {
"nodes": []
},
"metadata": {
"context": {},
"score": 0.42
Expand Down
6 changes: 4 additions & 2 deletions test/commands/issue/issue-list.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ Deno.test("Issue List Command - Filter By Label", async () => {
id: "state-1",
name: "In Progress",
color: "#f2c94c",
type: "started",
},
labels: {
nodes: [{
Expand All @@ -75,6 +76,7 @@ Deno.test("Issue List Command - Filter By Label", async () => {
color: "#eb5757",
}],
},
inverseRelations: { nodes: [] },
updatedAt: "2026-03-13T10:00:00.000Z",
},
],
Expand Down Expand Up @@ -102,8 +104,8 @@ Deno.test("Issue List Command - Filter By Label", async () => {

assertEquals(
logs.join("\n") + "\n",
"◌ ID TITLE LABELS E STATE UPDATED \n" +
"⚠⚠⚠ ENG-101 Fix login bug Bug 3 In Progress 17 days ago\n",
"◌ ID TITLE LABELS B E STATE UPDATED \n" +
"⚠⚠⚠ ENG-101 Fix login bug Bug 3 In Progress 17 days ago\n",
)
} finally {
logStub.restore()
Expand Down
Loading
Loading