@kud/jira-ink 0.7.0 → 0.9.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 +292 -104
- package/dist/index.js +494 -233
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -2,102 +2,7 @@ import * as react from 'react';
|
|
|
2
2
|
import { ReactNode } from 'react';
|
|
3
3
|
import { MarkdownTheme } from '@kud/ink-markdown';
|
|
4
4
|
import { Hint } from '@kud/ink-ui';
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* One issue, as the detail screen reads it: every field already a string, ADF
|
|
9
|
-
* already Markdown, attachments already located. The view never touches a raw
|
|
10
|
-
* `JiraIssue`, so a host that fakes a `JiraClient` for a test or a screenshot
|
|
11
|
-
* gets a screen it can predict, and one that fetched the issue by some other
|
|
12
|
-
* route can build this shape by hand.
|
|
13
|
-
*/
|
|
14
|
-
type IssueDetail = {
|
|
15
|
-
key: string;
|
|
16
|
-
summary: string;
|
|
17
|
-
status: string;
|
|
18
|
-
type: string;
|
|
19
|
-
assignee: string;
|
|
20
|
-
reporter: string;
|
|
21
|
-
labels: string[];
|
|
22
|
-
parent?: {
|
|
23
|
-
key: string;
|
|
24
|
-
summary: string;
|
|
25
|
-
};
|
|
26
|
-
url: string;
|
|
27
|
-
description: string;
|
|
28
|
-
comments: {
|
|
29
|
-
id: string;
|
|
30
|
-
author: string;
|
|
31
|
-
created: string;
|
|
32
|
-
body: string;
|
|
33
|
-
}[];
|
|
34
|
-
attachments: LocatedAttachment[];
|
|
35
|
-
};
|
|
36
|
-
type Transition = {
|
|
37
|
-
id: string;
|
|
38
|
-
name: string;
|
|
39
|
-
to: string;
|
|
40
|
-
};
|
|
41
|
-
declare const issueDetailOf: (client: JiraClient, baseUrl: string, key: string) => Promise<IssueDetail>;
|
|
42
|
-
declare const transitionsOf: (client: JiraClient, key: string) => Promise<Transition[]>;
|
|
43
|
-
|
|
44
|
-
type IssueDetailViewProps = {
|
|
45
|
-
issue: IssueDetail;
|
|
46
|
-
height: number;
|
|
47
|
-
width: number;
|
|
48
|
-
onBack: () => void;
|
|
49
|
-
/**
|
|
50
|
-
* The write actions are optional, and a key whose handler is absent is
|
|
51
|
-
* neither bound nor hinted. A host that only READS — cockpit's ticket drill —
|
|
52
|
-
* mounts the same screen jira-cli's TUI does, minus the verbs it has no way
|
|
53
|
-
* to honour, rather than a hint row promising `t` and `c` that do nothing.
|
|
54
|
-
*/
|
|
55
|
-
onTransition?: () => void;
|
|
56
|
-
onComment?: () => void;
|
|
57
|
-
onAssign?: () => void;
|
|
58
|
-
onOpenBrowser?: () => void;
|
|
59
|
-
/**
|
|
60
|
-
* A host that draws its own chrome — a bordered drill frame with the title
|
|
61
|
-
* on top and the hints pinned at the foot — takes the screen in parts and
|
|
62
|
-
* places them. The view then draws neither its title line nor its footer,
|
|
63
|
-
* so nothing appears twice. `height` is still the whole area the host has
|
|
64
|
-
* given the body, frame excluded.
|
|
65
|
-
*/
|
|
66
|
-
frame?: (parts: {
|
|
67
|
-
title: string;
|
|
68
|
-
subtitle: string;
|
|
69
|
-
hints: Hint[];
|
|
70
|
-
body: ReactNode;
|
|
71
|
-
}) => ReactNode;
|
|
72
|
-
/** Colours for the Markdown body; a host passes its own tokens so links, code and headings sit in its palette. */
|
|
73
|
-
theme?: MarkdownTheme;
|
|
74
|
-
};
|
|
75
|
-
declare const IssueDetailView: ({ issue, height, width, onBack, onTransition, onComment, onAssign, onOpenBrowser, frame, theme, }: IssueDetailViewProps) => 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;
|
|
76
|
-
|
|
77
|
-
type IssueBodyProps = {
|
|
78
|
-
client: JiraClient;
|
|
79
|
-
/** Instance URL, as `loadConfig` resolves it; only used to build the browser link. */
|
|
80
|
-
baseUrl: string;
|
|
81
|
-
issueKey: string;
|
|
82
|
-
onExit: () => void;
|
|
83
|
-
/** Override the terminal-derived size when the host draws inside a frame. */
|
|
84
|
-
width?: number;
|
|
85
|
-
height?: number;
|
|
86
|
-
/** The host's chrome around the screen — see `IssueDetailView`. */
|
|
87
|
-
frame?: IssueDetailViewProps["frame"];
|
|
88
|
-
theme?: IssueDetailViewProps["theme"];
|
|
89
|
-
};
|
|
90
|
-
/**
|
|
91
|
-
* The issue screen as a host mounts it: hand it a client and a key, and it
|
|
92
|
-
* fetches, draws and reads its own keys until `onExit`. `JenkinsBody`'s shape,
|
|
93
|
-
* for the same reason — a cockpit mounting a ticket over its list should not
|
|
94
|
-
* have to know how an issue is fetched or what the loading states look like.
|
|
95
|
-
*
|
|
96
|
-
* Read-only by construction. The write verbs (`t`, `c`, `a`) exist on
|
|
97
|
-
* `IssueDetailView` for jira-cli's own TUI, which owns the prompts they need;
|
|
98
|
-
* here they are simply not wired, so the hint row does not offer them.
|
|
99
|
-
*/
|
|
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;
|
|
5
|
+
import { BoardTab as BoardTab$1, JiraClient, JiraBoardConfiguration, JiraIssue, LocatedAttachment, SearchMode } from '@kud/jira';
|
|
101
6
|
|
|
102
7
|
/** Jira's own three, plus `unknown` for a status that arrives without one. */
|
|
103
8
|
type StatusCategory = "new" | "indeterminate" | "done" | "unknown";
|
|
@@ -124,6 +29,22 @@ type BoardRow = {
|
|
|
124
29
|
assignee: string;
|
|
125
30
|
/** ISO timestamp, kept whole so the view can draw a relative age. */
|
|
126
31
|
updated: string;
|
|
32
|
+
/**
|
|
33
|
+
* Set when the row's own status lags the evidence — an epic Off board with
|
|
34
|
+
* a child already started, a task whose PR is ahead of it. The board draws
|
|
35
|
+
* the word `behind` bright, then this string dim as the reason; pass `""`
|
|
36
|
+
* when the head's own note already says why. **Presence is the flag, so
|
|
37
|
+
* test `behind !== undefined`, never `if (row.behind)`.**
|
|
38
|
+
*
|
|
39
|
+
* Host-filled, because only the host knows the evidence outside Jira.
|
|
40
|
+
* `withBehind` fills the two cases the board can see for itself.
|
|
41
|
+
*/
|
|
42
|
+
behind?: string;
|
|
43
|
+
/**
|
|
44
|
+
* A transition in flight: the label of the tab it is moving to, drawn as
|
|
45
|
+
* `⋯ → QA` until `transitionRow` or `settleRow` clears it.
|
|
46
|
+
*/
|
|
47
|
+
pending?: string;
|
|
127
48
|
};
|
|
128
49
|
declare const toBoardRow: (issue: JiraIssue) => BoardRow;
|
|
129
50
|
/**
|
|
@@ -157,7 +78,44 @@ declare const tabsFromBoard: (configuration: JiraBoardConfiguration) => BoardTab
|
|
|
157
78
|
declare const tabsFromCategories: () => BoardTabs;
|
|
158
79
|
/** The tab a row belongs to. Nothing ever falls out: unclaimed rows go Off board, or In progress on a category board. */
|
|
159
80
|
declare const tabOf: (row: BoardRow, { tabs }: BoardTabs) => string;
|
|
160
|
-
|
|
81
|
+
type Placement = {
|
|
82
|
+
/** The tabs a row is drawn in, in tab order. A container can be in several. */
|
|
83
|
+
tabsOf: (key: string) => string[];
|
|
84
|
+
/** The rows the board holds that call this one parent. */
|
|
85
|
+
childrenOf: (key: string) => BoardRow[];
|
|
86
|
+
};
|
|
87
|
+
/**
|
|
88
|
+
* Where every row goes, read BOTTOM-UP. A task sits in the tab of its own
|
|
89
|
+
* status; a container is placed by its CHILDREN, heading a group in each tab
|
|
90
|
+
* that holds one — so an epic In Development whose tasks are all in Backlog
|
|
91
|
+
* is absent from In progress and heads them Off board. Its own status never
|
|
92
|
+
* picks a tab; the board says it in a note instead.
|
|
93
|
+
*
|
|
94
|
+
* A container with no children on the board has nothing to be placed under,
|
|
95
|
+
* and sits Off board rather than claiming a stage tab it is not working in.
|
|
96
|
+
*/
|
|
97
|
+
declare const placementOf: (rows: BoardRow[], tabs: BoardTabs) => Placement;
|
|
98
|
+
/**
|
|
99
|
+
* The rows one tab draws: the tasks whose own status is here, and the
|
|
100
|
+
* container heading each of them. One filter does both halves, because a
|
|
101
|
+
* container's placement already names every tab it has a child in.
|
|
102
|
+
*/
|
|
103
|
+
declare const rowsInTab: (rows: BoardRow[], tabs: BoardTabs, tab: string, placement?: Placement) => BoardRow[];
|
|
104
|
+
/**
|
|
105
|
+
* The dim segments after a head's summary: its own status when it differs
|
|
106
|
+
* from this tab, then where the rest of its children are, with counts. Only
|
|
107
|
+
* rows the board holds are counted — never the epic-children query.
|
|
108
|
+
*/
|
|
109
|
+
declare const headNote: (row: BoardRow, tabs: BoardTabs, tab: string, placement: Placement) => string | undefined;
|
|
110
|
+
/**
|
|
111
|
+
* Fills `behind` for the two cases the board can see without leaving Jira:
|
|
112
|
+
* an epic parked Off board while a child has already started, and an epic
|
|
113
|
+
* still open when every child is Done. Both reasons are left empty — the
|
|
114
|
+
* head's own note already names the status that lags. A `behind` the host
|
|
115
|
+
* has already set is never overwritten.
|
|
116
|
+
*/
|
|
117
|
+
declare const withBehind: (rows: BoardRow[], tabs: BoardTabs) => BoardRow[];
|
|
118
|
+
declare const countsFor: (rows: BoardRow[], tabs: BoardTabs, placement?: Placement) => Record<string, number>;
|
|
161
119
|
/** Tabs worth drawing: every configured one, plus Off board only when it holds something. */
|
|
162
120
|
declare const visibleTabs: (tabs: BoardTabs, counts: Record<string, number>) => BoardTab[];
|
|
163
121
|
/**
|
|
@@ -168,21 +126,85 @@ declare const visibleTabs: (tabs: BoardTabs, counts: Record<string, number>) =>
|
|
|
168
126
|
* with no parent — and each group after the first is preceded by a blank
|
|
169
127
|
* line. A tab where nothing has a parent and nothing is a container degrades
|
|
170
128
|
* to a plain list, never to a lone fence.
|
|
129
|
+
*
|
|
130
|
+
* A LEAF is something the host hangs under a row that is not a Jira issue —
|
|
131
|
+
* cockpit hangs a task's pull requests there. The board never looks inside
|
|
132
|
+
* one: `L` is opaque, the host renders it and opens it. A leaf sits one
|
|
133
|
+
* level under its row, directly beneath it and before any children, so a
|
|
134
|
+
* row's subtree is always one contiguous run. Leaves are cursor stops like
|
|
135
|
+
* issues; fences and gaps never are.
|
|
171
136
|
*/
|
|
172
|
-
type Block = {
|
|
137
|
+
type Block<L = never> = {
|
|
173
138
|
kind: "gap";
|
|
174
139
|
} | {
|
|
175
140
|
kind: "fence";
|
|
176
141
|
key: string | null;
|
|
177
142
|
summary: string;
|
|
143
|
+
parent?: BoardRow;
|
|
178
144
|
} | {
|
|
179
145
|
kind: "issue";
|
|
180
146
|
row: BoardRow;
|
|
181
147
|
depth: 0 | 1;
|
|
148
|
+
} | {
|
|
149
|
+
kind: "leaf";
|
|
150
|
+
parent: BoardRow;
|
|
151
|
+
depth: 1 | 2;
|
|
152
|
+
data: L;
|
|
153
|
+
};
|
|
154
|
+
type Stop<L = never> = Extract<Block<L>, {
|
|
155
|
+
kind: "issue" | "leaf";
|
|
156
|
+
}>;
|
|
157
|
+
declare const isStop: <L>(block: Block<L>) => block is Stop<L>;
|
|
158
|
+
/** The rows and leaves in cursor order — what ↑↓ walks and ↵ opens. */
|
|
159
|
+
declare const stopsOf: <L>(blocks: Block<L>[]) => Stop<L>[];
|
|
160
|
+
declare const blocksFor: <L = never>(rows: BoardRow[], under?: (row: BoardRow) => L[],
|
|
161
|
+
/**
|
|
162
|
+
* Parents the board holds no row for, looked up by the host — jira with one
|
|
163
|
+
* extra `key in (…)` search, cockpit with the foreign-epics pass it already
|
|
164
|
+
* runs. They are never drawn as rows; they tell a fence where its parent is.
|
|
165
|
+
*/
|
|
166
|
+
parents?: BoardRow[]) => Block<L>[];
|
|
167
|
+
/**
|
|
168
|
+
* What a fence says after the parent's name, and whether it carries `behind`.
|
|
169
|
+
*
|
|
170
|
+
* Where the parent IS says it best: a tab label when the reader can press
|
|
171
|
+
* through and find it, the OWNER when they cannot — `@` being the house glyph
|
|
172
|
+
* for scope, as in the title bar's `@you`. On a `mine` board those two are
|
|
173
|
+
* the same question asked twice, so a tab name means yours and `@name` means
|
|
174
|
+
* not yours, and no fence ever says both. A board showing several assignees
|
|
175
|
+
* has no such shorthand and always names the owner, as its rows do.
|
|
176
|
+
*
|
|
177
|
+
* `behind` is a judgement on someone's own work, so it goes on a fence only
|
|
178
|
+
* when that someone is the viewer.
|
|
179
|
+
*
|
|
180
|
+
* Empty until the lookup lands: a fence that guessed would be wrong for a
|
|
181
|
+
* frame and then silently correct itself, which is worse than a blank.
|
|
182
|
+
*/
|
|
183
|
+
declare const fenceNote: (parent: BoardRow | undefined, tabs: BoardTabs, { viewer, mine }: {
|
|
184
|
+
viewer: string;
|
|
185
|
+
mine: boolean;
|
|
186
|
+
}) => {
|
|
187
|
+
segment: string;
|
|
188
|
+
behind: boolean;
|
|
182
189
|
};
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
190
|
+
/** Index into `blocks` of the n-th stop, so a cursor over stops maps to a line. */
|
|
191
|
+
declare const blockIndexOfStop: <L>(blocks: Block<L>[], stopIndex: number) => number;
|
|
192
|
+
/**
|
|
193
|
+
* The tree glyphs to the left of the stop at `i`, three columns per level:
|
|
194
|
+
* `├─ ` while a sibling still follows, `└─ ` for the last, and ahead of a
|
|
195
|
+
* depth-2 connector a `│ ` stem while its parent is not last. A `└─` on
|
|
196
|
+
* every child was tried in gh-ink and rejected — three closing corners in a
|
|
197
|
+
* column draw no tree at all — which is why last-ness is tracked here
|
|
198
|
+
* rather than left to the renderer.
|
|
199
|
+
*/
|
|
200
|
+
declare const treePrefix: <L>(blocks: Block<L>[], i: number) => string;
|
|
201
|
+
/**
|
|
202
|
+
* The block at `i` and everything hanging under it: a row's own leaves, its
|
|
203
|
+
* children and theirs. Pure, so a host can walk a subtree for every URL in
|
|
204
|
+
* it (`C` / `O`) without knowing how the tree was laid out. A leaf, a fence
|
|
205
|
+
* or a gap has no subtree beyond itself.
|
|
206
|
+
*/
|
|
207
|
+
declare const subtreeOf: <L>(blocks: Block<L>[], i: number) => Block<L>[];
|
|
186
208
|
type PriorityGlyph = "⇈" | "↑" | "=" | "↓" | "⇊" | " ";
|
|
187
209
|
/**
|
|
188
210
|
* Rank against the scheme's default rung, in Jira's own arrow grammar: two
|
|
@@ -208,6 +230,26 @@ type BoardModel = {
|
|
|
208
230
|
rows: BoardRow[];
|
|
209
231
|
tabs: BoardTabs;
|
|
210
232
|
};
|
|
233
|
+
/** A transition's target, as Jira gives it: id, name and category together. */
|
|
234
|
+
type TransitionTarget = {
|
|
235
|
+
id?: string;
|
|
236
|
+
name: string;
|
|
237
|
+
category?: StatusCategory;
|
|
238
|
+
};
|
|
239
|
+
/** The label of the tab a status lands in — what `⋯ → QA` names. */
|
|
240
|
+
declare const tabLabelOf: (to: TransitionTarget, tabs: BoardTabs) => string;
|
|
241
|
+
/**
|
|
242
|
+
* The hybrid move, in three pure steps. The row stays where it is under a
|
|
243
|
+
* `⋯ → QA` marker while the request is out; on the 204 its status is
|
|
244
|
+
* rewritten here and the tab follows within the same second, long before the
|
|
245
|
+
* board refetches — the refetch stays as reconciliation, never as the thing
|
|
246
|
+
* that makes the move visible. Nothing is optimistic: the row moves because
|
|
247
|
+
* Jira said it moved.
|
|
248
|
+
*/
|
|
249
|
+
declare const pendingRow: (model: BoardModel, key: string, to: TransitionTarget) => BoardModel;
|
|
250
|
+
declare const transitionRow: (model: BoardModel, key: string, to: TransitionTarget) => BoardModel;
|
|
251
|
+
/** Clears the marker without moving anything — what a rejected move leaves. */
|
|
252
|
+
declare const settleRow: (model: BoardModel, key: string) => BoardModel;
|
|
211
253
|
type BoardOptions = {
|
|
212
254
|
/** Hand-written tabs from the config file — wins over everything. */
|
|
213
255
|
tabs?: BoardTab$1[];
|
|
@@ -215,6 +257,16 @@ type BoardOptions = {
|
|
|
215
257
|
board?: number;
|
|
216
258
|
limit?: number;
|
|
217
259
|
};
|
|
260
|
+
/**
|
|
261
|
+
* The parents the board holds no row for, in ONE `key in (…)` search — an
|
|
262
|
+
* epic assigned to someone else, or one that fell outside the window while
|
|
263
|
+
* its child stayed open. They are never drawn as rows; they are what lets a
|
|
264
|
+
* fence say whose epic it is, which is the whole of their job here.
|
|
265
|
+
*
|
|
266
|
+
* Empty in, empty out, and no call made: a board of unparented rows must not
|
|
267
|
+
* pay for a search that can only return nothing.
|
|
268
|
+
*/
|
|
269
|
+
declare const parentsOf: (client: JiraClient, rows: BoardRow[]) => Promise<BoardRow[]>;
|
|
218
270
|
/**
|
|
219
271
|
* One assignee-scoped query, bucketed client-side. The board supplies the
|
|
220
272
|
* VOCABULARY, never the population — `getBoardIssues` would drop your
|
|
@@ -223,6 +275,119 @@ type BoardOptions = {
|
|
|
223
275
|
*/
|
|
224
276
|
declare const boardOf: (client: JiraClient, jql: string, { tabs, board, limit }?: BoardOptions) => Promise<BoardModel>;
|
|
225
277
|
|
|
278
|
+
/**
|
|
279
|
+
* One issue, as the detail screen reads it: every field already a string, ADF
|
|
280
|
+
* already Markdown, attachments already located. The view never touches a raw
|
|
281
|
+
* `JiraIssue`, so a host that fakes a `JiraClient` for a test or a screenshot
|
|
282
|
+
* gets a screen it can predict, and one that fetched the issue by some other
|
|
283
|
+
* route can build this shape by hand.
|
|
284
|
+
*/
|
|
285
|
+
type IssueDetail = {
|
|
286
|
+
key: string;
|
|
287
|
+
summary: string;
|
|
288
|
+
status: string;
|
|
289
|
+
type: string;
|
|
290
|
+
assignee: string;
|
|
291
|
+
reporter: string;
|
|
292
|
+
labels: string[];
|
|
293
|
+
/** Jira's priority name, verbatim; absent when the field is off. */
|
|
294
|
+
priority?: string;
|
|
295
|
+
parent?: {
|
|
296
|
+
key: string;
|
|
297
|
+
summary: string;
|
|
298
|
+
};
|
|
299
|
+
url: string;
|
|
300
|
+
description: string;
|
|
301
|
+
comments: {
|
|
302
|
+
id: string;
|
|
303
|
+
author: string;
|
|
304
|
+
created: string;
|
|
305
|
+
body: string;
|
|
306
|
+
}[];
|
|
307
|
+
attachments: LocatedAttachment[];
|
|
308
|
+
};
|
|
309
|
+
/**
|
|
310
|
+
* A transition and where it lands. `to` carries the whole status — id, name
|
|
311
|
+
* and category — because a host that must decide which TAB the move ends in
|
|
312
|
+
* cannot do it from a name: names are per-instance, and matching on one is
|
|
313
|
+
* how a transition table once silently stopped matching anything.
|
|
314
|
+
*/
|
|
315
|
+
type Transition = {
|
|
316
|
+
id: string;
|
|
317
|
+
name: string;
|
|
318
|
+
to: {
|
|
319
|
+
id?: string;
|
|
320
|
+
name: string;
|
|
321
|
+
category?: StatusCategory;
|
|
322
|
+
};
|
|
323
|
+
};
|
|
324
|
+
declare const issueDetailOf: (client: JiraClient, baseUrl: string, key: string) => Promise<IssueDetail>;
|
|
325
|
+
declare const transitionsOf: (client: JiraClient, key: string) => Promise<Transition[]>;
|
|
326
|
+
|
|
327
|
+
type IssueDetailViewProps = {
|
|
328
|
+
issue: IssueDetail;
|
|
329
|
+
height: number;
|
|
330
|
+
width: number;
|
|
331
|
+
onBack: () => void;
|
|
332
|
+
/**
|
|
333
|
+
* The write actions are optional, and a key whose handler is absent is
|
|
334
|
+
* neither bound nor hinted. A host that only READS — cockpit's ticket drill —
|
|
335
|
+
* mounts the same screen jira-cli's TUI does, minus the verbs it has no way
|
|
336
|
+
* to honour, rather than a hint row promising `t` and `c` that do nothing.
|
|
337
|
+
*/
|
|
338
|
+
onTransition?: () => void;
|
|
339
|
+
onComment?: () => void;
|
|
340
|
+
onAssign?: () => void;
|
|
341
|
+
onOpenBrowser?: () => void;
|
|
342
|
+
/**
|
|
343
|
+
* A host that draws its own chrome — a bordered drill frame with the title
|
|
344
|
+
* on top and the hints pinned at the foot — takes the screen in parts and
|
|
345
|
+
* places them. The view then draws neither its title line nor its footer,
|
|
346
|
+
* so nothing appears twice. `height` is still the whole area the host has
|
|
347
|
+
* given the body, frame excluded.
|
|
348
|
+
*/
|
|
349
|
+
frame?: (parts: {
|
|
350
|
+
title: string;
|
|
351
|
+
subtitle: string;
|
|
352
|
+
hints: Hint[];
|
|
353
|
+
body: ReactNode;
|
|
354
|
+
}) => ReactNode;
|
|
355
|
+
/** Colours for the Markdown body; a host passes its own tokens so links, code and headings sit in its palette. */
|
|
356
|
+
theme?: MarkdownTheme;
|
|
357
|
+
};
|
|
358
|
+
declare const IssueDetailView: ({ issue, height, width, onBack, onTransition, onComment, onAssign, onOpenBrowser, frame, theme, }: IssueDetailViewProps) => 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;
|
|
359
|
+
|
|
360
|
+
type IssueBodyProps = {
|
|
361
|
+
client: JiraClient;
|
|
362
|
+
/** Instance URL, as `loadConfig` resolves it; only used to build the browser link. */
|
|
363
|
+
baseUrl: string;
|
|
364
|
+
issueKey: string;
|
|
365
|
+
/**
|
|
366
|
+
* The summary as the host already knows it — from the list row that was
|
|
367
|
+
* opened — so the frame carries the title from the first frame rather than
|
|
368
|
+
* only once the fetch answers. The fetched summary wins once it is here.
|
|
369
|
+
*/
|
|
370
|
+
summary?: string;
|
|
371
|
+
onExit: () => void;
|
|
372
|
+
/** Override the terminal-derived size when the host draws inside a frame. */
|
|
373
|
+
width?: number;
|
|
374
|
+
height?: number;
|
|
375
|
+
/** The host's chrome around the screen — see `IssueDetailView`. */
|
|
376
|
+
frame?: IssueDetailViewProps["frame"];
|
|
377
|
+
theme?: IssueDetailViewProps["theme"];
|
|
378
|
+
};
|
|
379
|
+
/**
|
|
380
|
+
* The issue screen as a host mounts it: hand it a client and a key, and it
|
|
381
|
+
* fetches, draws and reads its own keys until `onExit`. `JenkinsBody`'s shape,
|
|
382
|
+
* for the same reason — a cockpit mounting a ticket over its list should not
|
|
383
|
+
* have to know how an issue is fetched or what the loading states look like.
|
|
384
|
+
*
|
|
385
|
+
* Read-only by construction. The write verbs (`t`, `c`, `a`) exist on
|
|
386
|
+
* `IssueDetailView` for jira-cli's own TUI, which owns the prompts they need;
|
|
387
|
+
* here they are simply not wired, so the hint row does not offer them.
|
|
388
|
+
*/
|
|
389
|
+
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;
|
|
390
|
+
|
|
226
391
|
/** What the board is showing, named in the title row. */
|
|
227
392
|
type BoardScope = {
|
|
228
393
|
kind: "mine";
|
|
@@ -231,8 +396,31 @@ type BoardScope = {
|
|
|
231
396
|
query: string;
|
|
232
397
|
mode: "jql" | "text";
|
|
233
398
|
};
|
|
234
|
-
|
|
399
|
+
/**
|
|
400
|
+
* What a host hangs under rows that is not a Jira issue — cockpit hangs a
|
|
401
|
+
* task's pull requests. The board draws the cursor marker and the tree cell
|
|
402
|
+
* and hands the rest of the line to `render`; `width` is the columns left
|
|
403
|
+
* after those, so the renderer can truncate honestly. `L` never reaches
|
|
404
|
+
* the board's own logic: this package stays Jira-only.
|
|
405
|
+
*/
|
|
406
|
+
type BoardLeaves<L> = {
|
|
407
|
+
under: (row: BoardRow) => L[];
|
|
408
|
+
keyOf: (leaf: L) => string;
|
|
409
|
+
render: (leaf: L, ctx: {
|
|
410
|
+
active: boolean;
|
|
411
|
+
width: number;
|
|
412
|
+
}) => ReactNode;
|
|
413
|
+
onOpen: (leaf: L) => void;
|
|
414
|
+
};
|
|
415
|
+
type IssueBoardProps<L = never> = {
|
|
235
416
|
model: BoardModel;
|
|
417
|
+
leaves?: BoardLeaves<L>;
|
|
418
|
+
/**
|
|
419
|
+
* Parents the board holds no row for, so a fence can say where its parent
|
|
420
|
+
* is. Absent until the lookup lands, which leaves the fence bare rather
|
|
421
|
+
* than guessing.
|
|
422
|
+
*/
|
|
423
|
+
parents?: BoardRow[];
|
|
236
424
|
/** Who the `mine` scope belongs to, for the title row. */
|
|
237
425
|
viewer: string;
|
|
238
426
|
loadedAt: number;
|
|
@@ -289,7 +477,7 @@ type IssueBoardProps = {
|
|
|
289
477
|
* host — only the keys that open and close the layers it pushes itself: the
|
|
290
478
|
* search input and the legend.
|
|
291
479
|
*/
|
|
292
|
-
declare const IssueBoard: ({ model, viewer, loadedAt, scope, showingAll, searchError, width, height, onOpen, onToggleAll, onRefresh, onSearch, onClearSearch, frame, onInputFocus, }: IssueBoardProps) => 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;
|
|
480
|
+
declare const IssueBoard: <L = never>({ model, leaves, parents, viewer, loadedAt, scope, showingAll, searchError, width, height, onOpen, onToggleAll, onRefresh, onSearch, onClearSearch, frame, onInputFocus, }: IssueBoardProps<L>) => 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;
|
|
293
481
|
|
|
294
482
|
type PriorityMarker = {
|
|
295
483
|
/** Always one column wide, so a list filling every row keeps its grid. */
|
|
@@ -314,4 +502,4 @@ declare const priorityMarker: (name: string | null) => PriorityMarker;
|
|
|
314
502
|
declare const MOCK_BOARD_ROWS: BoardRow[];
|
|
315
503
|
declare const mockBoard: () => BoardModel;
|
|
316
504
|
|
|
317
|
-
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,
|
|
505
|
+
export { type Block, type BoardLeaves, 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 Placement, type PriorityGlyph, type PriorityMarker, type StatusCategory, type Stop, type TabSource, type Transition, type TransitionTarget, blockIndexOfStop, blocksFor, boardOf, countsFor, fenceNote, headNote, isStop, issueDetailOf, mockBoard, parentsOf, pendingRow, pillVariantFor, placementOf, priorityGlyph, priorityMarker, relativeAge, rowsInTab, settleRow, stopsOf, subtreeOf, tabLabelOf, tabOf, tabsFromBoard, tabsFromCategories, tabsFromConfig, toBoardRow, transitionRow, transitionsOf, treePrefix, visibleTabs, withBehind };
|
package/dist/index.js
CHANGED
|
@@ -1,12 +1,345 @@
|
|
|
1
1
|
import { MarkdownViewport } from '@kud/ink-markdown';
|
|
2
|
-
import { colors,
|
|
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, jqlEscape, 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 offBoardValue = (tabs) => tabs.tabs.find((t) => t.offBoard)?.value;
|
|
78
|
+
var placementOf = (rows, tabs) => {
|
|
79
|
+
const children = /* @__PURE__ */ new Map();
|
|
80
|
+
for (const row of rows) {
|
|
81
|
+
if (row.container) continue;
|
|
82
|
+
const key = row.parent?.key;
|
|
83
|
+
if (key) children.set(key, [...children.get(key) ?? [], row]);
|
|
84
|
+
}
|
|
85
|
+
const order = new Map(tabs.tabs.map((t, i) => [t.value, i]));
|
|
86
|
+
const placed = /* @__PURE__ */ new Map();
|
|
87
|
+
for (const row of rows) {
|
|
88
|
+
if (!row.container) {
|
|
89
|
+
placed.set(row.key, [tabOf(row, tabs)]);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
const where = [
|
|
93
|
+
...new Set((children.get(row.key) ?? []).map((k) => tabOf(k, tabs)))
|
|
94
|
+
].sort((a, b) => (order.get(a) ?? 0) - (order.get(b) ?? 0));
|
|
95
|
+
placed.set(
|
|
96
|
+
row.key,
|
|
97
|
+
where.length ? where : [offBoardValue(tabs) ?? tabOf(row, tabs)]
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
tabsOf: (key) => placed.get(key) ?? [],
|
|
102
|
+
childrenOf: (key) => children.get(key) ?? []
|
|
103
|
+
};
|
|
104
|
+
};
|
|
105
|
+
var rowsInTab = (rows, tabs, tab, placement = placementOf(rows, tabs)) => rows.filter((r) => placement.tabsOf(r.key).includes(tab));
|
|
106
|
+
var headNote = (row, tabs, tab, placement) => {
|
|
107
|
+
if (!row.container) return void 0;
|
|
108
|
+
const labelOf = (value) => tabs.tabs.find((t) => t.value === value)?.label ?? "";
|
|
109
|
+
const own = tabOf(row, tabs);
|
|
110
|
+
const segments = own === tab ? [] : [`own ${labelOf(own)}`];
|
|
111
|
+
const elsewhere = /* @__PURE__ */ new Map();
|
|
112
|
+
for (const kid of placement.childrenOf(row.key)) {
|
|
113
|
+
const where = tabOf(kid, tabs);
|
|
114
|
+
if (where !== tab) elsewhere.set(where, (elsewhere.get(where) ?? 0) + 1);
|
|
115
|
+
}
|
|
116
|
+
for (const t of tabs.tabs) {
|
|
117
|
+
const n = elsewhere.get(t.value);
|
|
118
|
+
if (n) segments.push(`${n} ${t.label}`);
|
|
119
|
+
}
|
|
120
|
+
return segments.length ? segments.join(" \xB7 ") : void 0;
|
|
121
|
+
};
|
|
122
|
+
var withBehind = (rows, tabs) => {
|
|
123
|
+
const placement = placementOf(rows, tabs);
|
|
124
|
+
const off = offBoardValue(tabs);
|
|
125
|
+
return rows.map((row) => {
|
|
126
|
+
if (!row.container || row.behind !== void 0) return row;
|
|
127
|
+
const kids = placement.childrenOf(row.key);
|
|
128
|
+
if (kids.length === 0) return row;
|
|
129
|
+
const started = kids.some((k) => k.category !== "new");
|
|
130
|
+
const allDone = kids.every((k) => k.category === "done");
|
|
131
|
+
const lags = off !== void 0 && tabOf(row, tabs) === off && started || row.category !== "done" && allDone;
|
|
132
|
+
return lags ? { ...row, behind: "" } : row;
|
|
133
|
+
});
|
|
134
|
+
};
|
|
135
|
+
var countsFor = (rows, tabs, placement = placementOf(rows, tabs)) => {
|
|
136
|
+
const counts = Object.fromEntries(
|
|
137
|
+
tabs.tabs.map((t) => [t.value, 0])
|
|
138
|
+
);
|
|
139
|
+
for (const row of rows)
|
|
140
|
+
for (const tab of placement.tabsOf(row.key))
|
|
141
|
+
counts[tab] = (counts[tab] ?? 0) + 1;
|
|
142
|
+
return counts;
|
|
143
|
+
};
|
|
144
|
+
var visibleTabs = (tabs, counts) => tabs.tabs.filter((t) => !t.offBoard || (counts[t.value] ?? 0) > 0);
|
|
145
|
+
var isStop = (block) => block.kind === "issue" || block.kind === "leaf";
|
|
146
|
+
var stopsOf = (blocks) => blocks.filter(isStop);
|
|
147
|
+
var blocksFor = (rows, under, parents) => {
|
|
148
|
+
const known = new Map((parents ?? []).map((p) => [p.key, p]));
|
|
149
|
+
const withLeaves = (row, depth) => [
|
|
150
|
+
{ kind: "issue", row, depth },
|
|
151
|
+
...(under?.(row) ?? []).map((data) => ({
|
|
152
|
+
kind: "leaf",
|
|
153
|
+
parent: row,
|
|
154
|
+
depth: depth === 0 ? 1 : 2,
|
|
155
|
+
data
|
|
156
|
+
}))
|
|
157
|
+
];
|
|
158
|
+
const hasStructure = rows.some((r) => r.parent || r.container);
|
|
159
|
+
if (!hasStructure) return rows.flatMap((row) => withLeaves(row, 0));
|
|
160
|
+
const heads = new Map(rows.filter((r) => r.container).map((r) => [r.key, r]));
|
|
161
|
+
const children = /* @__PURE__ */ new Map();
|
|
162
|
+
const orphans = [];
|
|
163
|
+
for (const row of rows) {
|
|
164
|
+
if (row.container) continue;
|
|
165
|
+
const key = row.parent?.key;
|
|
166
|
+
if (!key) {
|
|
167
|
+
orphans.push(row);
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
children.set(key, [...children.get(key) ?? [], row]);
|
|
171
|
+
}
|
|
172
|
+
const groups = [];
|
|
173
|
+
for (const [key, head] of heads)
|
|
174
|
+
groups.push([
|
|
175
|
+
...withLeaves(head, 0),
|
|
176
|
+
...(children.get(key) ?? []).flatMap((row) => withLeaves(row, 1))
|
|
177
|
+
]);
|
|
178
|
+
for (const [key, kids] of children) {
|
|
179
|
+
if (heads.has(key)) continue;
|
|
180
|
+
const parent = known.get(key);
|
|
181
|
+
groups.push([
|
|
182
|
+
{
|
|
183
|
+
kind: "fence",
|
|
184
|
+
key,
|
|
185
|
+
summary: kids[0].parent?.summary ?? "",
|
|
186
|
+
...parent ? { parent } : {}
|
|
187
|
+
},
|
|
188
|
+
...kids.flatMap((row) => withLeaves(row, 1))
|
|
189
|
+
]);
|
|
190
|
+
}
|
|
191
|
+
if (orphans.length)
|
|
192
|
+
groups.push([
|
|
193
|
+
...groups.length ? [{ kind: "fence", key: null, summary: "No epic" }] : [],
|
|
194
|
+
...orphans.flatMap((row) => withLeaves(row, 0))
|
|
195
|
+
]);
|
|
196
|
+
return groups.flatMap(
|
|
197
|
+
(group, i) => i === 0 ? group : [{ kind: "gap" }, ...group]
|
|
198
|
+
);
|
|
199
|
+
};
|
|
200
|
+
var firstName = (assignee) => assignee === "unassigned" ? "unassigned" : `@${assignee.split(" ")[0]}`;
|
|
201
|
+
var fenceNote = (parent, tabs, { viewer, mine }) => {
|
|
202
|
+
if (!parent) return { segment: "", behind: false };
|
|
203
|
+
const theirs = parent.assignee !== viewer;
|
|
204
|
+
if (!mine || theirs)
|
|
205
|
+
return { segment: firstName(parent.assignee), behind: false };
|
|
206
|
+
const own = tabOf(parent, tabs);
|
|
207
|
+
return {
|
|
208
|
+
segment: tabs.tabs.find((t) => t.value === own)?.label ?? "",
|
|
209
|
+
behind: parent.behind !== void 0
|
|
210
|
+
};
|
|
211
|
+
};
|
|
212
|
+
var blockIndexOfStop = (blocks, stopIndex) => {
|
|
213
|
+
let seen = -1;
|
|
214
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
215
|
+
if (isStop(blocks[i]) && ++seen === stopIndex) return i;
|
|
216
|
+
}
|
|
217
|
+
return 0;
|
|
218
|
+
};
|
|
219
|
+
var isLastAtDepth = (blocks, i) => {
|
|
220
|
+
const depth = blocks[i].depth;
|
|
221
|
+
for (let j = i + 1; j < blocks.length; j++) {
|
|
222
|
+
const b = blocks[j];
|
|
223
|
+
if (!isStop(b) || b.depth < depth) return true;
|
|
224
|
+
if (b.depth === depth) return false;
|
|
225
|
+
}
|
|
226
|
+
return true;
|
|
227
|
+
};
|
|
228
|
+
var treePrefix = (blocks, i) => {
|
|
229
|
+
const block = blocks[i];
|
|
230
|
+
if (!block || !isStop(block) || block.depth === 0) return "";
|
|
231
|
+
const own = isLastAtDepth(blocks, i) ? "\u2514\u2500 " : "\u251C\u2500 ";
|
|
232
|
+
if (block.depth === 1) return own;
|
|
233
|
+
let parent = i - 1;
|
|
234
|
+
while (parent >= 0 && blocks[parent].depth !== 1) parent -= 1;
|
|
235
|
+
const stem = parent >= 0 && !isLastAtDepth(blocks, parent) ? "\u2502 " : " ";
|
|
236
|
+
return stem + own;
|
|
237
|
+
};
|
|
238
|
+
var subtreeOf = (blocks, i) => {
|
|
239
|
+
const head = blocks[i];
|
|
240
|
+
if (!head || head.kind !== "issue") return head ? [head] : [];
|
|
241
|
+
const out = [head];
|
|
242
|
+
for (let j = i + 1; j < blocks.length; j++) {
|
|
243
|
+
const b = blocks[j];
|
|
244
|
+
if (!isStop(b) || b.depth <= head.depth) break;
|
|
245
|
+
out.push(b);
|
|
246
|
+
}
|
|
247
|
+
return out;
|
|
248
|
+
};
|
|
249
|
+
var priorityGlyph = (name) => {
|
|
250
|
+
const n = (name ?? "").trim().toLowerCase();
|
|
251
|
+
if (/^(highest|blocker|critical|p[01])$/.test(n)) return "\u21C8";
|
|
252
|
+
if (/^(high|urgent|major|p2)$/.test(n)) return "\u2191";
|
|
253
|
+
if (/^(medium|normal|p3)$/.test(n)) return "=";
|
|
254
|
+
if (/^(low|minor|p4)$/.test(n)) return "\u2193";
|
|
255
|
+
if (/^(lowest|trivial|p5)$/.test(n)) return "\u21CA";
|
|
256
|
+
return " ";
|
|
257
|
+
};
|
|
258
|
+
var UNITS = [
|
|
259
|
+
[60, "m"],
|
|
260
|
+
[60, "h"],
|
|
261
|
+
[24, "d"],
|
|
262
|
+
[7, "w"]
|
|
263
|
+
];
|
|
264
|
+
var relativeAge = (iso, now = Date.now()) => {
|
|
265
|
+
const then = Date.parse(iso);
|
|
266
|
+
if (Number.isNaN(then)) return "";
|
|
267
|
+
let value = Math.max(0, Math.round((now - then) / 1e3));
|
|
268
|
+
let unit = "s";
|
|
269
|
+
for (const [size, next] of UNITS) {
|
|
270
|
+
if (value < size) break;
|
|
271
|
+
value = Math.round(value / size);
|
|
272
|
+
unit = next;
|
|
273
|
+
}
|
|
274
|
+
return `${value}${unit}`;
|
|
275
|
+
};
|
|
276
|
+
var pillVariantFor = (row) => {
|
|
277
|
+
if (row.container) return "group";
|
|
278
|
+
const t = row.type.toLowerCase();
|
|
279
|
+
if (t === "bug") return "error";
|
|
280
|
+
if (t === "story") return "info";
|
|
281
|
+
return "muted";
|
|
282
|
+
};
|
|
283
|
+
var tabLabelOf = (to, tabs) => {
|
|
284
|
+
const value = tabOf(
|
|
285
|
+
{
|
|
286
|
+
status: to.name,
|
|
287
|
+
statusId: to.id ?? "",
|
|
288
|
+
category: to.category ?? "unknown"
|
|
289
|
+
},
|
|
290
|
+
tabs
|
|
291
|
+
);
|
|
292
|
+
return tabs.tabs.find((t) => t.value === value)?.label ?? to.name;
|
|
293
|
+
};
|
|
294
|
+
var mapRow = (model, key, f) => ({
|
|
295
|
+
...model,
|
|
296
|
+
rows: model.rows.map((row) => row.key === key ? f(row) : row)
|
|
297
|
+
});
|
|
298
|
+
var pendingRow = (model, key, to) => mapRow(model, key, (row) => ({
|
|
299
|
+
...row,
|
|
300
|
+
pending: tabLabelOf(to, model.tabs)
|
|
301
|
+
}));
|
|
302
|
+
var transitionRow = (model, key, to) => mapRow(model, key, ({ pending: _, ...row }) => ({
|
|
303
|
+
...row,
|
|
304
|
+
status: to.name,
|
|
305
|
+
statusId: to.id ?? "",
|
|
306
|
+
category: to.category ?? "unknown"
|
|
307
|
+
}));
|
|
308
|
+
var settleRow = (model, key) => mapRow(model, key, ({ pending: _, ...row }) => row);
|
|
309
|
+
var parentsOf = async (client, rows) => {
|
|
310
|
+
const held = new Set(rows.map((r) => r.key));
|
|
311
|
+
const missing = [
|
|
312
|
+
...new Set(
|
|
313
|
+
rows.map((r) => r.parent?.key).filter((key) => key !== void 0 && !held.has(key))
|
|
314
|
+
)
|
|
315
|
+
];
|
|
316
|
+
if (missing.length === 0) return [];
|
|
317
|
+
const issues = await client.searchIssues(
|
|
318
|
+
`key in (${missing.map(jqlEscape).join(", ")})`,
|
|
319
|
+
{ limit: missing.length }
|
|
320
|
+
);
|
|
321
|
+
return issues.map(toBoardRow);
|
|
322
|
+
};
|
|
323
|
+
var boardOf = async (client, jql, { tabs, board, limit = 200 } = {}) => {
|
|
324
|
+
const [issues, derived] = await Promise.all([
|
|
325
|
+
client.searchIssues(jql, { limit }),
|
|
326
|
+
tabs?.length ? Promise.resolve(tabsFromConfig(tabs)) : board !== void 0 ? client.getBoardConfiguration(board).then(tabsFromBoard) : Promise.resolve(null)
|
|
327
|
+
]);
|
|
328
|
+
return { rows: issues.map(toBoardRow), tabs: derived ?? tabsFromCategories() };
|
|
329
|
+
};
|
|
330
|
+
var COLOUR = {
|
|
331
|
+
"\u21C8": colors.warning,
|
|
332
|
+
"\u2191": colors.warning,
|
|
333
|
+
"=": colors.muted,
|
|
334
|
+
"\u2193": colors.info,
|
|
335
|
+
"\u21CA": colors.info,
|
|
336
|
+
" ": void 0
|
|
337
|
+
};
|
|
338
|
+
var priorityMarker = (name) => {
|
|
339
|
+
const marker = priorityGlyph(name);
|
|
340
|
+
const color = COLOUR[marker];
|
|
341
|
+
return color ? { marker, color } : { marker };
|
|
342
|
+
};
|
|
10
343
|
var truncate = (s, max) => s.length <= max ? s : `${s.slice(0, Math.max(0, max - 1))}\u2026`;
|
|
11
344
|
var originOf = (a) => a.origins.map(
|
|
12
345
|
(o) => o.kind === "comment" ? `comment by ${o.author ?? "unknown"}` : o.kind === "description" ? "description" : "issue"
|
|
@@ -20,14 +353,52 @@ var humanSize = (bytes) => {
|
|
|
20
353
|
const value = bytes / 1024 ** e;
|
|
21
354
|
return `${e === 0 ? value : value.toFixed(1)}${units[e]}`;
|
|
22
355
|
};
|
|
23
|
-
var
|
|
356
|
+
var ago = (iso) => {
|
|
357
|
+
const rel = relativeAge(iso);
|
|
358
|
+
return rel ? `${rel} ago` : iso;
|
|
359
|
+
};
|
|
360
|
+
var documentOf = (issue) => {
|
|
361
|
+
const parts = [issue.description || "_No description._"];
|
|
362
|
+
if (issue.comments.length > 0)
|
|
363
|
+
parts.push(
|
|
364
|
+
`## Comments (${issue.comments.length})`,
|
|
365
|
+
...issue.comments.map(
|
|
366
|
+
(c) => `**${c.author}** \xB7 ${ago(c.created)}
|
|
24
367
|
|
|
25
|
-
${c.body}`
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
368
|
+
${c.body}`
|
|
369
|
+
)
|
|
370
|
+
);
|
|
371
|
+
if (issue.attachments.length > 0)
|
|
372
|
+
parts.push(
|
|
373
|
+
`## Attachments (${issue.attachments.length})`,
|
|
374
|
+
issue.attachments.map(
|
|
375
|
+
(a) => `- \`${a.filename}\` \u2014 ${a.mimeType}, ${humanSize(a.size)} \xB7 from ${originOf(a)}
|
|
29
376
|
\`jira attachment read ${issue.key} ${a.filename}\``
|
|
30
|
-
).join("\n")
|
|
377
|
+
).join("\n")
|
|
378
|
+
);
|
|
379
|
+
return parts.join("\n\n");
|
|
380
|
+
};
|
|
381
|
+
var FactsLine = ({ issue }) => {
|
|
382
|
+
const priority = priorityMarker(issue.priority ?? null);
|
|
383
|
+
const lead = priority.marker !== " " && priority.marker !== "=" ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
384
|
+
/* @__PURE__ */ jsx(Text, { color: priority.color, children: priority.marker }),
|
|
385
|
+
/* @__PURE__ */ jsxs(Text, { children: [
|
|
386
|
+
" ",
|
|
387
|
+
issue.priority
|
|
388
|
+
] })
|
|
389
|
+
] }) : null;
|
|
390
|
+
const plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
|
|
391
|
+
const facts = [
|
|
392
|
+
`by ${issue.reporter}`,
|
|
393
|
+
...issue.labels.length > 0 ? [`labels ${issue.labels.join(", ")}`] : [],
|
|
394
|
+
plural(issue.comments.length, "comment"),
|
|
395
|
+
...issue.attachments.length > 0 ? [plural(issue.attachments.length, "attachment")] : []
|
|
396
|
+
];
|
|
397
|
+
return /* @__PURE__ */ jsxs(Box, { children: [
|
|
398
|
+
lead,
|
|
399
|
+
/* @__PURE__ */ jsx(Text, { color: colors.muted, children: (lead ? " \xB7 " : "") + facts.join(" \xB7 ") })
|
|
400
|
+
] });
|
|
401
|
+
};
|
|
31
402
|
var IssueDetailView = ({
|
|
32
403
|
issue,
|
|
33
404
|
height,
|
|
@@ -40,16 +411,6 @@ var IssueDetailView = ({
|
|
|
40
411
|
frame,
|
|
41
412
|
theme
|
|
42
413
|
}) => {
|
|
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
414
|
const [scroll, setScroll] = useState(0);
|
|
54
415
|
useInput((input, key) => {
|
|
55
416
|
if (input === "t" && onTransition) return onTransition();
|
|
@@ -59,12 +420,11 @@ var IssueDetailView = ({
|
|
|
59
420
|
if (key.downArrow || input === "j") setScroll((s) => s + 1);
|
|
60
421
|
if (key.upArrow || input === "k") setScroll((s) => Math.max(0, s - 1));
|
|
61
422
|
});
|
|
62
|
-
const source =
|
|
63
|
-
const keyValueRows =
|
|
64
|
-
const chromeLines = keyValueRows + 1 +
|
|
423
|
+
const source = documentOf(issue);
|
|
424
|
+
const keyValueRows = 2 + (issue.parent ? 1 : 0) + (frame ? 0 : 1);
|
|
425
|
+
const chromeLines = keyValueRows + 1 + 1 + (frame ? 0 : 4);
|
|
65
426
|
const bodyHeight = Math.max(3, height - chromeLines);
|
|
66
427
|
const hints = [
|
|
67
|
-
["\u2190\u2192", "section"],
|
|
68
428
|
["\u2191\u2193", "scroll"],
|
|
69
429
|
...onTransition ? [["t", "move"]] : [],
|
|
70
430
|
...onComment ? [["c", "comment"]] : [],
|
|
@@ -74,20 +434,27 @@ var IssueDetailView = ({
|
|
|
74
434
|
];
|
|
75
435
|
const body = /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
76
436
|
/* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
437
|
+
/* @__PURE__ */ jsx(KeyValue, { label: "status", value: /* @__PURE__ */ jsx(Badge, { children: issue.status }) }),
|
|
438
|
+
frame ? null : /* @__PURE__ */ jsx(KeyValue, { label: "type", value: issue.type }),
|
|
439
|
+
/* @__PURE__ */ jsx(
|
|
440
|
+
KeyValue,
|
|
441
|
+
{
|
|
442
|
+
label: "assignee",
|
|
443
|
+
value: issue.assignee === "unassigned" ? /* @__PURE__ */ jsx(Text, { color: colors.muted, children: "unassigned" }) : issue.assignee
|
|
444
|
+
}
|
|
445
|
+
),
|
|
77
446
|
issue.parent && /* @__PURE__ */ jsx(
|
|
78
447
|
KeyValue,
|
|
79
448
|
{
|
|
80
449
|
label: "parent",
|
|
81
|
-
value:
|
|
450
|
+
value: /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
451
|
+
/* @__PURE__ */ jsx(Text, { color: colors.secondary, children: issue.parent.key }),
|
|
452
|
+
/* @__PURE__ */ jsx(Text, { children: ` ${truncate(issue.parent.summary, 60)}` })
|
|
453
|
+
] })
|
|
82
454
|
}
|
|
83
455
|
),
|
|
84
|
-
/* @__PURE__ */ jsx(
|
|
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(", ") })
|
|
456
|
+
/* @__PURE__ */ jsx(FactsLine, { issue })
|
|
89
457
|
] }),
|
|
90
|
-
/* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Tabs, { items: tabs, active: active ?? "description" }) }),
|
|
91
458
|
/* @__PURE__ */ jsx(Box, { marginTop: 1, height: bodyHeight, overflow: "hidden", children: /* @__PURE__ */ jsx(
|
|
92
459
|
MarkdownViewport,
|
|
93
460
|
{
|
|
@@ -129,13 +496,14 @@ var issueDetailOf = async (client, baseUrl, key) => {
|
|
|
129
496
|
assignee: f.assignee?.displayName ?? "unassigned",
|
|
130
497
|
reporter: f.reporter?.displayName ?? "\u2014",
|
|
131
498
|
labels: f.labels ?? [],
|
|
499
|
+
...f.priority?.name ? { priority: f.priority.name } : {},
|
|
132
500
|
parent: f.parent ? { key: f.parent.key, summary: f.parent.fields?.summary ?? "" } : void 0,
|
|
133
501
|
url: `${baseUrl}/browse/${issue.key}`,
|
|
134
502
|
description: adfToMarkdown(f.description, filenameOf),
|
|
135
503
|
comments: (f.comment?.comments ?? []).map((c) => ({
|
|
136
504
|
id: c.id,
|
|
137
505
|
author: c.author?.displayName ?? "unknown",
|
|
138
|
-
created: c.created
|
|
506
|
+
created: c.created ?? "",
|
|
139
507
|
body: adfToMarkdown(c.body, filenameOf)
|
|
140
508
|
})),
|
|
141
509
|
attachments
|
|
@@ -144,7 +512,11 @@ var issueDetailOf = async (client, baseUrl, key) => {
|
|
|
144
512
|
var transitionsOf = async (client, key) => (await client.getTransitions(key)).transitions.map((t) => ({
|
|
145
513
|
id: t.id,
|
|
146
514
|
name: t.name,
|
|
147
|
-
to:
|
|
515
|
+
to: {
|
|
516
|
+
...t.to?.id ? { id: t.to.id } : {},
|
|
517
|
+
name: t.to?.name ?? t.name,
|
|
518
|
+
...t.to?.statusCategory?.key ? { category: categoryOf(t.to.statusCategory.key) } : {}
|
|
519
|
+
}
|
|
148
520
|
}));
|
|
149
521
|
var useTermSize = () => {
|
|
150
522
|
const { stdout } = useStdout();
|
|
@@ -169,6 +541,7 @@ var IssueBody = ({
|
|
|
169
541
|
client,
|
|
170
542
|
baseUrl,
|
|
171
543
|
issueKey,
|
|
544
|
+
summary,
|
|
172
545
|
onExit,
|
|
173
546
|
width,
|
|
174
547
|
height,
|
|
@@ -192,7 +565,7 @@ var IssueBody = ({
|
|
|
192
565
|
}, [client, baseUrl, issueKey]);
|
|
193
566
|
const framed = (body) => frame ? frame({
|
|
194
567
|
title: issueKey,
|
|
195
|
-
subtitle: "",
|
|
568
|
+
subtitle: summary ?? "",
|
|
196
569
|
hints: [["\u232B", "back"]],
|
|
197
570
|
body
|
|
198
571
|
}) : body;
|
|
@@ -219,192 +592,14 @@ var IssueBody = ({
|
|
|
219
592
|
}
|
|
220
593
|
);
|
|
221
594
|
};
|
|
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
595
|
var CHROME = 6;
|
|
596
|
+
var ROW_CHROME = 7;
|
|
402
597
|
var LEGEND = [
|
|
403
598
|
["\u21C8 \u2191", "priority above the default"],
|
|
404
599
|
["=", "default priority"],
|
|
405
600
|
["\u2193 \u21CA", "priority below the default"],
|
|
406
601
|
["\u2500\u2500 epic \u2500\u2500", "a parent not in this tab; its rows hang beneath"],
|
|
407
|
-
["\u2514\u2500", "a row under the container above it"],
|
|
602
|
+
["\u251C\u2500 \u2514\u2500", "a row under the container above it; \u2514\u2500 is the last"],
|
|
408
603
|
["story \xB7 bug \xB7 task", "issue type"],
|
|
409
604
|
["2d", "time since last update"]
|
|
410
605
|
];
|
|
@@ -424,8 +619,21 @@ var tabLegendLine = (model) => model.tabs.source === "categories" ? [
|
|
|
424
619
|
"To do \xB7 In progress \xB7 Done",
|
|
425
620
|
"status category \u2014 not any one board's column"
|
|
426
621
|
] : model.tabs.source === "board" ? ["tabs", "this board's own columns, matched by status id"] : ["tabs", "your config file's tabs"];
|
|
622
|
+
var RowNote = ({ row, note }) => {
|
|
623
|
+
if (row.pending)
|
|
624
|
+
return /* @__PURE__ */ jsx(Box, { flexShrink: 0, paddingLeft: 1, children: /* @__PURE__ */ jsx(Text, { color: colors.accent, children: `\u22EF \u2192 ${row.pending}` }) });
|
|
625
|
+
const behind = row.behind !== void 0;
|
|
626
|
+
const dim = [row.behind || "", note ?? ""].filter(Boolean).join(" \xB7 ");
|
|
627
|
+
if (!behind && !dim) return null;
|
|
628
|
+
return /* @__PURE__ */ jsxs(Box, { flexShrink: 0, paddingLeft: 1, children: [
|
|
629
|
+
behind ? /* @__PURE__ */ jsx(Text, { color: colors.warning, bold: true, children: "behind" }) : null,
|
|
630
|
+
dim ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: (behind ? " \xB7 " : "") + dim }) : null
|
|
631
|
+
] });
|
|
632
|
+
};
|
|
427
633
|
var IssueBoard = ({
|
|
428
634
|
model,
|
|
635
|
+
leaves,
|
|
636
|
+
parents,
|
|
429
637
|
viewer,
|
|
430
638
|
loadedAt,
|
|
431
639
|
scope,
|
|
@@ -463,7 +671,11 @@ var IssueBoard = ({
|
|
|
463
671
|
(r) => `${r.key} ${r.summary}`.toLowerCase().includes(words)
|
|
464
672
|
);
|
|
465
673
|
}, [model.rows, search]);
|
|
466
|
-
const
|
|
674
|
+
const placement = useMemo(
|
|
675
|
+
() => placementOf(narrowed, model.tabs),
|
|
676
|
+
[narrowed, model.tabs]
|
|
677
|
+
);
|
|
678
|
+
const counts = countsFor(narrowed, model.tabs, placement);
|
|
467
679
|
const shown = visibleTabs(model.tabs, counts);
|
|
468
680
|
const tabItems = shown.map((t) => ({
|
|
469
681
|
value: t.value,
|
|
@@ -477,18 +689,26 @@ var IssueBoard = ({
|
|
|
477
689
|
isActive: listFocused
|
|
478
690
|
});
|
|
479
691
|
const tab = active ?? initial ?? "";
|
|
692
|
+
const under = leaves?.under;
|
|
480
693
|
const blocks = useMemo(
|
|
481
|
-
() => blocksFor(
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
694
|
+
() => blocksFor(
|
|
695
|
+
rowsInTab(narrowed, model.tabs, tab, placement),
|
|
696
|
+
under,
|
|
697
|
+
parents
|
|
698
|
+
),
|
|
699
|
+
[narrowed, model.tabs, tab, placement, under, parents]
|
|
486
700
|
);
|
|
487
|
-
const
|
|
701
|
+
const stops = stopsOf(blocks);
|
|
702
|
+
const { cursor, setCursor } = useListCursor(stops.length, {
|
|
488
703
|
vimKeys: true,
|
|
489
704
|
isActive: listFocused
|
|
490
705
|
});
|
|
491
706
|
useEffect(() => setCursor(0), [tab, setCursor]);
|
|
707
|
+
const openStop = (stop) => {
|
|
708
|
+
if (!stop) return;
|
|
709
|
+
if (stop.kind === "issue") onOpen(stop.row.key);
|
|
710
|
+
else leaves?.onOpen(stop.data);
|
|
711
|
+
};
|
|
492
712
|
useInput((input, key) => {
|
|
493
713
|
if (search.open) {
|
|
494
714
|
if (key.escape) setSearch((s) => ({ ...s, open: false }));
|
|
@@ -503,7 +723,7 @@ var IssueBoard = ({
|
|
|
503
723
|
if (input === "?" || key.escape) setLegend(false);
|
|
504
724
|
return;
|
|
505
725
|
}
|
|
506
|
-
if (key.return
|
|
726
|
+
if (key.return) openStop(stops[cursor]);
|
|
507
727
|
if (input === "/")
|
|
508
728
|
setSearch((s) => ({
|
|
509
729
|
...s,
|
|
@@ -518,7 +738,7 @@ var IssueBoard = ({
|
|
|
518
738
|
const searching = search.open || scope.kind === "search";
|
|
519
739
|
const chrome = CHROME + (searching ? 1 : 0) + (searchError ? 1 : 0);
|
|
520
740
|
const size = Math.max(3, height - chrome);
|
|
521
|
-
const focus =
|
|
741
|
+
const focus = blockIndexOfStop(blocks, cursor);
|
|
522
742
|
const { start, end } = windowFor(focus, blocks.length, size);
|
|
523
743
|
const keyWidth = Math.max(8, ...narrowed.map((r) => r.key.length));
|
|
524
744
|
const typeWidth = Math.max(
|
|
@@ -589,20 +809,61 @@ var IssueBoard = ({
|
|
|
589
809
|
/* @__PURE__ */ jsx(Box, { flexDirection: "column", marginTop: 1, height: size, paddingRight: 1, children: legend ? [...LEGEND, tabLegendLine(model)].map(([glyph, meaning]) => /* @__PURE__ */ jsxs(Box, { paddingLeft: 4, children: [
|
|
590
810
|
/* @__PURE__ */ jsx(Box, { width: 28, flexShrink: 0, children: /* @__PURE__ */ jsx(Text, { color: colors.accent, children: glyph }) }),
|
|
591
811
|
/* @__PURE__ */ jsx(Text, { dimColor: true, children: meaning })
|
|
592
|
-
] }, glyph)) :
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
812
|
+
] }, glyph)) : stops.length === 0 ? /* @__PURE__ */ jsx(Box, { paddingLeft: 4, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: emptyHint() }) }) : blocks.slice(start, end).map((block, i) => {
|
|
813
|
+
const at = start + i;
|
|
814
|
+
const focused = at === focus;
|
|
815
|
+
const prefix = treePrefix(blocks, at);
|
|
816
|
+
return block.kind === "gap" ? /* @__PURE__ */ jsx(Text, { children: " " }, `gap:${at}`) : block.kind === "fence" ? (() => {
|
|
817
|
+
const { segment, behind } = fenceNote(
|
|
818
|
+
block.parent,
|
|
819
|
+
model.tabs,
|
|
820
|
+
{
|
|
821
|
+
viewer,
|
|
822
|
+
mine: scope.kind === "mine"
|
|
823
|
+
}
|
|
824
|
+
);
|
|
825
|
+
const tail = [behind ? "behind" : "", segment].filter(Boolean).join(" \xB7 ");
|
|
826
|
+
const label = block.key ? `${block.summary} \xB7 ${block.key}` : block.summary;
|
|
827
|
+
return /* @__PURE__ */ jsxs(Box, { paddingLeft: 4, children: [
|
|
828
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: rule(
|
|
829
|
+
label,
|
|
830
|
+
width - ROW_CHROME - (tail ? tail.length + 1 : 0)
|
|
831
|
+
) }),
|
|
832
|
+
tail ? /* @__PURE__ */ jsxs(Box, { flexShrink: 0, paddingLeft: 1, children: [
|
|
833
|
+
behind ? /* @__PURE__ */ jsx(Text, { color: colors.warning, bold: true, children: "behind" }) : null,
|
|
834
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: (behind ? " \xB7 " : "") + segment })
|
|
835
|
+
] }) : null
|
|
836
|
+
] }, `f:${block.key ?? "none"}`);
|
|
837
|
+
})() : block.kind === "leaf" ? /* @__PURE__ */ jsxs(
|
|
838
|
+
SelectableRow,
|
|
839
|
+
{
|
|
840
|
+
active: focused,
|
|
841
|
+
children: [
|
|
842
|
+
/* @__PURE__ */ jsx(Box, { flexShrink: 0, width: prefix.length, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: prefix }) }),
|
|
843
|
+
leaves?.render(block.data, {
|
|
844
|
+
active: focused,
|
|
845
|
+
width: width - ROW_CHROME - prefix.length
|
|
846
|
+
})
|
|
847
|
+
]
|
|
848
|
+
},
|
|
849
|
+
`leaf:${block.parent.key}:${leaves?.keyOf(block.data)}`
|
|
850
|
+
) : /* @__PURE__ */ jsxs(SelectableRow, { active: focused, children: [
|
|
851
|
+
prefix ? /* @__PURE__ */ jsx(Box, { flexShrink: 0, width: prefix.length, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: prefix }) }) : null,
|
|
598
852
|
/* @__PURE__ */ jsx(Box, { flexShrink: 0, width: 2, children: /* @__PURE__ */ jsx(Text, { color: priorityMarker(block.row.priority).color, children: priorityMarker(block.row.priority).marker }) }),
|
|
599
|
-
/* @__PURE__ */ jsx(Box, { flexShrink: 0, width: keyWidth + 2, children: /* @__PURE__ */ jsx(Text, { color: colors.accent, bold:
|
|
853
|
+
/* @__PURE__ */ jsx(Box, { flexShrink: 0, width: keyWidth + 2, children: /* @__PURE__ */ jsx(Text, { color: colors.accent, bold: focused, children: block.row.key }) }),
|
|
600
854
|
/* @__PURE__ */ jsx(Box, { flexShrink: 0, width: typeWidth + 2, children: /* @__PURE__ */ jsx(Pill, { tone: "soft", variant: pillVariantFor(block.row), children: block.row.type.toLowerCase() }) }),
|
|
601
|
-
/* @__PURE__ */ jsx(Box, { flexGrow: 1, children: /* @__PURE__ */ jsx(Text, { wrap: "truncate-end", bold:
|
|
855
|
+
/* @__PURE__ */ jsx(Box, { flexGrow: 1, children: /* @__PURE__ */ jsx(Text, { wrap: "truncate-end", bold: focused, children: block.row.summary }) }),
|
|
856
|
+
/* @__PURE__ */ jsx(
|
|
857
|
+
RowNote,
|
|
858
|
+
{
|
|
859
|
+
row: block.row,
|
|
860
|
+
note: headNote(block.row, model.tabs, tab, placement)
|
|
861
|
+
}
|
|
862
|
+
),
|
|
602
863
|
/* @__PURE__ */ jsx(Box, { flexShrink: 0, width: 5, justifyContent: "flex-end", children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: relativeAge(block.row.updated, now) }) })
|
|
603
|
-
] }, block.row.key)
|
|
604
|
-
) }),
|
|
605
|
-
/* @__PURE__ */ jsx(Box, { marginTop: 1, paddingLeft: 2, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children:
|
|
864
|
+
] }, block.row.key);
|
|
865
|
+
}) }),
|
|
866
|
+
/* @__PURE__ */ jsx(Box, { marginTop: 1, paddingLeft: 2, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: stops.length ? `${cursor + 1}/${stops.length}` : " " }) })
|
|
606
867
|
] });
|
|
607
868
|
if (frame) return frame({ facts, title, hints, body });
|
|
608
869
|
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
@@ -711,4 +972,4 @@ var mockBoard = () => ({
|
|
|
711
972
|
tabs: tabsFromCategories()
|
|
712
973
|
});
|
|
713
974
|
|
|
714
|
-
export { IssueBoard, IssueBody, IssueDetailView, MOCK_BOARD_ROWS, OFF_BOARD,
|
|
975
|
+
export { IssueBoard, IssueBody, IssueDetailView, MOCK_BOARD_ROWS, OFF_BOARD, blockIndexOfStop, blocksFor, boardOf, countsFor, fenceNote, headNote, isStop, issueDetailOf, mockBoard, parentsOf, pendingRow, pillVariantFor, placementOf, priorityGlyph, priorityMarker, relativeAge, rowsInTab, settleRow, stopsOf, subtreeOf, tabLabelOf, tabOf, tabsFromBoard, tabsFromCategories, tabsFromConfig, toBoardRow, transitionRow, transitionsOf, treePrefix, visibleTabs, withBehind };
|
package/package.json
CHANGED