@kud/jira-ink 0.8.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 -112
- package/dist/index.js +265 -40
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -2,110 +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
|
-
/** Jira's priority name, verbatim; absent when the field is off. */
|
|
23
|
-
priority?: string;
|
|
24
|
-
parent?: {
|
|
25
|
-
key: string;
|
|
26
|
-
summary: string;
|
|
27
|
-
};
|
|
28
|
-
url: string;
|
|
29
|
-
description: string;
|
|
30
|
-
comments: {
|
|
31
|
-
id: string;
|
|
32
|
-
author: string;
|
|
33
|
-
created: string;
|
|
34
|
-
body: string;
|
|
35
|
-
}[];
|
|
36
|
-
attachments: LocatedAttachment[];
|
|
37
|
-
};
|
|
38
|
-
type Transition = {
|
|
39
|
-
id: string;
|
|
40
|
-
name: string;
|
|
41
|
-
to: string;
|
|
42
|
-
};
|
|
43
|
-
declare const issueDetailOf: (client: JiraClient, baseUrl: string, key: string) => Promise<IssueDetail>;
|
|
44
|
-
declare const transitionsOf: (client: JiraClient, key: string) => Promise<Transition[]>;
|
|
45
|
-
|
|
46
|
-
type IssueDetailViewProps = {
|
|
47
|
-
issue: IssueDetail;
|
|
48
|
-
height: number;
|
|
49
|
-
width: number;
|
|
50
|
-
onBack: () => void;
|
|
51
|
-
/**
|
|
52
|
-
* The write actions are optional, and a key whose handler is absent is
|
|
53
|
-
* neither bound nor hinted. A host that only READS — cockpit's ticket drill —
|
|
54
|
-
* mounts the same screen jira-cli's TUI does, minus the verbs it has no way
|
|
55
|
-
* to honour, rather than a hint row promising `t` and `c` that do nothing.
|
|
56
|
-
*/
|
|
57
|
-
onTransition?: () => void;
|
|
58
|
-
onComment?: () => void;
|
|
59
|
-
onAssign?: () => void;
|
|
60
|
-
onOpenBrowser?: () => void;
|
|
61
|
-
/**
|
|
62
|
-
* A host that draws its own chrome — a bordered drill frame with the title
|
|
63
|
-
* on top and the hints pinned at the foot — takes the screen in parts and
|
|
64
|
-
* places them. The view then draws neither its title line nor its footer,
|
|
65
|
-
* so nothing appears twice. `height` is still the whole area the host has
|
|
66
|
-
* given the body, frame excluded.
|
|
67
|
-
*/
|
|
68
|
-
frame?: (parts: {
|
|
69
|
-
title: string;
|
|
70
|
-
subtitle: string;
|
|
71
|
-
hints: Hint[];
|
|
72
|
-
body: ReactNode;
|
|
73
|
-
}) => ReactNode;
|
|
74
|
-
/** Colours for the Markdown body; a host passes its own tokens so links, code and headings sit in its palette. */
|
|
75
|
-
theme?: MarkdownTheme;
|
|
76
|
-
};
|
|
77
|
-
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;
|
|
78
|
-
|
|
79
|
-
type IssueBodyProps = {
|
|
80
|
-
client: JiraClient;
|
|
81
|
-
/** Instance URL, as `loadConfig` resolves it; only used to build the browser link. */
|
|
82
|
-
baseUrl: string;
|
|
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;
|
|
90
|
-
onExit: () => void;
|
|
91
|
-
/** Override the terminal-derived size when the host draws inside a frame. */
|
|
92
|
-
width?: number;
|
|
93
|
-
height?: number;
|
|
94
|
-
/** The host's chrome around the screen — see `IssueDetailView`. */
|
|
95
|
-
frame?: IssueDetailViewProps["frame"];
|
|
96
|
-
theme?: IssueDetailViewProps["theme"];
|
|
97
|
-
};
|
|
98
|
-
/**
|
|
99
|
-
* The issue screen as a host mounts it: hand it a client and a key, and it
|
|
100
|
-
* fetches, draws and reads its own keys until `onExit`. `JenkinsBody`'s shape,
|
|
101
|
-
* for the same reason — a cockpit mounting a ticket over its list should not
|
|
102
|
-
* have to know how an issue is fetched or what the loading states look like.
|
|
103
|
-
*
|
|
104
|
-
* Read-only by construction. The write verbs (`t`, `c`, `a`) exist on
|
|
105
|
-
* `IssueDetailView` for jira-cli's own TUI, which owns the prompts they need;
|
|
106
|
-
* here they are simply not wired, so the hint row does not offer them.
|
|
107
|
-
*/
|
|
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;
|
|
5
|
+
import { BoardTab as BoardTab$1, JiraClient, JiraBoardConfiguration, JiraIssue, LocatedAttachment, SearchMode } from '@kud/jira';
|
|
109
6
|
|
|
110
7
|
/** Jira's own three, plus `unknown` for a status that arrives without one. */
|
|
111
8
|
type StatusCategory = "new" | "indeterminate" | "done" | "unknown";
|
|
@@ -132,6 +29,22 @@ type BoardRow = {
|
|
|
132
29
|
assignee: string;
|
|
133
30
|
/** ISO timestamp, kept whole so the view can draw a relative age. */
|
|
134
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;
|
|
135
48
|
};
|
|
136
49
|
declare const toBoardRow: (issue: JiraIssue) => BoardRow;
|
|
137
50
|
/**
|
|
@@ -165,7 +78,44 @@ declare const tabsFromBoard: (configuration: JiraBoardConfiguration) => BoardTab
|
|
|
165
78
|
declare const tabsFromCategories: () => BoardTabs;
|
|
166
79
|
/** The tab a row belongs to. Nothing ever falls out: unclaimed rows go Off board, or In progress on a category board. */
|
|
167
80
|
declare const tabOf: (row: BoardRow, { tabs }: BoardTabs) => string;
|
|
168
|
-
|
|
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>;
|
|
169
119
|
/** Tabs worth drawing: every configured one, plus Off board only when it holds something. */
|
|
170
120
|
declare const visibleTabs: (tabs: BoardTabs, counts: Record<string, number>) => BoardTab[];
|
|
171
121
|
/**
|
|
@@ -176,21 +126,85 @@ declare const visibleTabs: (tabs: BoardTabs, counts: Record<string, number>) =>
|
|
|
176
126
|
* with no parent — and each group after the first is preceded by a blank
|
|
177
127
|
* line. A tab where nothing has a parent and nothing is a container degrades
|
|
178
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.
|
|
179
136
|
*/
|
|
180
|
-
type Block = {
|
|
137
|
+
type Block<L = never> = {
|
|
181
138
|
kind: "gap";
|
|
182
139
|
} | {
|
|
183
140
|
kind: "fence";
|
|
184
141
|
key: string | null;
|
|
185
142
|
summary: string;
|
|
143
|
+
parent?: BoardRow;
|
|
186
144
|
} | {
|
|
187
145
|
kind: "issue";
|
|
188
146
|
row: BoardRow;
|
|
189
147
|
depth: 0 | 1;
|
|
148
|
+
} | {
|
|
149
|
+
kind: "leaf";
|
|
150
|
+
parent: BoardRow;
|
|
151
|
+
depth: 1 | 2;
|
|
152
|
+
data: L;
|
|
190
153
|
};
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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;
|
|
189
|
+
};
|
|
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>[];
|
|
194
208
|
type PriorityGlyph = "⇈" | "↑" | "=" | "↓" | "⇊" | " ";
|
|
195
209
|
/**
|
|
196
210
|
* Rank against the scheme's default rung, in Jira's own arrow grammar: two
|
|
@@ -216,6 +230,26 @@ type BoardModel = {
|
|
|
216
230
|
rows: BoardRow[];
|
|
217
231
|
tabs: BoardTabs;
|
|
218
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;
|
|
219
253
|
type BoardOptions = {
|
|
220
254
|
/** Hand-written tabs from the config file — wins over everything. */
|
|
221
255
|
tabs?: BoardTab$1[];
|
|
@@ -223,6 +257,16 @@ type BoardOptions = {
|
|
|
223
257
|
board?: number;
|
|
224
258
|
limit?: number;
|
|
225
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[]>;
|
|
226
270
|
/**
|
|
227
271
|
* One assignee-scoped query, bucketed client-side. The board supplies the
|
|
228
272
|
* VOCABULARY, never the population — `getBoardIssues` would drop your
|
|
@@ -231,6 +275,119 @@ type BoardOptions = {
|
|
|
231
275
|
*/
|
|
232
276
|
declare const boardOf: (client: JiraClient, jql: string, { tabs, board, limit }?: BoardOptions) => Promise<BoardModel>;
|
|
233
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
|
+
|
|
234
391
|
/** What the board is showing, named in the title row. */
|
|
235
392
|
type BoardScope = {
|
|
236
393
|
kind: "mine";
|
|
@@ -239,8 +396,31 @@ type BoardScope = {
|
|
|
239
396
|
query: string;
|
|
240
397
|
mode: "jql" | "text";
|
|
241
398
|
};
|
|
242
|
-
|
|
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> = {
|
|
243
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[];
|
|
244
424
|
/** Who the `mine` scope belongs to, for the title row. */
|
|
245
425
|
viewer: string;
|
|
246
426
|
loadedAt: number;
|
|
@@ -297,7 +477,7 @@ type IssueBoardProps = {
|
|
|
297
477
|
* host — only the keys that open and close the layers it pushes itself: the
|
|
298
478
|
* search input and the legend.
|
|
299
479
|
*/
|
|
300
|
-
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;
|
|
301
481
|
|
|
302
482
|
type PriorityMarker = {
|
|
303
483
|
/** Always one column wide, so a list filling every row keeps its grid. */
|
|
@@ -322,4 +502,4 @@ declare const priorityMarker: (name: string | null) => PriorityMarker;
|
|
|
322
502
|
declare const MOCK_BOARD_ROWS: BoardRow[];
|
|
323
503
|
declare const mockBoard: () => BoardModel;
|
|
324
504
|
|
|
325
|
-
export { type Block, type BoardModel, type BoardOptions, type BoardRow, type BoardScope, type BoardTab, type BoardTabs, IssueBoard, type IssueBoardProps, IssueBody, type IssueBodyProps, type IssueDetail, IssueDetailView, type IssueDetailViewProps, MOCK_BOARD_ROWS, OFF_BOARD, type PriorityGlyph, type PriorityMarker, type StatusCategory, type TabSource, type Transition,
|
|
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
|
@@ -2,7 +2,7 @@ import { MarkdownViewport } from '@kud/ink-markdown';
|
|
|
2
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
|
+
import { isContainerType, jqlEscape, locateAttachments, adfToMarkdown, looksLikeJql } from '@kud/jira';
|
|
6
6
|
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
7
7
|
import { exec } from 'child_process';
|
|
8
8
|
|
|
@@ -74,19 +74,89 @@ var tabOf = (row, { tabs }) => {
|
|
|
74
74
|
if (byCategory) return byCategory.value;
|
|
75
75
|
return tabs.find((t) => t.offBoard)?.value ?? "doing";
|
|
76
76
|
};
|
|
77
|
-
var
|
|
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)) => {
|
|
78
136
|
const counts = Object.fromEntries(
|
|
79
137
|
tabs.tabs.map((t) => [t.value, 0])
|
|
80
138
|
);
|
|
81
139
|
for (const row of rows)
|
|
82
|
-
|
|
140
|
+
for (const tab of placement.tabsOf(row.key))
|
|
141
|
+
counts[tab] = (counts[tab] ?? 0) + 1;
|
|
83
142
|
return counts;
|
|
84
143
|
};
|
|
85
144
|
var visibleTabs = (tabs, counts) => tabs.tabs.filter((t) => !t.offBoard || (counts[t.value] ?? 0) > 0);
|
|
86
|
-
var
|
|
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
|
+
];
|
|
87
158
|
const hasStructure = rows.some((r) => r.parent || r.container);
|
|
88
|
-
if (!hasStructure)
|
|
89
|
-
return rows.map((row) => ({ kind: "issue", row, depth: 0 }));
|
|
159
|
+
if (!hasStructure) return rows.flatMap((row) => withLeaves(row, 0));
|
|
90
160
|
const heads = new Map(rows.filter((r) => r.container).map((r) => [r.key, r]));
|
|
91
161
|
const children = /* @__PURE__ */ new Map();
|
|
92
162
|
const orphans = [];
|
|
@@ -102,36 +172,80 @@ var blocksFor = (rows) => {
|
|
|
102
172
|
const groups = [];
|
|
103
173
|
for (const [key, head] of heads)
|
|
104
174
|
groups.push([
|
|
105
|
-
|
|
106
|
-
...(children.get(key) ?? []).
|
|
107
|
-
kind: "issue",
|
|
108
|
-
row,
|
|
109
|
-
depth: 1
|
|
110
|
-
}))
|
|
175
|
+
...withLeaves(head, 0),
|
|
176
|
+
...(children.get(key) ?? []).flatMap((row) => withLeaves(row, 1))
|
|
111
177
|
]);
|
|
112
178
|
for (const [key, kids] of children) {
|
|
113
179
|
if (heads.has(key)) continue;
|
|
180
|
+
const parent = known.get(key);
|
|
114
181
|
groups.push([
|
|
115
|
-
{
|
|
116
|
-
|
|
182
|
+
{
|
|
183
|
+
kind: "fence",
|
|
184
|
+
key,
|
|
185
|
+
summary: kids[0].parent?.summary ?? "",
|
|
186
|
+
...parent ? { parent } : {}
|
|
187
|
+
},
|
|
188
|
+
...kids.flatMap((row) => withLeaves(row, 1))
|
|
117
189
|
]);
|
|
118
190
|
}
|
|
119
191
|
if (orphans.length)
|
|
120
192
|
groups.push([
|
|
121
193
|
...groups.length ? [{ kind: "fence", key: null, summary: "No epic" }] : [],
|
|
122
|
-
...orphans.
|
|
194
|
+
...orphans.flatMap((row) => withLeaves(row, 0))
|
|
123
195
|
]);
|
|
124
196
|
return groups.flatMap(
|
|
125
197
|
(group, i) => i === 0 ? group : [{ kind: "gap" }, ...group]
|
|
126
198
|
);
|
|
127
199
|
};
|
|
128
|
-
var
|
|
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) => {
|
|
129
213
|
let seen = -1;
|
|
130
214
|
for (let i = 0; i < blocks.length; i++) {
|
|
131
|
-
if (blocks[i]
|
|
215
|
+
if (isStop(blocks[i]) && ++seen === stopIndex) return i;
|
|
132
216
|
}
|
|
133
217
|
return 0;
|
|
134
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
|
+
};
|
|
135
249
|
var priorityGlyph = (name) => {
|
|
136
250
|
const n = (name ?? "").trim().toLowerCase();
|
|
137
251
|
if (/^(highest|blocker|critical|p[01])$/.test(n)) return "\u21C8";
|
|
@@ -166,6 +280,46 @@ var pillVariantFor = (row) => {
|
|
|
166
280
|
if (t === "story") return "info";
|
|
167
281
|
return "muted";
|
|
168
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
|
+
};
|
|
169
323
|
var boardOf = async (client, jql, { tabs, board, limit = 200 } = {}) => {
|
|
170
324
|
const [issues, derived] = await Promise.all([
|
|
171
325
|
client.searchIssues(jql, { limit }),
|
|
@@ -358,7 +512,11 @@ var issueDetailOf = async (client, baseUrl, key) => {
|
|
|
358
512
|
var transitionsOf = async (client, key) => (await client.getTransitions(key)).transitions.map((t) => ({
|
|
359
513
|
id: t.id,
|
|
360
514
|
name: t.name,
|
|
361
|
-
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
|
+
}
|
|
362
520
|
}));
|
|
363
521
|
var useTermSize = () => {
|
|
364
522
|
const { stdout } = useStdout();
|
|
@@ -435,12 +593,13 @@ var IssueBody = ({
|
|
|
435
593
|
);
|
|
436
594
|
};
|
|
437
595
|
var CHROME = 6;
|
|
596
|
+
var ROW_CHROME = 7;
|
|
438
597
|
var LEGEND = [
|
|
439
598
|
["\u21C8 \u2191", "priority above the default"],
|
|
440
599
|
["=", "default priority"],
|
|
441
600
|
["\u2193 \u21CA", "priority below the default"],
|
|
442
601
|
["\u2500\u2500 epic \u2500\u2500", "a parent not in this tab; its rows hang beneath"],
|
|
443
|
-
["\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"],
|
|
444
603
|
["story \xB7 bug \xB7 task", "issue type"],
|
|
445
604
|
["2d", "time since last update"]
|
|
446
605
|
];
|
|
@@ -460,8 +619,21 @@ var tabLegendLine = (model) => model.tabs.source === "categories" ? [
|
|
|
460
619
|
"To do \xB7 In progress \xB7 Done",
|
|
461
620
|
"status category \u2014 not any one board's column"
|
|
462
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
|
+
};
|
|
463
633
|
var IssueBoard = ({
|
|
464
634
|
model,
|
|
635
|
+
leaves,
|
|
636
|
+
parents,
|
|
465
637
|
viewer,
|
|
466
638
|
loadedAt,
|
|
467
639
|
scope,
|
|
@@ -499,7 +671,11 @@ var IssueBoard = ({
|
|
|
499
671
|
(r) => `${r.key} ${r.summary}`.toLowerCase().includes(words)
|
|
500
672
|
);
|
|
501
673
|
}, [model.rows, search]);
|
|
502
|
-
const
|
|
674
|
+
const placement = useMemo(
|
|
675
|
+
() => placementOf(narrowed, model.tabs),
|
|
676
|
+
[narrowed, model.tabs]
|
|
677
|
+
);
|
|
678
|
+
const counts = countsFor(narrowed, model.tabs, placement);
|
|
503
679
|
const shown = visibleTabs(model.tabs, counts);
|
|
504
680
|
const tabItems = shown.map((t) => ({
|
|
505
681
|
value: t.value,
|
|
@@ -513,18 +689,26 @@ var IssueBoard = ({
|
|
|
513
689
|
isActive: listFocused
|
|
514
690
|
});
|
|
515
691
|
const tab = active ?? initial ?? "";
|
|
692
|
+
const under = leaves?.under;
|
|
516
693
|
const blocks = useMemo(
|
|
517
|
-
() => blocksFor(
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
694
|
+
() => blocksFor(
|
|
695
|
+
rowsInTab(narrowed, model.tabs, tab, placement),
|
|
696
|
+
under,
|
|
697
|
+
parents
|
|
698
|
+
),
|
|
699
|
+
[narrowed, model.tabs, tab, placement, under, parents]
|
|
522
700
|
);
|
|
523
|
-
const
|
|
701
|
+
const stops = stopsOf(blocks);
|
|
702
|
+
const { cursor, setCursor } = useListCursor(stops.length, {
|
|
524
703
|
vimKeys: true,
|
|
525
704
|
isActive: listFocused
|
|
526
705
|
});
|
|
527
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
|
+
};
|
|
528
712
|
useInput((input, key) => {
|
|
529
713
|
if (search.open) {
|
|
530
714
|
if (key.escape) setSearch((s) => ({ ...s, open: false }));
|
|
@@ -539,7 +723,7 @@ var IssueBoard = ({
|
|
|
539
723
|
if (input === "?" || key.escape) setLegend(false);
|
|
540
724
|
return;
|
|
541
725
|
}
|
|
542
|
-
if (key.return
|
|
726
|
+
if (key.return) openStop(stops[cursor]);
|
|
543
727
|
if (input === "/")
|
|
544
728
|
setSearch((s) => ({
|
|
545
729
|
...s,
|
|
@@ -554,7 +738,7 @@ var IssueBoard = ({
|
|
|
554
738
|
const searching = search.open || scope.kind === "search";
|
|
555
739
|
const chrome = CHROME + (searching ? 1 : 0) + (searchError ? 1 : 0);
|
|
556
740
|
const size = Math.max(3, height - chrome);
|
|
557
|
-
const focus =
|
|
741
|
+
const focus = blockIndexOfStop(blocks, cursor);
|
|
558
742
|
const { start, end } = windowFor(focus, blocks.length, size);
|
|
559
743
|
const keyWidth = Math.max(8, ...narrowed.map((r) => r.key.length));
|
|
560
744
|
const typeWidth = Math.max(
|
|
@@ -625,20 +809,61 @@ var IssueBoard = ({
|
|
|
625
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: [
|
|
626
810
|
/* @__PURE__ */ jsx(Box, { width: 28, flexShrink: 0, children: /* @__PURE__ */ jsx(Text, { color: colors.accent, children: glyph }) }),
|
|
627
811
|
/* @__PURE__ */ jsx(Text, { dimColor: true, children: meaning })
|
|
628
|
-
] }, glyph)) :
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
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,
|
|
634
852
|
/* @__PURE__ */ jsx(Box, { flexShrink: 0, width: 2, children: /* @__PURE__ */ jsx(Text, { color: priorityMarker(block.row.priority).color, children: priorityMarker(block.row.priority).marker }) }),
|
|
635
|
-
/* @__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 }) }),
|
|
636
854
|
/* @__PURE__ */ jsx(Box, { flexShrink: 0, width: typeWidth + 2, children: /* @__PURE__ */ jsx(Pill, { tone: "soft", variant: pillVariantFor(block.row), children: block.row.type.toLowerCase() }) }),
|
|
637
|
-
/* @__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
|
+
),
|
|
638
863
|
/* @__PURE__ */ jsx(Box, { flexShrink: 0, width: 5, justifyContent: "flex-end", children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: relativeAge(block.row.updated, now) }) })
|
|
639
|
-
] }, block.row.key)
|
|
640
|
-
) }),
|
|
641
|
-
/* @__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}` : " " }) })
|
|
642
867
|
] });
|
|
643
868
|
if (frame) return frame({ facts, title, hints, body });
|
|
644
869
|
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
@@ -747,4 +972,4 @@ var mockBoard = () => ({
|
|
|
747
972
|
tabs: tabsFromCategories()
|
|
748
973
|
});
|
|
749
974
|
|
|
750
|
-
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