Skip to content
Open
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
16 changes: 16 additions & 0 deletions apps/cli/src/commands/issue/comment.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,22 @@ describe("issue comment", () => {
expect(printTable).toHaveBeenCalled();
});

it("passes paging options to the API with --list", async () => {
mockClient.getIssueComments.mockResolvedValue([]);

await parseCommand(
() => import("./comment"),
["TEST-1", "--list", "--order", "desc", "--count", "5", "--min-id", "10", "--max-id", "20"],
);

expect(mockClient.getIssueComments).toHaveBeenCalledWith("TEST-1", {
order: "desc",
count: 5,
minId: 10,
maxId: 20,
});
});

it("shows message when no comments found with --list", async () => {
mockClient.getIssueComments.mockResolvedValue([]);

Expand Down
24 changes: 21 additions & 3 deletions apps/cli/src/commands/issue/comment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@ import {
confirmOrExit,
formatDate,
outputResult,
parseArg,
printTable,
resolveStdinArg,
vInteger,
} from "@repo/cli-utils";
import consola from "consola";
import * as v from "valibot";
import { BeeCommand, ENV_AUTH } from "../../lib/bee-command";
import * as opt from "../../lib/common-options";

Expand All @@ -16,13 +19,19 @@ const comment = new BeeCommand("comment")
.description(
`When input is piped, it is used as the body automatically.

Use \`--list\`, \`--edit-last\`, or \`--delete-last\` for other comment operations.`,
Use \`--list\`, \`--edit-last\`, or \`--delete-last\` for other comment operations.

\`--list\` returns the 20 oldest comments by default. Use \`--order desc\` to list the newest comments first, and \`--min-id\` / \`--max-id\` for cursor-based pagination.`,
)
.argument("<issue>", "Issue ID or issue key")
.option("-b, --body <text>", "Comment body")
.addOption(opt.notify())
.addOption(opt.attachment())
.option("--list", "List comments on the issue")
.addOption(opt.minId())
.addOption(opt.maxId())
.addOption(opt.count())
.addOption(opt.order())
.option("--edit-last", "Edit your most recent comment")
.option("--delete-last", "Delete your most recent comment")
.addOption(opt.json())
Expand All @@ -37,7 +46,11 @@ Use \`--list\`, \`--edit-last\`, or \`--delete-last\` for other comment operatio
description: "Add a comment from stdin",
command: 'echo "Comment body" | bee issue comment PROJECT-123',
},
{ description: "List all comments", command: "bee issue comment PROJECT-123 --list" },
{ description: "List comments", command: "bee issue comment PROJECT-123 --list" },
{
description: "List the 5 newest comments",
command: "bee issue comment PROJECT-123 --list --order desc -L 5",
},
{
description: "Edit your last comment",
command: 'bee issue comment PROJECT-123 --edit-last -b "Updated text"',
Expand All @@ -51,7 +64,12 @@ Use \`--list\`, \`--edit-last\`, or \`--delete-last\` for other comment operatio
const { client } = await getClient(opts.space);

if (opts.list) {
const comments = await client.getIssueComments(issue, { order: "asc" });
const comments = await client.getIssueComments(issue, {
minId: parseArg(v.optional(vInteger), opts.minId, "--min-id"),
maxId: parseArg(v.optional(vInteger), opts.maxId, "--max-id"),
count: parseArg(v.optional(vInteger), opts.count, "--count"),
order: opts.order ?? "asc",
});

outputResult(comments, opts as { json?: string }, (data) => {
const filtered = data.filter((c) => c.content);
Expand Down
32 changes: 31 additions & 1 deletion apps/cli/src/commands/issue/view.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,12 +82,42 @@ describe("issue view", () => {

await parseCommand(() => import("./view"), ["PROJ-1", "--comments"]);

expect(mockClient.getIssueComments).toHaveBeenCalledWith("PROJ-1", { order: "asc" });
expect(mockClient.getIssueComments).toHaveBeenCalledWith("PROJ-1", {
order: "asc",
count: 100,
});
expect(consola.log).toHaveBeenCalledWith(expect.stringContaining("Comments"));
expect(consola.log).toHaveBeenCalledWith(expect.stringContaining("Charlie"));
expect(consola.log).toHaveBeenCalledWith(expect.stringContaining("A comment"));
});

it("fetches every page of comments with --comments flag", async () => {
const firstPage = Array.from({ length: 100 }, (_, index) => ({
id: index + 1,
content: `Comment ${index + 1}`,
createdUser: { name: "Charlie" },
created: "2025-01-03T00:00:00Z",
}));
mockClient.getIssue.mockResolvedValue(sampleIssue);
mockClient.getIssueComments.mockResolvedValueOnce(firstPage).mockResolvedValueOnce([
{
id: 101,
content: "Newest comment",
createdUser: { name: "Dana" },
created: "2025-01-04T00:00:00Z",
},
]);

await parseCommand(() => import("./view"), ["PROJ-1", "--comments"]);

expect(mockClient.getIssueComments).toHaveBeenLastCalledWith("PROJ-1", {
order: "asc",
count: 100,
minId: 100,
});
expect(consola.log).toHaveBeenCalledWith(expect.stringContaining("Newest comment"));
});

it("opens browser with --web flag", async () => {
await parseCommand(() => import("./view"), ["PROJ-1", "--web"]);

Expand Down
27 changes: 24 additions & 3 deletions apps/cli/src/commands/issue/view.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,36 @@
import { getClient, issueUrl, openOrPrintUrl } from "@repo/backlog-utils";
import { type BacklogClient, getClient, issueUrl, openOrPrintUrl } from "@repo/backlog-utils";
import { formatDate, outputResult, printDefinitionList } from "@repo/cli-utils";
import { type Entity } from "backlog-js";
import consola from "consola";
import { BeeCommand, ENV_AUTH } from "../../lib/bee-command";
import * as opt from "../../lib/common-options";

const formatHours = (hours: number | null | undefined): string | undefined =>
hours === null || hours === undefined ? undefined : `${hours}h`;

// The API returns at most 100 comments per request, so page through them by ID.
const COMMENTS_PAGE_SIZE = 100;

const getAllIssueComments = async (
client: BacklogClient,
issue: string,
): Promise<Entity.Issue.Comment[]> => {
const comments: Entity.Issue.Comment[] = [];
let page: Entity.Issue.Comment[];
do {
page = await client.getIssueComments(issue, {
order: "asc",
count: COMMENTS_PAGE_SIZE,
minId: comments.at(-1)?.id,
});
comments.push(...page);
} while (page.length === COMMENTS_PAGE_SIZE);
return comments;
};

const view = new BeeCommand("view")
.summary("View an issue")
.description(`Use \`--comments\` to include comments. Use \`--web\` to open in the browser.`)
.description(`Use \`--comments\` to include all comments. Use \`--web\` to open in the browser.`)
.argument("<issue>", "Issue ID or issue key")
.option("--comments", "Include comments")
.addOption(opt.web("issue"))
Expand All @@ -34,7 +55,7 @@ const view = new BeeCommand("view")

const issueData = await client.getIssue(issue);

const comments = opts.comments ? await client.getIssueComments(issue, { order: "asc" }) : [];
const comments = opts.comments ? await getAllIssueComments(client, issue) : [];

outputResult(issueData, opts as { json?: string }, (data) => {
consola.log("");
Expand Down