@kud/jira-ink 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/index.d.ts +30 -13
  2. package/dist/index.js +249 -200
  3. package/package.json +1 -1
package/dist/index.d.ts CHANGED
@@ -19,6 +19,8 @@ type IssueDetail = {
19
19
  assignee: string;
20
20
  reporter: string;
21
21
  labels: string[];
22
+ /** Jira's priority name, verbatim; absent when the field is off. */
23
+ priority?: string;
22
24
  parent?: {
23
25
  key: string;
24
26
  summary: string;
@@ -79,6 +81,12 @@ type IssueBodyProps = {
79
81
  /** Instance URL, as `loadConfig` resolves it; only used to build the browser link. */
80
82
  baseUrl: string;
81
83
  issueKey: string;
84
+ /**
85
+ * The summary as the host already knows it — from the list row that was
86
+ * opened — so the frame carries the title from the first frame rather than
87
+ * only once the fetch answers. The fetched summary wins once it is here.
88
+ */
89
+ summary?: string;
82
90
  onExit: () => void;
83
91
  /** Override the terminal-derived size when the host draws inside a frame. */
84
92
  width?: number;
@@ -97,7 +105,7 @@ type IssueBodyProps = {
97
105
  * `IssueDetailView` for jira-cli's own TUI, which owns the prompts they need;
98
106
  * here they are simply not wired, so the hint row does not offer them.
99
107
  */
100
- declare const IssueBody: ({ client, baseUrl, issueKey, onExit, width, height, frame, theme, }: IssueBodyProps) => string | number | bigint | boolean | Iterable<ReactNode> | Promise<string | number | bigint | boolean | react.ReactPortal | react.ReactElement<unknown, string | react.JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined> | react.JSX.Element | null | undefined;
108
+ declare const IssueBody: ({ client, baseUrl, issueKey, summary, onExit, width, height, frame, theme, }: IssueBodyProps) => string | number | bigint | boolean | Iterable<ReactNode> | Promise<string | number | bigint | boolean | react.ReactPortal | react.ReactElement<unknown, string | react.JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined> | react.JSX.Element | null | undefined;
101
109
 
102
110
  /** Jira's own three, plus `unknown` for a status that arrives without one. */
103
111
  type StatusCategory = "new" | "indeterminate" | "done" | "unknown";
@@ -183,13 +191,25 @@ type Block = {
183
191
  declare const blocksFor: (rows: BoardRow[]) => Block[];
184
192
  /** Index into `blocks` of the n-th issue, so a cursor over issues maps to a line. */
185
193
  declare const blockIndexOfIssue: (blocks: Block[], issueIndex: number) => number;
194
+ type PriorityGlyph = "⇈" | "↑" | "=" | "↓" | "⇊" | " ";
186
195
  /**
187
- * Priority names are per-instance, so this matches the default scheme, the
188
- * common renames and the P0–P4 ladder rather than an exact list. Medium — or
189
- * anything unrecognised — draws nothing: the glyph column marks the two ends
190
- * only.
196
+ * Rank against the scheme's default rung, in Jira's own arrow grammar: two
197
+ * rungs above draws `⇈`, one `↑`, the default `=`, one below `↓`, two `⇊`.
198
+ *
199
+ * Five glyphs, not three, and the normal rung gets one. The column used to
200
+ * mark the two ends only (`▲` / `▼`, nothing for normal), which on a four-rung
201
+ * scheme — P1 to P4, the common shape — put a mark on half the rows, drew P2
202
+ * as if it were normal, and made P3 and P4 identical. And a blank beside a
203
+ * marked row read as MISSING rather than as normal: the absent-vs-zero
204
+ * distinction, one glyph at a time. So `=` says "we looked, it is normal",
205
+ * and the blank finally means only "no priority, or one we cannot place".
206
+ *
207
+ * Names are per-instance, so this matches the default scheme, the common
208
+ * renames and the P0–P5 ladder rather than an exact list. Deriving rank from
209
+ * the instance's ordered priority list, with its default flag, is the honest
210
+ * version of this and where it goes next; the names are the fallback.
191
211
  */
192
- declare const priorityGlyph: (name: string | null) => "\u25B2" | "\u25BC" | " ";
212
+ declare const priorityGlyph: (name: string | null) => PriorityGlyph;
193
213
  declare const relativeAge: (iso: string, now?: number) => string;
194
214
  declare const pillVariantFor: (row: Pick<BoardRow, "type" | "container">) => "group" | "info" | "error" | "muted";
195
215
  type BoardModel = {
@@ -281,18 +301,15 @@ declare const IssueBoard: ({ model, viewer, loadedAt, scope, showingAll, searchE
281
301
 
282
302
  type PriorityMarker = {
283
303
  /** Always one column wide, so a list filling every row keeps its grid. */
284
- marker: "▲" | "▼" | " ";
304
+ marker: PriorityGlyph;
285
305
  /** Absent for the blank: nothing to paint. */
286
306
  color?: string;
287
307
  };
288
308
  /**
289
309
  * The priority cell as the board draws it — glyph and colour together, so a
290
310
  * host filling gh-ink's `marker` / `markerColor` pair and the board's own row
291
- * cannot disagree on a hue. Shape is the channel (`▲` above normal, `▼`
292
- * below, nothing for normal); the hues sit in different bins so the two arrows
293
- * do not merge at row-scan speed, and a warm one on the high end so it clears
294
- * the row text. The hex-vs-token question was settled with the pill map: one
295
- * site, here, never a host-side table that drifts.
311
+ * cannot disagree on a hue. One site, never a host-side table that drifts;
312
+ * see `priorityGlyph` for what the glyphs say.
296
313
  */
297
314
  declare const priorityMarker: (name: string | null) => PriorityMarker;
298
315
 
@@ -305,4 +322,4 @@ declare const priorityMarker: (name: string | null) => PriorityMarker;
305
322
  declare const MOCK_BOARD_ROWS: BoardRow[];
306
323
  declare const mockBoard: () => BoardModel;
307
324
 
308
- export { type Block, type BoardModel, type BoardOptions, type BoardRow, type BoardScope, type BoardTab, type BoardTabs, IssueBoard, type IssueBoardProps, IssueBody, type IssueBodyProps, type IssueDetail, IssueDetailView, type IssueDetailViewProps, MOCK_BOARD_ROWS, OFF_BOARD, type PriorityMarker, type StatusCategory, type TabSource, type Transition, blockIndexOfIssue, blocksFor, boardOf, countsFor, issueDetailOf, mockBoard, pillVariantFor, priorityGlyph, priorityMarker, relativeAge, tabOf, tabsFromBoard, tabsFromCategories, tabsFromConfig, toBoardRow, transitionsOf, visibleTabs };
325
+ export { type Block, type BoardModel, type BoardOptions, type BoardRow, type BoardScope, type BoardTab, type BoardTabs, IssueBoard, type IssueBoardProps, IssueBody, type IssueBodyProps, type IssueDetail, IssueDetailView, type IssueDetailViewProps, MOCK_BOARD_ROWS, OFF_BOARD, type PriorityGlyph, type PriorityMarker, type StatusCategory, type TabSource, type Transition, blockIndexOfIssue, blocksFor, boardOf, countsFor, issueDetailOf, mockBoard, pillVariantFor, priorityGlyph, priorityMarker, relativeAge, tabOf, tabsFromBoard, tabsFromCategories, tabsFromConfig, toBoardRow, transitionsOf, visibleTabs };
package/dist/index.js CHANGED
@@ -1,12 +1,191 @@
1
1
  import { MarkdownViewport } from '@kud/ink-markdown';
2
- import { useTabs, KeyValue, Badge, Tabs, FooterHints, StatusMessage, colors, useListCursor, pillWidth, TextInput, SelectableRow, Pill, LoadingScreen } from '@kud/ink-ui';
2
+ import { colors, KeyValue, Badge, FooterHints, StatusMessage, useTabs, useListCursor, pillWidth, TextInput, Tabs, SelectableRow, Pill, LoadingScreen } from '@kud/ink-ui';
3
3
  import { useInput, Box, Text, useStdout } from 'ink';
4
4
  import { useState, useEffect, useMemo } from 'react';
5
+ import { isContainerType, locateAttachments, adfToMarkdown, looksLikeJql } from '@kud/jira';
5
6
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
6
7
  import { exec } from 'child_process';
7
- import { locateAttachments, adfToMarkdown, isContainerType, looksLikeJql } from '@kud/jira';
8
8
 
9
9
  // src/components/issue-detail-view.tsx
10
+ var CATEGORIES = ["new", "indeterminate", "done"];
11
+ var categoryOf = (key) => CATEGORIES.find((c) => c === key) ?? "unknown";
12
+ var toBoardRow = (issue) => ({
13
+ key: issue.key,
14
+ summary: issue.fields.summary ?? "",
15
+ status: issue.fields.status?.name ?? "\u2014",
16
+ statusId: issue.fields.status?.id ?? "",
17
+ category: categoryOf(issue.fields.status?.statusCategory?.key),
18
+ type: issue.fields.issuetype?.name ?? "",
19
+ container: isContainerType(issue.fields.issuetype),
20
+ priority: issue.fields.priority?.name ?? null,
21
+ ...issue.fields.parent ? {
22
+ parent: {
23
+ key: issue.fields.parent.key,
24
+ summary: issue.fields.parent.fields?.summary ?? ""
25
+ }
26
+ } : {},
27
+ assignee: issue.fields.assignee?.displayName ?? "unassigned",
28
+ updated: issue.fields.updated ?? ""
29
+ });
30
+ var OFF_BOARD = "__off_board__";
31
+ var CATEGORY_TABS = [
32
+ { value: "todo", label: "To do", category: "new" },
33
+ { value: "doing", label: "In progress", category: "indeterminate" },
34
+ { value: "done", label: "Done", category: "done" }
35
+ ];
36
+ var withOffBoard = (tabs) => [
37
+ ...tabs,
38
+ { value: OFF_BOARD, label: "Off board", offBoard: true }
39
+ ];
40
+ var tabsFromConfig = (configured) => ({
41
+ source: "config",
42
+ tabs: withOffBoard(
43
+ configured.map((t, i) => ({
44
+ value: `config:${i}`,
45
+ label: t.label,
46
+ statuses: new Set(t.statuses.map((s) => s.toLowerCase()))
47
+ }))
48
+ )
49
+ });
50
+ var tabsFromBoard = (configuration) => {
51
+ const columns = configuration.columnConfig?.columns ?? [];
52
+ if (columns.length === 0) return null;
53
+ return {
54
+ source: "board",
55
+ tabs: withOffBoard(
56
+ columns.map((c, i) => ({
57
+ value: `column:${i}`,
58
+ label: c.name,
59
+ statuses: new Set(c.statuses.map((s) => s.id))
60
+ }))
61
+ )
62
+ };
63
+ };
64
+ var tabsFromCategories = () => ({
65
+ source: "categories",
66
+ tabs: CATEGORY_TABS
67
+ });
68
+ var tabOf = (row, { tabs }) => {
69
+ const claimed = tabs.find(
70
+ (t) => t.statuses?.has(row.statusId) || t.statuses?.has(row.status.toLowerCase())
71
+ );
72
+ if (claimed) return claimed.value;
73
+ const byCategory = tabs.find((t) => t.category && t.category === row.category);
74
+ if (byCategory) return byCategory.value;
75
+ return tabs.find((t) => t.offBoard)?.value ?? "doing";
76
+ };
77
+ var countsFor = (rows, tabs) => {
78
+ const counts = Object.fromEntries(
79
+ tabs.tabs.map((t) => [t.value, 0])
80
+ );
81
+ for (const row of rows)
82
+ counts[tabOf(row, tabs)] = (counts[tabOf(row, tabs)] ?? 0) + 1;
83
+ return counts;
84
+ };
85
+ var visibleTabs = (tabs, counts) => tabs.tabs.filter((t) => !t.offBoard || (counts[t.value] ?? 0) > 0);
86
+ var blocksFor = (rows) => {
87
+ const hasStructure = rows.some((r) => r.parent || r.container);
88
+ if (!hasStructure)
89
+ return rows.map((row) => ({ kind: "issue", row, depth: 0 }));
90
+ const heads = new Map(rows.filter((r) => r.container).map((r) => [r.key, r]));
91
+ const children = /* @__PURE__ */ new Map();
92
+ const orphans = [];
93
+ for (const row of rows) {
94
+ if (row.container) continue;
95
+ const key = row.parent?.key;
96
+ if (!key) {
97
+ orphans.push(row);
98
+ continue;
99
+ }
100
+ children.set(key, [...children.get(key) ?? [], row]);
101
+ }
102
+ const groups = [];
103
+ for (const [key, head] of heads)
104
+ groups.push([
105
+ { kind: "issue", row: head, depth: 0 },
106
+ ...(children.get(key) ?? []).map((row) => ({
107
+ kind: "issue",
108
+ row,
109
+ depth: 1
110
+ }))
111
+ ]);
112
+ for (const [key, kids] of children) {
113
+ if (heads.has(key)) continue;
114
+ groups.push([
115
+ { kind: "fence", key, summary: kids[0].parent?.summary ?? "" },
116
+ ...kids.map((row) => ({ kind: "issue", row, depth: 1 }))
117
+ ]);
118
+ }
119
+ if (orphans.length)
120
+ groups.push([
121
+ ...groups.length ? [{ kind: "fence", key: null, summary: "No epic" }] : [],
122
+ ...orphans.map((row) => ({ kind: "issue", row, depth: 0 }))
123
+ ]);
124
+ return groups.flatMap(
125
+ (group, i) => i === 0 ? group : [{ kind: "gap" }, ...group]
126
+ );
127
+ };
128
+ var blockIndexOfIssue = (blocks, issueIndex) => {
129
+ let seen = -1;
130
+ for (let i = 0; i < blocks.length; i++) {
131
+ if (blocks[i].kind === "issue" && ++seen === issueIndex) return i;
132
+ }
133
+ return 0;
134
+ };
135
+ var priorityGlyph = (name) => {
136
+ const n = (name ?? "").trim().toLowerCase();
137
+ if (/^(highest|blocker|critical|p[01])$/.test(n)) return "\u21C8";
138
+ if (/^(high|urgent|major|p2)$/.test(n)) return "\u2191";
139
+ if (/^(medium|normal|p3)$/.test(n)) return "=";
140
+ if (/^(low|minor|p4)$/.test(n)) return "\u2193";
141
+ if (/^(lowest|trivial|p5)$/.test(n)) return "\u21CA";
142
+ return " ";
143
+ };
144
+ var UNITS = [
145
+ [60, "m"],
146
+ [60, "h"],
147
+ [24, "d"],
148
+ [7, "w"]
149
+ ];
150
+ var relativeAge = (iso, now = Date.now()) => {
151
+ const then = Date.parse(iso);
152
+ if (Number.isNaN(then)) return "";
153
+ let value = Math.max(0, Math.round((now - then) / 1e3));
154
+ let unit = "s";
155
+ for (const [size, next] of UNITS) {
156
+ if (value < size) break;
157
+ value = Math.round(value / size);
158
+ unit = next;
159
+ }
160
+ return `${value}${unit}`;
161
+ };
162
+ var pillVariantFor = (row) => {
163
+ if (row.container) return "group";
164
+ const t = row.type.toLowerCase();
165
+ if (t === "bug") return "error";
166
+ if (t === "story") return "info";
167
+ return "muted";
168
+ };
169
+ var boardOf = async (client, jql, { tabs, board, limit = 200 } = {}) => {
170
+ const [issues, derived] = await Promise.all([
171
+ client.searchIssues(jql, { limit }),
172
+ tabs?.length ? Promise.resolve(tabsFromConfig(tabs)) : board !== void 0 ? client.getBoardConfiguration(board).then(tabsFromBoard) : Promise.resolve(null)
173
+ ]);
174
+ return { rows: issues.map(toBoardRow), tabs: derived ?? tabsFromCategories() };
175
+ };
176
+ var COLOUR = {
177
+ "\u21C8": colors.warning,
178
+ "\u2191": colors.warning,
179
+ "=": colors.muted,
180
+ "\u2193": colors.info,
181
+ "\u21CA": colors.info,
182
+ " ": void 0
183
+ };
184
+ var priorityMarker = (name) => {
185
+ const marker = priorityGlyph(name);
186
+ const color = COLOUR[marker];
187
+ return color ? { marker, color } : { marker };
188
+ };
10
189
  var truncate = (s, max) => s.length <= max ? s : `${s.slice(0, Math.max(0, max - 1))}\u2026`;
11
190
  var originOf = (a) => a.origins.map(
12
191
  (o) => o.kind === "comment" ? `comment by ${o.author ?? "unknown"}` : o.kind === "description" ? "description" : "issue"
@@ -20,14 +199,52 @@ var humanSize = (bytes) => {
20
199
  const value = bytes / 1024 ** e;
21
200
  return `${e === 0 ? value : value.toFixed(1)}${units[e]}`;
22
201
  };
23
- var commentsAsMarkdown = (issue) => issue.comments.length === 0 ? "_No comments._" : issue.comments.map((c) => `**${c.author}** \xB7 ${c.created}
202
+ var ago = (iso) => {
203
+ const rel = relativeAge(iso);
204
+ return rel ? `${rel} ago` : iso;
205
+ };
206
+ var documentOf = (issue) => {
207
+ const parts = [issue.description || "_No description._"];
208
+ if (issue.comments.length > 0)
209
+ parts.push(
210
+ `## Comments (${issue.comments.length})`,
211
+ ...issue.comments.map(
212
+ (c) => `**${c.author}** \xB7 ${ago(c.created)}
24
213
 
25
- ${c.body}`).join("\n\n---\n\n");
26
- var attachmentsAsMarkdown = (issue) => issue.attachments.length === 0 ? "_No attachments._" : issue.attachments.map(
27
- (a) => `- \`${a.filename}\` \u2014 ${a.mimeType}, ${humanSize(a.size)}
28
- from ${originOf(a)}
214
+ ${c.body}`
215
+ )
216
+ );
217
+ if (issue.attachments.length > 0)
218
+ parts.push(
219
+ `## Attachments (${issue.attachments.length})`,
220
+ issue.attachments.map(
221
+ (a) => `- \`${a.filename}\` \u2014 ${a.mimeType}, ${humanSize(a.size)} \xB7 from ${originOf(a)}
29
222
  \`jira attachment read ${issue.key} ${a.filename}\``
30
- ).join("\n");
223
+ ).join("\n")
224
+ );
225
+ return parts.join("\n\n");
226
+ };
227
+ var FactsLine = ({ issue }) => {
228
+ const priority = priorityMarker(issue.priority ?? null);
229
+ const lead = priority.marker !== " " && priority.marker !== "=" ? /* @__PURE__ */ jsxs(Fragment, { children: [
230
+ /* @__PURE__ */ jsx(Text, { color: priority.color, children: priority.marker }),
231
+ /* @__PURE__ */ jsxs(Text, { children: [
232
+ " ",
233
+ issue.priority
234
+ ] })
235
+ ] }) : null;
236
+ const plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
237
+ const facts = [
238
+ `by ${issue.reporter}`,
239
+ ...issue.labels.length > 0 ? [`labels ${issue.labels.join(", ")}`] : [],
240
+ plural(issue.comments.length, "comment"),
241
+ ...issue.attachments.length > 0 ? [plural(issue.attachments.length, "attachment")] : []
242
+ ];
243
+ return /* @__PURE__ */ jsxs(Box, { children: [
244
+ lead,
245
+ /* @__PURE__ */ jsx(Text, { color: colors.muted, children: (lead ? " \xB7 " : "") + facts.join(" \xB7 ") })
246
+ ] });
247
+ };
31
248
  var IssueDetailView = ({
32
249
  issue,
33
250
  height,
@@ -40,16 +257,6 @@ var IssueDetailView = ({
40
257
  frame,
41
258
  theme
42
259
  }) => {
43
- const tabs = [
44
- { value: "description", label: "Description" },
45
- { value: "comments", label: "Comments", count: issue.comments.length },
46
- {
47
- value: "attachments",
48
- label: "Attachments",
49
- count: issue.attachments.length
50
- }
51
- ];
52
- const { active } = useTabs(tabs, { initial: "description" });
53
260
  const [scroll, setScroll] = useState(0);
54
261
  useInput((input, key) => {
55
262
  if (input === "t" && onTransition) return onTransition();
@@ -59,12 +266,11 @@ var IssueDetailView = ({
59
266
  if (key.downArrow || input === "j") setScroll((s) => s + 1);
60
267
  if (key.upArrow || input === "k") setScroll((s) => Math.max(0, s - 1));
61
268
  });
62
- const source = active === "comments" ? commentsAsMarkdown(issue) : active === "attachments" ? attachmentsAsMarkdown(issue) : issue.description || "_No description._";
63
- const keyValueRows = 3 + (issue.parent ? 1 : 0) + (issue.labels.length > 0 ? 1 : 0) + (frame ? 0 : 1);
64
- const chromeLines = keyValueRows + 1 + 2 + 1 + (frame ? 0 : 4);
269
+ const source = documentOf(issue);
270
+ const keyValueRows = 2 + (issue.parent ? 1 : 0) + (frame ? 0 : 1);
271
+ const chromeLines = keyValueRows + 1 + 1 + (frame ? 0 : 4);
65
272
  const bodyHeight = Math.max(3, height - chromeLines);
66
273
  const hints = [
67
- ["\u2190\u2192", "section"],
68
274
  ["\u2191\u2193", "scroll"],
69
275
  ...onTransition ? [["t", "move"]] : [],
70
276
  ...onComment ? [["c", "comment"]] : [],
@@ -74,20 +280,27 @@ var IssueDetailView = ({
74
280
  ];
75
281
  const body = /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
76
282
  /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
283
+ /* @__PURE__ */ jsx(KeyValue, { label: "status", value: /* @__PURE__ */ jsx(Badge, { children: issue.status }) }),
284
+ frame ? null : /* @__PURE__ */ jsx(KeyValue, { label: "type", value: issue.type }),
285
+ /* @__PURE__ */ jsx(
286
+ KeyValue,
287
+ {
288
+ label: "assignee",
289
+ value: issue.assignee === "unassigned" ? /* @__PURE__ */ jsx(Text, { color: colors.muted, children: "unassigned" }) : issue.assignee
290
+ }
291
+ ),
77
292
  issue.parent && /* @__PURE__ */ jsx(
78
293
  KeyValue,
79
294
  {
80
295
  label: "parent",
81
- value: `${issue.parent.key} ${truncate(issue.parent.summary, 60)}`
296
+ value: /* @__PURE__ */ jsxs(Fragment, { children: [
297
+ /* @__PURE__ */ jsx(Text, { color: colors.secondary, children: issue.parent.key }),
298
+ /* @__PURE__ */ jsx(Text, { children: ` ${truncate(issue.parent.summary, 60)}` })
299
+ ] })
82
300
  }
83
301
  ),
84
- /* @__PURE__ */ jsx(KeyValue, { label: "status", value: /* @__PURE__ */ jsx(Badge, { children: issue.status }) }),
85
- frame ? null : /* @__PURE__ */ jsx(KeyValue, { label: "type", value: issue.type }),
86
- /* @__PURE__ */ jsx(KeyValue, { label: "assignee", value: issue.assignee }),
87
- /* @__PURE__ */ jsx(KeyValue, { label: "reporter", value: issue.reporter }),
88
- issue.labels.length > 0 && /* @__PURE__ */ jsx(KeyValue, { label: "labels", value: issue.labels.join(", ") })
302
+ /* @__PURE__ */ jsx(FactsLine, { issue })
89
303
  ] }),
90
- /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Tabs, { items: tabs, active: active ?? "description" }) }),
91
304
  /* @__PURE__ */ jsx(Box, { marginTop: 1, height: bodyHeight, overflow: "hidden", children: /* @__PURE__ */ jsx(
92
305
  MarkdownViewport,
93
306
  {
@@ -129,13 +342,14 @@ var issueDetailOf = async (client, baseUrl, key) => {
129
342
  assignee: f.assignee?.displayName ?? "unassigned",
130
343
  reporter: f.reporter?.displayName ?? "\u2014",
131
344
  labels: f.labels ?? [],
345
+ ...f.priority?.name ? { priority: f.priority.name } : {},
132
346
  parent: f.parent ? { key: f.parent.key, summary: f.parent.fields?.summary ?? "" } : void 0,
133
347
  url: `${baseUrl}/browse/${issue.key}`,
134
348
  description: adfToMarkdown(f.description, filenameOf),
135
349
  comments: (f.comment?.comments ?? []).map((c) => ({
136
350
  id: c.id,
137
351
  author: c.author?.displayName ?? "unknown",
138
- created: c.created?.slice(0, 10) ?? "",
352
+ created: c.created ?? "",
139
353
  body: adfToMarkdown(c.body, filenameOf)
140
354
  })),
141
355
  attachments
@@ -169,6 +383,7 @@ var IssueBody = ({
169
383
  client,
170
384
  baseUrl,
171
385
  issueKey,
386
+ summary,
172
387
  onExit,
173
388
  width,
174
389
  height,
@@ -192,7 +407,7 @@ var IssueBody = ({
192
407
  }, [client, baseUrl, issueKey]);
193
408
  const framed = (body) => frame ? frame({
194
409
  title: issueKey,
195
- subtitle: "",
410
+ subtitle: summary ?? "",
196
411
  hints: [["\u232B", "back"]],
197
412
  body
198
413
  }) : body;
@@ -219,177 +434,11 @@ var IssueBody = ({
219
434
  }
220
435
  );
221
436
  };
222
- var CATEGORIES = ["new", "indeterminate", "done"];
223
- var categoryOf = (key) => CATEGORIES.find((c) => c === key) ?? "unknown";
224
- var toBoardRow = (issue) => ({
225
- key: issue.key,
226
- summary: issue.fields.summary ?? "",
227
- status: issue.fields.status?.name ?? "\u2014",
228
- statusId: issue.fields.status?.id ?? "",
229
- category: categoryOf(issue.fields.status?.statusCategory?.key),
230
- type: issue.fields.issuetype?.name ?? "",
231
- container: isContainerType(issue.fields.issuetype),
232
- priority: issue.fields.priority?.name ?? null,
233
- ...issue.fields.parent ? {
234
- parent: {
235
- key: issue.fields.parent.key,
236
- summary: issue.fields.parent.fields?.summary ?? ""
237
- }
238
- } : {},
239
- assignee: issue.fields.assignee?.displayName ?? "unassigned",
240
- updated: issue.fields.updated ?? ""
241
- });
242
- var OFF_BOARD = "__off_board__";
243
- var CATEGORY_TABS = [
244
- { value: "todo", label: "To do", category: "new" },
245
- { value: "doing", label: "In progress", category: "indeterminate" },
246
- { value: "done", label: "Done", category: "done" }
247
- ];
248
- var withOffBoard = (tabs) => [
249
- ...tabs,
250
- { value: OFF_BOARD, label: "Off board", offBoard: true }
251
- ];
252
- var tabsFromConfig = (configured) => ({
253
- source: "config",
254
- tabs: withOffBoard(
255
- configured.map((t, i) => ({
256
- value: `config:${i}`,
257
- label: t.label,
258
- statuses: new Set(t.statuses.map((s) => s.toLowerCase()))
259
- }))
260
- )
261
- });
262
- var tabsFromBoard = (configuration) => {
263
- const columns = configuration.columnConfig?.columns ?? [];
264
- if (columns.length === 0) return null;
265
- return {
266
- source: "board",
267
- tabs: withOffBoard(
268
- columns.map((c, i) => ({
269
- value: `column:${i}`,
270
- label: c.name,
271
- statuses: new Set(c.statuses.map((s) => s.id))
272
- }))
273
- )
274
- };
275
- };
276
- var tabsFromCategories = () => ({
277
- source: "categories",
278
- tabs: CATEGORY_TABS
279
- });
280
- var tabOf = (row, { tabs }) => {
281
- const claimed = tabs.find(
282
- (t) => t.statuses?.has(row.statusId) || t.statuses?.has(row.status.toLowerCase())
283
- );
284
- if (claimed) return claimed.value;
285
- const byCategory = tabs.find((t) => t.category && t.category === row.category);
286
- if (byCategory) return byCategory.value;
287
- return tabs.find((t) => t.offBoard)?.value ?? "doing";
288
- };
289
- var countsFor = (rows, tabs) => {
290
- const counts = Object.fromEntries(
291
- tabs.tabs.map((t) => [t.value, 0])
292
- );
293
- for (const row of rows)
294
- counts[tabOf(row, tabs)] = (counts[tabOf(row, tabs)] ?? 0) + 1;
295
- return counts;
296
- };
297
- var visibleTabs = (tabs, counts) => tabs.tabs.filter((t) => !t.offBoard || (counts[t.value] ?? 0) > 0);
298
- var blocksFor = (rows) => {
299
- const hasStructure = rows.some((r) => r.parent || r.container);
300
- if (!hasStructure)
301
- return rows.map((row) => ({ kind: "issue", row, depth: 0 }));
302
- const heads = new Map(rows.filter((r) => r.container).map((r) => [r.key, r]));
303
- const children = /* @__PURE__ */ new Map();
304
- const orphans = [];
305
- for (const row of rows) {
306
- if (row.container) continue;
307
- const key = row.parent?.key;
308
- if (!key) {
309
- orphans.push(row);
310
- continue;
311
- }
312
- children.set(key, [...children.get(key) ?? [], row]);
313
- }
314
- const groups = [];
315
- for (const [key, head] of heads)
316
- groups.push([
317
- { kind: "issue", row: head, depth: 0 },
318
- ...(children.get(key) ?? []).map((row) => ({
319
- kind: "issue",
320
- row,
321
- depth: 1
322
- }))
323
- ]);
324
- for (const [key, kids] of children) {
325
- if (heads.has(key)) continue;
326
- groups.push([
327
- { kind: "fence", key, summary: kids[0].parent?.summary ?? "" },
328
- ...kids.map((row) => ({ kind: "issue", row, depth: 1 }))
329
- ]);
330
- }
331
- if (orphans.length)
332
- groups.push([
333
- ...groups.length ? [{ kind: "fence", key: null, summary: "No epic" }] : [],
334
- ...orphans.map((row) => ({ kind: "issue", row, depth: 0 }))
335
- ]);
336
- return groups.flatMap(
337
- (group, i) => i === 0 ? group : [{ kind: "gap" }, ...group]
338
- );
339
- };
340
- var blockIndexOfIssue = (blocks, issueIndex) => {
341
- let seen = -1;
342
- for (let i = 0; i < blocks.length; i++) {
343
- if (blocks[i].kind === "issue" && ++seen === issueIndex) return i;
344
- }
345
- return 0;
346
- };
347
- var priorityGlyph = (name) => {
348
- const n = (name ?? "").trim().toLowerCase();
349
- if (/highest|high|critical|blocker|urgent|^p[01]$/.test(n)) return "\u25B2";
350
- if (/lowest|low|minor|trivial|^p[3-5]$/.test(n)) return "\u25BC";
351
- return " ";
352
- };
353
- var UNITS = [
354
- [60, "m"],
355
- [60, "h"],
356
- [24, "d"],
357
- [7, "w"]
358
- ];
359
- var relativeAge = (iso, now = Date.now()) => {
360
- const then = Date.parse(iso);
361
- if (Number.isNaN(then)) return "";
362
- let value = Math.max(0, Math.round((now - then) / 1e3));
363
- let unit = "s";
364
- for (const [size, next] of UNITS) {
365
- if (value < size) break;
366
- value = Math.round(value / size);
367
- unit = next;
368
- }
369
- return `${value}${unit}`;
370
- };
371
- var pillVariantFor = (row) => {
372
- if (row.container) return "group";
373
- const t = row.type.toLowerCase();
374
- if (t === "bug") return "error";
375
- if (t === "story") return "info";
376
- return "muted";
377
- };
378
- var boardOf = async (client, jql, { tabs, board, limit = 200 } = {}) => {
379
- const [issues, derived] = await Promise.all([
380
- client.searchIssues(jql, { limit }),
381
- tabs?.length ? Promise.resolve(tabsFromConfig(tabs)) : board !== void 0 ? client.getBoardConfiguration(board).then(tabsFromBoard) : Promise.resolve(null)
382
- ]);
383
- return { rows: issues.map(toBoardRow), tabs: derived ?? tabsFromCategories() };
384
- };
385
- var priorityMarker = (name) => {
386
- const marker = priorityGlyph(name);
387
- return marker === "\u25B2" ? { marker, color: colors.warning } : marker === "\u25BC" ? { marker, color: colors.muted } : { marker };
388
- };
389
437
  var CHROME = 6;
390
438
  var LEGEND = [
391
- ["\u25B2", "high priority"],
392
- ["\u25BC", "low priority"],
439
+ ["\u21C8 \u2191", "priority above the default"],
440
+ ["=", "default priority"],
441
+ ["\u2193 \u21CA", "priority below the default"],
393
442
  ["\u2500\u2500 epic \u2500\u2500", "a parent not in this tab; its rows hang beneath"],
394
443
  ["\u2514\u2500", "a row under the container above it"],
395
444
  ["story \xB7 bug \xB7 task", "issue type"],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kud/jira-ink",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "description": "Ink components for viewing Jira issues: the board and the issue detail screen, shared by jira-cli's TUI and cockpit",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",