@kud/jira-ink 0.7.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.
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";
package/dist/index.js CHANGED
@@ -1,12 +1,191 @@
1
1
  import { MarkdownViewport } from '@kud/ink-markdown';
2
- import { colors, useTabs, KeyValue, Badge, Tabs, FooterHints, StatusMessage, 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,185 +434,6 @@ 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|blocker|critical|p[01])$/.test(n)) return "\u21C8";
350
- if (/^(high|urgent|major|p2)$/.test(n)) return "\u2191";
351
- if (/^(medium|normal|p3)$/.test(n)) return "=";
352
- if (/^(low|minor|p4)$/.test(n)) return "\u2193";
353
- if (/^(lowest|trivial|p5)$/.test(n)) return "\u21CA";
354
- return " ";
355
- };
356
- var UNITS = [
357
- [60, "m"],
358
- [60, "h"],
359
- [24, "d"],
360
- [7, "w"]
361
- ];
362
- var relativeAge = (iso, now = Date.now()) => {
363
- const then = Date.parse(iso);
364
- if (Number.isNaN(then)) return "";
365
- let value = Math.max(0, Math.round((now - then) / 1e3));
366
- let unit = "s";
367
- for (const [size, next] of UNITS) {
368
- if (value < size) break;
369
- value = Math.round(value / size);
370
- unit = next;
371
- }
372
- return `${value}${unit}`;
373
- };
374
- var pillVariantFor = (row) => {
375
- if (row.container) return "group";
376
- const t = row.type.toLowerCase();
377
- if (t === "bug") return "error";
378
- if (t === "story") return "info";
379
- return "muted";
380
- };
381
- var boardOf = async (client, jql, { tabs, board, limit = 200 } = {}) => {
382
- const [issues, derived] = await Promise.all([
383
- client.searchIssues(jql, { limit }),
384
- tabs?.length ? Promise.resolve(tabsFromConfig(tabs)) : board !== void 0 ? client.getBoardConfiguration(board).then(tabsFromBoard) : Promise.resolve(null)
385
- ]);
386
- return { rows: issues.map(toBoardRow), tabs: derived ?? tabsFromCategories() };
387
- };
388
- var COLOUR = {
389
- "\u21C8": colors.warning,
390
- "\u2191": colors.warning,
391
- "=": colors.muted,
392
- "\u2193": colors.info,
393
- "\u21CA": colors.info,
394
- " ": void 0
395
- };
396
- var priorityMarker = (name) => {
397
- const marker = priorityGlyph(name);
398
- const color = COLOUR[marker];
399
- return color ? { marker, color } : { marker };
400
- };
401
437
  var CHROME = 6;
402
438
  var LEGEND = [
403
439
  ["\u21C8 \u2191", "priority above the default"],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kud/jira-ink",
3
- "version": "0.7.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",