diff --git a/src/commands/issue/issue-view.ts b/src/commands/issue/issue-view.ts index f90c639a..8a6e6e42 100644 --- a/src/commands/issue/issue-view.ts +++ b/src/commands/issue/issue-view.ts @@ -206,6 +206,17 @@ export const viewCommand = new Command() outputLines.push(...renderedAttachments.split("\n")) } + if (issueData.documents && issueData.documents.length > 0) { + const documentsMarkdown = formatDocumentsAsMarkdown( + issueData.documents, + ) + const renderedDocuments = renderMarkdown(documentsMarkdown, { + lineWidth: terminalWidth, + extensions, + }) + outputLines.push(...renderedDocuments.split("\n")) + } + if ( showComments && derivedComments && derivedComments.visibleRootComments.length > 0 @@ -255,6 +266,10 @@ export const viewCommand = new Command() ) } + if (issueData.documents && issueData.documents.length > 0) { + markdown += formatDocumentsAsMarkdown(issueData.documents) + } + if ( showComments && derivedComments && derivedComments.visibleRootComments.length > 0 @@ -727,8 +742,9 @@ async function downloadIssueImages( return urlToPath } -// Type for attachments +// Type for attachments and documents type AttachmentInfo = FetchedIssueDetails["attachments"][number] +type DocumentInfo = FetchedIssueDetails["documents"][number] function getAttachmentCacheDir(): string { const configuredDir = getOption("attachment_dir") @@ -837,3 +853,17 @@ function formatAttachmentsAsMarkdown( return markdown } + +function formatDocumentsAsMarkdown(documents: DocumentInfo[]): string { + if (documents.length === 0) { + return "" + } + + let markdown = "\n\n## Documents\n\n" + + for (const document of documents) { + markdown += `- **${document.title}**: ${document.url}\n` + } + + return markdown +} diff --git a/src/utils/linear.ts b/src/utils/linear.ts index b4efa0de..501617ec 100644 --- a/src/utils/linear.ts +++ b/src/utils/linear.ts @@ -266,6 +266,16 @@ const issueDetailsWithCommentsQuery = gql(/* GraphQL */ ` createdAt } } + documents(first: 50) { + nodes { + id + title + slugId + url + createdAt + updatedAt + } + } } } `) @@ -326,6 +336,16 @@ const issueDetailsQuery = gql(/* GraphQL */ ` createdAt } } + documents(first: 50) { + nodes { + id + title + slugId + url + createdAt + updatedAt + } + } } } `) @@ -354,18 +374,26 @@ export type FetchedIssueComment = IssueDetailsWithComments["comments"]["nodes"][ ] export type FetchedIssueDetailsWithComments = - & Omit + & Omit< + IssueDetailsWithComments, + "children" | "comments" | "attachments" | "documents" + > & { children: IssueDetailsWithComments["children"]["nodes"] comments: IssueDetailsWithComments["comments"]["nodes"] attachments: IssueDetailsWithComments["attachments"]["nodes"] + documents: IssueDetailsWithComments["documents"]["nodes"] } export type FetchedIssueDetailsWithoutComments = - & Omit + & Omit< + IssueDetailsWithoutComments, + "children" | "attachments" | "documents" + > & { children: IssueDetailsWithoutComments["children"]["nodes"] attachments: IssueDetailsWithoutComments["attachments"]["nodes"] + documents: IssueDetailsWithoutComments["documents"]["nodes"] } export type FetchedIssueDetails = @@ -395,6 +423,7 @@ export async function fetchIssueDetails( children: data.children?.nodes || [], comments: data.comments?.nodes || [], attachments: data.attachments?.nodes || [], + documents: data.documents?.nodes || [], } } @@ -405,6 +434,7 @@ export async function fetchIssueDetails( ...data, children: data.children?.nodes || [], attachments: data.attachments?.nodes || [], + documents: data.documents?.nodes || [], } } catch (error) { spinner?.stop() diff --git a/test/commands/issue/__snapshots__/issue-view.test.ts.snap b/test/commands/issue/__snapshots__/issue-view.test.ts.snap index 04836686..496a25e1 100644 --- a/test/commands/issue/__snapshots__/issue-view.test.ts.snap +++ b/test/commands/issue/__snapshots__/issue-view.test.ts.snap @@ -88,6 +88,30 @@ Users are experiencing issues logging in when their session expires. Sounds good! Also, we should add better error messaging for expired sessions. +" +stderr: +"" +`; + +snapshot[`Issue View Command - With Documents And Attachments 1`] = ` +stdout: +"# TEST-246: Audit issue resource output + +**State:** In Progress | **Priority:** ▄▆█ | **Assignee:** @Jane Smith + +Ensure issue view shows both attachments and documents. + +## Attachments + +- **Design mock**: https://example.com/design-mock _[figma]_ + _Figma file_ + + +## Documents + +- **Implementation plan**: https://linear.app/test-team/document/implementation-plan-impl-plan-123 +- **QA checklist**: https://linear.app/test-team/document/qa-checklist-qa-checklist-456 + " stderr: "" @@ -188,6 +212,69 @@ stderr: "" `; +snapshot[`Issue View Command - JSON Output With Documents And Attachments 1`] = ` +stdout: +'{ + "identifier": "TEST-246", + "title": "Audit issue resource output", + "description": "Ensure issue view shows both attachments and documents.", + "url": "https://linear.app/test-team/issue/TEST-246/audit-issue-resource-output", + "branchName": "test-246-issue-resource-output", + "state": { + "name": "In Progress", + "color": "#f87462" + }, + "assignee": { + "name": "jane.smith", + "displayName": "Jane Smith" + }, + "priority": 2, + "project": null, + "projectMilestone": null, + "cycle": null, + "parent": null, + "children": { + "nodes": [] + }, + "attachments": { + "nodes": [ + { + "id": "attachment-1", + "title": "Design mock", + "url": "https://example.com/design-mock", + "subtitle": "Figma file", + "sourceType": "figma", + "metadata": {}, + "createdAt": "2024-01-15T10:30:00Z" + } + ] + }, + "documents": { + "nodes": [ + { + "id": "document-1", + "title": "Implementation plan", + "slugId": "impl-plan-123", + "url": "https://linear.app/test-team/document/implementation-plan-impl-plan-123", + "createdAt": "2024-01-15T09:30:00Z", + "updatedAt": "2024-01-15T09:45:00Z" + }, + { + "id": "document-2", + "title": "QA checklist", + "slugId": "qa-checklist-456", + "url": "https://linear.app/test-team/document/qa-checklist-qa-checklist-456", + "createdAt": "2024-01-15T09:00:00Z", + "updatedAt": "2024-01-15T09:15:00Z" + } + ] + } +} +' +stderr: +"" +`; + snapshot[`Issue View Command - With Parent And Sub-issues 1`] = ` stdout: "# TEST-456: Implement user authentication diff --git a/test/commands/issue/issue-view.test.ts b/test/commands/issue/issue-view.test.ts index a166f7bf..9225a3d8 100644 --- a/test/commands/issue/issue-view.test.ts +++ b/test/commands/issue/issue-view.test.ts @@ -262,6 +262,100 @@ await snapshotTest({ }, }) +// Test with documents and attachments +await snapshotTest({ + name: "Issue View Command - With Documents And Attachments", + meta: import.meta, + colors: false, + args: ["TEST-246", "--no-comments"], + denoArgs, + async fn() { + const server = new MockLinearServer([ + { + queryName: "GetIssueDetails", + queryIncludes: "documents(first: 50)", + variables: { id: "TEST-246" }, + response: { + data: { + issue: { + identifier: "TEST-246", + title: "Audit issue resource output", + description: + "Ensure issue view shows both attachments and documents.", + url: + "https://linear.app/test-team/issue/TEST-246/audit-issue-resource-output", + branchName: "test-246-issue-resource-output", + state: { + name: "In Progress", + color: "#f87462", + }, + assignee: { + name: "jane.smith", + displayName: "Jane Smith", + }, + priority: 2, + project: null, + projectMilestone: null, + cycle: null, + parent: null, + children: { + nodes: [], + }, + attachments: { + nodes: [ + { + id: "attachment-1", + title: "Design mock", + url: "https://example.com/design-mock", + subtitle: "Figma file", + sourceType: "figma", + metadata: {}, + createdAt: "2024-01-15T10:30:00Z", + }, + ], + }, + documents: { + nodes: [ + { + id: "document-1", + title: "Implementation plan", + slugId: "impl-plan-123", + url: + "https://linear.app/test-team/document/implementation-plan-impl-plan-123", + createdAt: "2024-01-15T09:30:00Z", + updatedAt: "2024-01-15T09:45:00Z", + }, + { + id: "document-2", + title: "QA checklist", + slugId: "qa-checklist-456", + url: + "https://linear.app/test-team/document/qa-checklist-qa-checklist-456", + createdAt: "2024-01-15T09:00:00Z", + updatedAt: "2024-01-15T09:15:00Z", + }, + ], + }, + }, + }, + }, + }, + ]) + + try { + await server.start() + Deno.env.set("LINEAR_GRAPHQL_ENDPOINT", server.getEndpoint()) + Deno.env.set("LINEAR_API_KEY", "Bearer test-token") + + await viewCommand.parse() + } finally { + await server.stop() + Deno.env.delete("LINEAR_GRAPHQL_ENDPOINT") + Deno.env.delete("LINEAR_API_KEY") + } + }, +}) + // Test with mock server - Issue not found await snapshotTest({ name: "Issue View Command - Issue Not Found", @@ -443,6 +537,100 @@ await snapshotTest({ }, }) +// Test JSON output with documents and attachments +await snapshotTest({ + name: "Issue View Command - JSON Output With Documents And Attachments", + meta: import.meta, + colors: false, + args: ["TEST-246", "--json", "--no-comments"], + denoArgs, + async fn() { + const server = new MockLinearServer([ + { + queryName: "GetIssueDetails", + queryIncludes: "documents(first: 50)", + variables: { id: "TEST-246" }, + response: { + data: { + issue: { + identifier: "TEST-246", + title: "Audit issue resource output", + description: + "Ensure issue view shows both attachments and documents.", + url: + "https://linear.app/test-team/issue/TEST-246/audit-issue-resource-output", + branchName: "test-246-issue-resource-output", + state: { + name: "In Progress", + color: "#f87462", + }, + assignee: { + name: "jane.smith", + displayName: "Jane Smith", + }, + priority: 2, + project: null, + projectMilestone: null, + cycle: null, + parent: null, + children: { + nodes: [], + }, + attachments: { + nodes: [ + { + id: "attachment-1", + title: "Design mock", + url: "https://example.com/design-mock", + subtitle: "Figma file", + sourceType: "figma", + metadata: {}, + createdAt: "2024-01-15T10:30:00Z", + }, + ], + }, + documents: { + nodes: [ + { + id: "document-1", + title: "Implementation plan", + slugId: "impl-plan-123", + url: + "https://linear.app/test-team/document/implementation-plan-impl-plan-123", + createdAt: "2024-01-15T09:30:00Z", + updatedAt: "2024-01-15T09:45:00Z", + }, + { + id: "document-2", + title: "QA checklist", + slugId: "qa-checklist-456", + url: + "https://linear.app/test-team/document/qa-checklist-qa-checklist-456", + createdAt: "2024-01-15T09:00:00Z", + updatedAt: "2024-01-15T09:15:00Z", + }, + ], + }, + }, + }, + }, + }, + ]) + + try { + await server.start() + Deno.env.set("LINEAR_GRAPHQL_ENDPOINT", server.getEndpoint()) + Deno.env.set("LINEAR_API_KEY", "Bearer test-token") + + await viewCommand.parse() + } finally { + await server.stop() + Deno.env.delete("LINEAR_GRAPHQL_ENDPOINT") + Deno.env.delete("LINEAR_API_KEY") + } + }, +}) + // Test with parent and sub-issues await snapshotTest({ name: "Issue View Command - With Parent And Sub-issues", diff --git a/test/utils/mock_linear_server.ts b/test/utils/mock_linear_server.ts index 7a2bbd52..b041b8b4 100644 --- a/test/utils/mock_linear_server.ts +++ b/test/utils/mock_linear_server.ts @@ -13,6 +13,7 @@ interface MockResponse { queryName: string + queryIncludes?: string variables?: Record response: Record status?: number @@ -136,6 +137,10 @@ export class MockLinearServer { return false } + if (mock.queryIncludes != null && !query.includes(mock.queryIncludes)) { + return false + } + // If no variables specified in mock, match any variables if (!mock.variables) { return true