plc-cli 0.2.0 → 0.3.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/cli.js +596 -68
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -6,7 +6,7 @@ import { Command } from "commander";
|
|
|
6
6
|
// src/meta.ts
|
|
7
7
|
var PRODUCT_NAME = "Plane Cockpit";
|
|
8
8
|
var BINARY_NAME = "plc";
|
|
9
|
-
var VERSION = true ? "0.
|
|
9
|
+
var VERSION = true ? "0.3.0" : "0.0.0-dev";
|
|
10
10
|
var AUTHOR_HANDLE = "@brunoomariano";
|
|
11
11
|
|
|
12
12
|
// src/commands/auth/index.ts
|
|
@@ -258,6 +258,7 @@ var planeConfigSchema = z.strictObject({
|
|
|
258
258
|
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
259
259
|
var DEFAULT_CACHE_TTL_SECONDS = 300;
|
|
260
260
|
var STATES_LABELS_TTL_SECONDS = 300;
|
|
261
|
+
var ACTIVITIES_TTL_SECONDS = 60;
|
|
261
262
|
var DEFAULT_CONFIG_PATHS = ["~/.config/plane-cli/config.yaml"];
|
|
262
263
|
|
|
263
264
|
// src/config/load-config.ts
|
|
@@ -986,7 +987,11 @@ var cacheKeys = {
|
|
|
986
987
|
states: (slug, projectId) => workspaceKey(slug, "project", projectId, "states"),
|
|
987
988
|
// Labels are project-scoped too, cached per project id.
|
|
988
989
|
labels: (slug, projectId) => workspaceKey(slug, "project", projectId, "labels"),
|
|
989
|
-
issuesPage: (slug, projectId, hash) => workspaceKey(slug, "project", projectId, "issues", hash)
|
|
990
|
+
issuesPage: (slug, projectId, hash) => workspaceKey(slug, "project", projectId, "issues", hash),
|
|
991
|
+
// The activity log is per work item, so it is cached per project + issue id.
|
|
992
|
+
issueActivities: (slug, projectId, issueId) => workspaceKey(slug, "project", projectId, "issue", issueId, "activities"),
|
|
993
|
+
// Relations are per work item too, cached per project + issue id.
|
|
994
|
+
issueRelations: (slug, projectId, issueId) => workspaceKey(slug, "project", projectId, "issue", issueId, "relations")
|
|
990
995
|
};
|
|
991
996
|
|
|
992
997
|
// src/plane/projects.ts
|
|
@@ -1628,6 +1633,104 @@ var LabelsService = class {
|
|
|
1628
1633
|
}
|
|
1629
1634
|
};
|
|
1630
1635
|
|
|
1636
|
+
// src/plane/activities.ts
|
|
1637
|
+
function toActivity(raw) {
|
|
1638
|
+
return {
|
|
1639
|
+
id: raw.id,
|
|
1640
|
+
verb: raw.verb,
|
|
1641
|
+
field: raw.field ?? void 0,
|
|
1642
|
+
oldValue: raw.old_value ?? void 0,
|
|
1643
|
+
newValue: raw.new_value ?? void 0,
|
|
1644
|
+
oldIdentifier: raw.old_identifier ?? void 0,
|
|
1645
|
+
newIdentifier: raw.new_identifier ?? void 0,
|
|
1646
|
+
createdAt: raw.created_at,
|
|
1647
|
+
actor: raw.actor ?? void 0
|
|
1648
|
+
};
|
|
1649
|
+
}
|
|
1650
|
+
function unwrap(res) {
|
|
1651
|
+
if (Array.isArray(res)) return res;
|
|
1652
|
+
if ("result" in res && Array.isArray(res.result)) return res.result;
|
|
1653
|
+
if ("results" in res && Array.isArray(res.results)) return res.results;
|
|
1654
|
+
return [];
|
|
1655
|
+
}
|
|
1656
|
+
var ActivitiesService = class {
|
|
1657
|
+
constructor(api, cache) {
|
|
1658
|
+
this.api = api;
|
|
1659
|
+
this.cache = cache;
|
|
1660
|
+
}
|
|
1661
|
+
api;
|
|
1662
|
+
cache;
|
|
1663
|
+
// list returns the issue's activity log, cache-first. The cached copy lets the
|
|
1664
|
+
// detail view's timing line and the activity tab share a single fetch; the
|
|
1665
|
+
// short TTL means a fresh change shows up the next time the detail opens.
|
|
1666
|
+
async list(project, issueId, signal) {
|
|
1667
|
+
const key = cacheKeys.issueActivities(this.api.workspace, project.id, issueId);
|
|
1668
|
+
const cached2 = await this.cache.get(key);
|
|
1669
|
+
if (cached2) return cached2;
|
|
1670
|
+
const res = await this.api.request(
|
|
1671
|
+
this.api.workspacePath("projects", project.id, "issues", issueId, "activities"),
|
|
1672
|
+
{ signal }
|
|
1673
|
+
);
|
|
1674
|
+
const activities = unwrap(res).map(toActivity);
|
|
1675
|
+
await this.cache.set(key, activities, ACTIVITIES_TTL_SECONDS);
|
|
1676
|
+
return activities;
|
|
1677
|
+
}
|
|
1678
|
+
};
|
|
1679
|
+
|
|
1680
|
+
// src/types/relation.ts
|
|
1681
|
+
var RELATION_TYPES = [
|
|
1682
|
+
"blocking",
|
|
1683
|
+
"blocked_by",
|
|
1684
|
+
"relates_to",
|
|
1685
|
+
"duplicate",
|
|
1686
|
+
"start_after",
|
|
1687
|
+
"start_before",
|
|
1688
|
+
"finish_after",
|
|
1689
|
+
"finish_before"
|
|
1690
|
+
];
|
|
1691
|
+
var RELATION_LABELS = {
|
|
1692
|
+
blocking: "blocking",
|
|
1693
|
+
blocked_by: "blocked by",
|
|
1694
|
+
relates_to: "relates to",
|
|
1695
|
+
duplicate: "duplicate of",
|
|
1696
|
+
start_after: "starts after",
|
|
1697
|
+
start_before: "starts before",
|
|
1698
|
+
finish_after: "finishes after",
|
|
1699
|
+
finish_before: "finishes before"
|
|
1700
|
+
};
|
|
1701
|
+
|
|
1702
|
+
// src/plane/relations.ts
|
|
1703
|
+
function normalize(raw) {
|
|
1704
|
+
const relations = {};
|
|
1705
|
+
for (const type of RELATION_TYPES) {
|
|
1706
|
+
relations[type] = raw[type] ?? [];
|
|
1707
|
+
}
|
|
1708
|
+
return relations;
|
|
1709
|
+
}
|
|
1710
|
+
var RelationsService = class {
|
|
1711
|
+
constructor(api, cache) {
|
|
1712
|
+
this.api = api;
|
|
1713
|
+
this.cache = cache;
|
|
1714
|
+
}
|
|
1715
|
+
api;
|
|
1716
|
+
cache;
|
|
1717
|
+
// list returns the current relations for an issue, cache-first. The short TTL
|
|
1718
|
+
// matches the activity log's: opening the detail and toggling the relations
|
|
1719
|
+
// section reuses one fetch, and a freshly added relation appears on reopen.
|
|
1720
|
+
async list(project, issueId, signal) {
|
|
1721
|
+
const key = cacheKeys.issueRelations(this.api.workspace, project.id, issueId);
|
|
1722
|
+
const cached2 = await this.cache.get(key);
|
|
1723
|
+
if (cached2) return cached2;
|
|
1724
|
+
const raw = await this.api.request(
|
|
1725
|
+
this.api.workspacePath("projects", project.id, "work-items", issueId, "relations"),
|
|
1726
|
+
{ signal }
|
|
1727
|
+
);
|
|
1728
|
+
const relations = normalize(raw);
|
|
1729
|
+
await this.cache.set(key, relations, ACTIVITIES_TTL_SECONDS);
|
|
1730
|
+
return relations;
|
|
1731
|
+
}
|
|
1732
|
+
};
|
|
1733
|
+
|
|
1631
1734
|
// src/keybindings/load.ts
|
|
1632
1735
|
import { readFile as readFile3 } from "fs/promises";
|
|
1633
1736
|
import YAML3 from "yaml";
|
|
@@ -1749,6 +1852,24 @@ var ACTIONS = [
|
|
|
1749
1852
|
},
|
|
1750
1853
|
{ id: "detail.comment", context: "detail", description: "comment on issue", defaultKey: "c" },
|
|
1751
1854
|
{ id: "detail.edit", context: "detail", description: "edit issue fields", defaultKey: "e" },
|
|
1855
|
+
{
|
|
1856
|
+
id: "detail.activity",
|
|
1857
|
+
context: "detail",
|
|
1858
|
+
description: "toggle the state-change activity log",
|
|
1859
|
+
defaultKey: "a"
|
|
1860
|
+
},
|
|
1861
|
+
{
|
|
1862
|
+
id: "detail.relations",
|
|
1863
|
+
context: "detail",
|
|
1864
|
+
description: "toggle the relations section",
|
|
1865
|
+
defaultKey: "l"
|
|
1866
|
+
},
|
|
1867
|
+
{
|
|
1868
|
+
id: "detail.relation-open",
|
|
1869
|
+
context: "detail",
|
|
1870
|
+
description: "open the focused relation",
|
|
1871
|
+
defaultKey: "enter"
|
|
1872
|
+
},
|
|
1752
1873
|
{
|
|
1753
1874
|
id: "detail.close",
|
|
1754
1875
|
context: "detail",
|
|
@@ -2043,6 +2164,8 @@ async function buildContext(flags) {
|
|
|
2043
2164
|
const users = new UsersService(api, cache);
|
|
2044
2165
|
const states = new StatesService(api, cache);
|
|
2045
2166
|
const labels = new LabelsService(api, cache);
|
|
2167
|
+
const activities = new ActivitiesService(api, cache);
|
|
2168
|
+
const relations = new RelationsService(api, cache);
|
|
2046
2169
|
const issues = new IssuesService(projects, workItems, users);
|
|
2047
2170
|
const { bindings: keybindings, sourcePath: keybindingsSourcePath } = await loadKeybindings();
|
|
2048
2171
|
const runtime = {
|
|
@@ -2062,6 +2185,8 @@ async function buildContext(flags) {
|
|
|
2062
2185
|
users,
|
|
2063
2186
|
states,
|
|
2064
2187
|
labels,
|
|
2188
|
+
activities,
|
|
2189
|
+
relations,
|
|
2065
2190
|
keybindings,
|
|
2066
2191
|
keybindingsSourcePath,
|
|
2067
2192
|
theme: resolveTheme(profile.theme),
|
|
@@ -2664,7 +2789,7 @@ import React9 from "react";
|
|
|
2664
2789
|
import { render } from "ink";
|
|
2665
2790
|
|
|
2666
2791
|
// src/tui/dashboard.tsx
|
|
2667
|
-
import React7, { useCallback as
|
|
2792
|
+
import React7, { useCallback as useCallback7, useEffect as useEffect7, useMemo as useMemo8, useState as useState14 } from "react";
|
|
2668
2793
|
import { Box as Box12, Text as Text12, useApp, useInput as useInput2 } from "ink";
|
|
2669
2794
|
|
|
2670
2795
|
// src/tui/theme/context.tsx
|
|
@@ -3058,7 +3183,7 @@ function IssueList(props) {
|
|
|
3058
3183
|
}
|
|
3059
3184
|
|
|
3060
3185
|
// src/tui/issue-detail.tsx
|
|
3061
|
-
import { useMemo } from "react";
|
|
3186
|
+
import React5, { useMemo } from "react";
|
|
3062
3187
|
import { Box as Box5, Text as Text5 } from "ink";
|
|
3063
3188
|
|
|
3064
3189
|
// src/utils/markdown-to-ansi.ts
|
|
@@ -3215,18 +3340,67 @@ function splitAnsiIntoLines(text, width) {
|
|
|
3215
3340
|
return rows;
|
|
3216
3341
|
}
|
|
3217
3342
|
|
|
3343
|
+
// src/utils/format-duration.ts
|
|
3344
|
+
var MINUTE_MS = 6e4;
|
|
3345
|
+
var HOUR_MS = 60 * MINUTE_MS;
|
|
3346
|
+
var DAY_MS = 24 * HOUR_MS;
|
|
3347
|
+
function humanizeDuration(ms) {
|
|
3348
|
+
if (ms < MINUTE_MS) return "just now";
|
|
3349
|
+
const days = Math.floor(ms / DAY_MS);
|
|
3350
|
+
const hours = Math.floor(ms % DAY_MS / HOUR_MS);
|
|
3351
|
+
const minutes = Math.floor(ms % HOUR_MS / MINUTE_MS);
|
|
3352
|
+
const parts = [];
|
|
3353
|
+
if (days > 0) parts.push(`${days}d`);
|
|
3354
|
+
if (hours > 0) parts.push(`${hours}h`);
|
|
3355
|
+
if (minutes > 0 && days === 0) parts.push(`${minutes}m`);
|
|
3356
|
+
return parts.slice(0, 2).join(" ");
|
|
3357
|
+
}
|
|
3358
|
+
|
|
3359
|
+
// src/tui/format-relation.ts
|
|
3360
|
+
function formatRelationRow(relation, now) {
|
|
3361
|
+
const head = relation.targetKey ?? `${relation.targetId.slice(0, 8)}\u2026`;
|
|
3362
|
+
const parts = [head];
|
|
3363
|
+
if (relation.target) {
|
|
3364
|
+
parts.push(relation.target.state.name);
|
|
3365
|
+
if (relation.target.name) parts.push(relation.target.name);
|
|
3366
|
+
}
|
|
3367
|
+
if (relation.relatedAt) {
|
|
3368
|
+
const when = Date.parse(relation.relatedAt);
|
|
3369
|
+
if (!Number.isNaN(when)) {
|
|
3370
|
+
const elapsed = humanizeDuration(now - when);
|
|
3371
|
+
parts.push(elapsed === "just now" ? "just now" : `${elapsed} ago`);
|
|
3372
|
+
}
|
|
3373
|
+
}
|
|
3374
|
+
return parts.join(" \xB7 ");
|
|
3375
|
+
}
|
|
3376
|
+
|
|
3218
3377
|
// src/tui/issue-detail.tsx
|
|
3219
3378
|
import { Fragment as Fragment2, jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
3379
|
+
function formatStateChange(activity, now) {
|
|
3380
|
+
const when = Date.parse(activity.createdAt);
|
|
3381
|
+
const from = activity.oldValue ? `${activity.oldValue} \u2192 ` : "\u2192 ";
|
|
3382
|
+
const head = `${from}${activity.newValue ?? "?"}`;
|
|
3383
|
+
if (Number.isNaN(when)) return head;
|
|
3384
|
+
const elapsed = humanizeDuration(now - when);
|
|
3385
|
+
return elapsed === "just now" ? `${head} \xB7 just now` : `${head} \xB7 ${elapsed} ago`;
|
|
3386
|
+
}
|
|
3220
3387
|
var MODAL_WIDTH = 100;
|
|
3221
3388
|
var PANEL_WIDTH = 50;
|
|
3222
3389
|
var HORIZONTAL_CHROME = 4;
|
|
3223
3390
|
var DETAIL_CHROME_ROWS = 16;
|
|
3224
|
-
function IssueMeta({
|
|
3391
|
+
function IssueMeta({
|
|
3392
|
+
issue,
|
|
3393
|
+
timeInState
|
|
3394
|
+
}) {
|
|
3225
3395
|
const theme = useTheme();
|
|
3226
3396
|
return /* @__PURE__ */ jsxs5(Box5, { marginTop: 1, flexDirection: "column", flexShrink: 0, children: [
|
|
3227
3397
|
/* @__PURE__ */ jsxs5(Text5, { children: [
|
|
3228
3398
|
"state: ",
|
|
3229
|
-
/* @__PURE__ */ jsx6(Text5, { color: theme.accent, children: issue.state.name })
|
|
3399
|
+
/* @__PURE__ */ jsx6(Text5, { color: theme.accent, children: issue.state.name }),
|
|
3400
|
+
timeInState ? /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
|
|
3401
|
+
" \xB7 for ",
|
|
3402
|
+
timeInState
|
|
3403
|
+
] }) : null
|
|
3230
3404
|
] }),
|
|
3231
3405
|
/* @__PURE__ */ jsxs5(Text5, { children: [
|
|
3232
3406
|
"priority: ",
|
|
@@ -3246,12 +3420,12 @@ function IssueMeta({ issue }) {
|
|
|
3246
3420
|
] })
|
|
3247
3421
|
] });
|
|
3248
3422
|
}
|
|
3249
|
-
function
|
|
3423
|
+
function ScrollableBody(props) {
|
|
3250
3424
|
let content;
|
|
3251
|
-
if (props.loading &&
|
|
3252
|
-
content = /* @__PURE__ */ jsx6(Text5, { dimColor: true, children:
|
|
3425
|
+
if (props.loading && props.visible.length === 0) {
|
|
3426
|
+
content = /* @__PURE__ */ jsx6(Text5, { dimColor: true, children: props.loadingLabel });
|
|
3253
3427
|
} else if (props.visible.length === 0 && props.hiddenAbove === 0 && props.hiddenBelow === 0) {
|
|
3254
|
-
content = /* @__PURE__ */ jsx6(Text5, { dimColor: true, children:
|
|
3428
|
+
content = /* @__PURE__ */ jsx6(Text5, { dimColor: true, children: props.emptyLabel });
|
|
3255
3429
|
} else {
|
|
3256
3430
|
content = /* @__PURE__ */ jsxs5(Fragment2, { children: [
|
|
3257
3431
|
props.hiddenAbove > 0 ? /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
|
|
@@ -3269,60 +3443,173 @@ function DescriptionBody(props) {
|
|
|
3269
3443
|
}
|
|
3270
3444
|
return /* @__PURE__ */ jsx6(Box5, { marginTop: 1, flexDirection: "column", overflow: "hidden", children: content });
|
|
3271
3445
|
}
|
|
3272
|
-
function
|
|
3273
|
-
|
|
3446
|
+
function RelationRow({
|
|
3447
|
+
relation,
|
|
3448
|
+
focused,
|
|
3449
|
+
now
|
|
3450
|
+
}) {
|
|
3451
|
+
const theme = useTheme();
|
|
3452
|
+
return /* @__PURE__ */ jsxs5(Text5, { wrap: "truncate", color: focused ? theme.accent : void 0, bold: focused, children: [
|
|
3453
|
+
focused ? "\u203A " : " ",
|
|
3454
|
+
formatRelationRow(relation, now)
|
|
3455
|
+
] });
|
|
3456
|
+
}
|
|
3457
|
+
function RelationsBody(props) {
|
|
3458
|
+
if (props.relations.length === 0) {
|
|
3459
|
+
return /* @__PURE__ */ jsx6(Box5, { marginTop: 1, flexDirection: "column", overflow: "hidden", children: /* @__PURE__ */ jsx6(Text5, { dimColor: true, children: props.loading ? "loading relations\u2026" : "(no relations)" }) });
|
|
3460
|
+
}
|
|
3461
|
+
const now = Date.now();
|
|
3462
|
+
const start = Math.max(
|
|
3463
|
+
0,
|
|
3464
|
+
Math.min(props.selected - 1, props.relations.length - props.viewportRows)
|
|
3465
|
+
);
|
|
3466
|
+
const window = props.relations.slice(Math.max(0, start), Math.max(0, start) + props.viewportRows);
|
|
3467
|
+
let lastType;
|
|
3468
|
+
return /* @__PURE__ */ jsx6(Box5, { marginTop: 1, flexDirection: "column", overflow: "hidden", children: window.map((relation, idx) => {
|
|
3469
|
+
const flatIdx = Math.max(0, start) + idx;
|
|
3470
|
+
const heading = relation.type !== lastType ? RELATION_LABELS[relation.type] : void 0;
|
|
3471
|
+
lastType = relation.type;
|
|
3472
|
+
return /* @__PURE__ */ jsxs5(React5.Fragment, { children: [
|
|
3473
|
+
heading ? /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
|
|
3474
|
+
heading,
|
|
3475
|
+
":"
|
|
3476
|
+
] }) : null,
|
|
3477
|
+
/* @__PURE__ */ jsx6(RelationRow, { relation, focused: flatIdx === props.selected, now })
|
|
3478
|
+
] }, `${relation.type}-${relation.targetId}`);
|
|
3479
|
+
}) });
|
|
3480
|
+
}
|
|
3481
|
+
var HINT_BY_MODE = {
|
|
3482
|
+
detail: "esc to close \xB7 a: activity \xB7 l: relations",
|
|
3483
|
+
activity: "esc to close \xB7 a: description",
|
|
3484
|
+
relations: "esc back \xB7 enter: open \xB7 l: description"
|
|
3485
|
+
};
|
|
3486
|
+
function closeHintFor(scrollTop, viewportRows, total, mode) {
|
|
3487
|
+
const label = HINT_BY_MODE[mode];
|
|
3488
|
+
if (mode === "relations" || total <= viewportRows) return label;
|
|
3274
3489
|
const end = Math.min(scrollTop + viewportRows, total);
|
|
3275
|
-
return
|
|
3490
|
+
return `${label} \xB7 ${scrollTop + 1}-${end}/${total}`;
|
|
3491
|
+
}
|
|
3492
|
+
function DetailHeader(props) {
|
|
3493
|
+
return /* @__PURE__ */ jsxs5(Box5, { justifyContent: "space-between", flexShrink: 0, children: [
|
|
3494
|
+
/* @__PURE__ */ jsxs5(Text5, { bold: true, children: [
|
|
3495
|
+
props.issueKey,
|
|
3496
|
+
props.mode !== "detail" ? /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
|
|
3497
|
+
" \xB7 ",
|
|
3498
|
+
props.mode
|
|
3499
|
+
] }) : null
|
|
3500
|
+
] }),
|
|
3501
|
+
props.variant === "modal" ? /* @__PURE__ */ jsx6(Text5, { dimColor: true, children: closeHintFor(props.scrollTop, props.viewportRows, props.total, props.mode) }) : null
|
|
3502
|
+
] });
|
|
3503
|
+
}
|
|
3504
|
+
var BODY_LABELS = {
|
|
3505
|
+
detail: { loadingLabel: "loading description\u2026", emptyLabel: "(no description)" },
|
|
3506
|
+
activity: { loadingLabel: "loading activity\u2026", emptyLabel: "(no state changes)" }
|
|
3507
|
+
};
|
|
3508
|
+
function resolveBody(mode, lines, props) {
|
|
3509
|
+
const viewportRows = props.viewportRows ?? lines.length;
|
|
3510
|
+
const scrollTop = Math.max(
|
|
3511
|
+
0,
|
|
3512
|
+
Math.min(props.scrollTop ?? 0, Math.max(0, lines.length - viewportRows))
|
|
3513
|
+
);
|
|
3514
|
+
return {
|
|
3515
|
+
lines,
|
|
3516
|
+
visible: lines.slice(scrollTop, scrollTop + viewportRows),
|
|
3517
|
+
scrollTop,
|
|
3518
|
+
hiddenBelow: Math.max(0, lines.length - scrollTop - viewportRows),
|
|
3519
|
+
loading: (mode === "activity" ? props.activityLoading : props.loading) ?? false,
|
|
3520
|
+
...BODY_LABELS[mode]
|
|
3521
|
+
};
|
|
3522
|
+
}
|
|
3523
|
+
function frameStyle(variant, accent) {
|
|
3524
|
+
return variant === "modal" ? { borderStyle: "double", borderColor: accent, paddingY: 1 } : { borderStyle: "round", borderColor: void 0, paddingY: 0 };
|
|
3525
|
+
}
|
|
3526
|
+
function DetailBody(props) {
|
|
3527
|
+
if (props.mode === "relations") {
|
|
3528
|
+
return /* @__PURE__ */ jsx6(
|
|
3529
|
+
RelationsBody,
|
|
3530
|
+
{
|
|
3531
|
+
relations: props.relations,
|
|
3532
|
+
selected: props.relationsSelected,
|
|
3533
|
+
loading: props.relationsLoading,
|
|
3534
|
+
viewportRows: props.relationsViewport
|
|
3535
|
+
}
|
|
3536
|
+
);
|
|
3537
|
+
}
|
|
3538
|
+
const b = props.textBody;
|
|
3539
|
+
return /* @__PURE__ */ jsx6(
|
|
3540
|
+
ScrollableBody,
|
|
3541
|
+
{
|
|
3542
|
+
loading: b.loading,
|
|
3543
|
+
loadingLabel: b.loadingLabel,
|
|
3544
|
+
emptyLabel: b.emptyLabel,
|
|
3545
|
+
visible: b.visible,
|
|
3546
|
+
hiddenAbove: b.scrollTop,
|
|
3547
|
+
hiddenBelow: b.hiddenBelow,
|
|
3548
|
+
scrollTop: b.scrollTop
|
|
3549
|
+
}
|
|
3550
|
+
);
|
|
3276
3551
|
}
|
|
3277
3552
|
function IssueDetail(props) {
|
|
3278
3553
|
const theme = useTheme();
|
|
3279
3554
|
const variant = props.variant ?? "panel";
|
|
3555
|
+
const mode = props.mode ?? "detail";
|
|
3280
3556
|
const width = variant === "modal" ? MODAL_WIDTH : PANEL_WIDTH;
|
|
3281
3557
|
const contentWidth = width - HORIZONTAL_CHROME;
|
|
3282
3558
|
const description = props.issue?.description;
|
|
3283
|
-
const
|
|
3284
|
-
const
|
|
3285
|
-
() =>
|
|
3286
|
-
[
|
|
3559
|
+
const stateChanges = props.stateChanges;
|
|
3560
|
+
const descriptionLines = useMemo(
|
|
3561
|
+
() => description ? splitAnsiIntoLines(markdownToAnsi(description), contentWidth) : [],
|
|
3562
|
+
[description, contentWidth]
|
|
3287
3563
|
);
|
|
3564
|
+
const activityLines = useMemo(() => {
|
|
3565
|
+
const now = Date.now();
|
|
3566
|
+
return (stateChanges ?? []).map((a) => formatStateChange(a, now)).reverse();
|
|
3567
|
+
}, [stateChanges]);
|
|
3288
3568
|
if (!props.issue) {
|
|
3289
3569
|
return /* @__PURE__ */ jsx6(Box5, { borderStyle: "round", paddingX: 1, width, children: /* @__PURE__ */ jsx6(Text5, { dimColor: true, children: "select an issue" }) });
|
|
3290
3570
|
}
|
|
3291
3571
|
const i = props.issue;
|
|
3292
|
-
const
|
|
3293
|
-
const
|
|
3294
|
-
|
|
3295
|
-
|
|
3572
|
+
const isActivity = mode === "activity";
|
|
3573
|
+
const textBody = resolveBody(
|
|
3574
|
+
isActivity ? "activity" : "detail",
|
|
3575
|
+
isActivity ? activityLines : descriptionLines,
|
|
3576
|
+
props
|
|
3296
3577
|
);
|
|
3297
|
-
const
|
|
3578
|
+
const viewportRows = props.viewportRows ?? textBody.lines.length;
|
|
3579
|
+
const relationsViewport = Math.max(1, viewportRows);
|
|
3298
3580
|
return /* @__PURE__ */ jsxs5(
|
|
3299
3581
|
Box5,
|
|
3300
3582
|
{
|
|
3301
3583
|
flexDirection: "column",
|
|
3302
|
-
|
|
3303
|
-
borderColor: variant === "modal" ? theme.accent : void 0,
|
|
3584
|
+
...frameStyle(variant, theme.accent),
|
|
3304
3585
|
paddingX: 1,
|
|
3305
|
-
paddingY: variant === "modal" ? 1 : 0,
|
|
3306
3586
|
width,
|
|
3307
3587
|
height: props.height,
|
|
3308
3588
|
flexShrink: 0,
|
|
3309
3589
|
overflow: "hidden",
|
|
3310
3590
|
children: [
|
|
3311
|
-
/* @__PURE__ */
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3591
|
+
/* @__PURE__ */ jsx6(
|
|
3592
|
+
DetailHeader,
|
|
3593
|
+
{
|
|
3594
|
+
issueKey: i.key,
|
|
3595
|
+
mode,
|
|
3596
|
+
variant,
|
|
3597
|
+
scrollTop: textBody.scrollTop,
|
|
3598
|
+
viewportRows,
|
|
3599
|
+
total: textBody.lines.length
|
|
3600
|
+
}
|
|
3601
|
+
),
|
|
3315
3602
|
/* @__PURE__ */ jsx6(Text5, { wrap: "truncate", children: i.name }),
|
|
3316
|
-
/* @__PURE__ */ jsx6(IssueMeta, { issue: i }),
|
|
3603
|
+
/* @__PURE__ */ jsx6(IssueMeta, { issue: i, timeInState: props.timeInState }),
|
|
3317
3604
|
/* @__PURE__ */ jsx6(
|
|
3318
|
-
|
|
3605
|
+
DetailBody,
|
|
3319
3606
|
{
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3607
|
+
mode,
|
|
3608
|
+
textBody,
|
|
3609
|
+
relations: props.relations ?? [],
|
|
3610
|
+
relationsSelected: props.relationsSelected ?? 0,
|
|
3611
|
+
relationsLoading: props.relationsLoading ?? false,
|
|
3612
|
+
relationsViewport
|
|
3326
3613
|
}
|
|
3327
3614
|
)
|
|
3328
3615
|
]
|
|
@@ -4714,6 +5001,183 @@ function useDetailPanel(opts) {
|
|
|
4714
5001
|
};
|
|
4715
5002
|
}
|
|
4716
5003
|
|
|
5004
|
+
// src/tui/use-activity-log.ts
|
|
5005
|
+
import { useEffect as useEffect5, useMemo as useMemo6, useState as useState11 } from "react";
|
|
5006
|
+
|
|
5007
|
+
// src/types/activity.ts
|
|
5008
|
+
var STATE_FIELD = "state";
|
|
5009
|
+
function isStateChange(activity) {
|
|
5010
|
+
return activity.field === STATE_FIELD;
|
|
5011
|
+
}
|
|
5012
|
+
|
|
5013
|
+
// src/plane/state-duration.ts
|
|
5014
|
+
function currentStateEntry(activities, issueCreatedAt) {
|
|
5015
|
+
const transitions = activities.filter(isStateChange).map((a) => a.createdAt).sort();
|
|
5016
|
+
return transitions.at(-1) ?? issueCreatedAt;
|
|
5017
|
+
}
|
|
5018
|
+
function timeInCurrentState(activities, issueCreatedAt, now) {
|
|
5019
|
+
const entry = Date.parse(currentStateEntry(activities, issueCreatedAt));
|
|
5020
|
+
if (Number.isNaN(entry)) return 0;
|
|
5021
|
+
return Math.max(0, now - entry);
|
|
5022
|
+
}
|
|
5023
|
+
|
|
5024
|
+
// src/tui/use-activity-log.ts
|
|
5025
|
+
function useActivityLog(opts) {
|
|
5026
|
+
const { open: open2, target, createdAt, ctx, logger } = opts;
|
|
5027
|
+
const [activities, setActivities] = useState11();
|
|
5028
|
+
const [loading, setLoading] = useState11(false);
|
|
5029
|
+
useEffect5(() => {
|
|
5030
|
+
if (!open2 || !target) {
|
|
5031
|
+
setActivities(void 0);
|
|
5032
|
+
return;
|
|
5033
|
+
}
|
|
5034
|
+
const controller = new AbortController();
|
|
5035
|
+
setLoading(true);
|
|
5036
|
+
const project = {
|
|
5037
|
+
id: target.project_id,
|
|
5038
|
+
identifier: target.project_identifier,
|
|
5039
|
+
name: "",
|
|
5040
|
+
workspace_id: ""
|
|
5041
|
+
};
|
|
5042
|
+
ctx.activities.list(project, target.id, controller.signal).then((log) => {
|
|
5043
|
+
if (!controller.signal.aborted) setActivities(log);
|
|
5044
|
+
}).catch((err) => {
|
|
5045
|
+
if (controller.signal.aborted) return;
|
|
5046
|
+
logger.warn("activity log fetch failed", { issue: target.key, err });
|
|
5047
|
+
setActivities(void 0);
|
|
5048
|
+
}).finally(() => {
|
|
5049
|
+
if (!controller.signal.aborted) setLoading(false);
|
|
5050
|
+
});
|
|
5051
|
+
return () => {
|
|
5052
|
+
controller.abort();
|
|
5053
|
+
};
|
|
5054
|
+
}, [open2, target, ctx, logger]);
|
|
5055
|
+
const stateChanges = useMemo6(
|
|
5056
|
+
() => activities ? activities.filter(isStateChange) : [],
|
|
5057
|
+
[activities]
|
|
5058
|
+
);
|
|
5059
|
+
const timeInState = useMemo6(() => {
|
|
5060
|
+
if (!activities || !createdAt) return void 0;
|
|
5061
|
+
return humanizeDuration(timeInCurrentState(activities, createdAt, Date.now()));
|
|
5062
|
+
}, [activities, createdAt]);
|
|
5063
|
+
return { activities, stateChanges, loading, timeInState };
|
|
5064
|
+
}
|
|
5065
|
+
|
|
5066
|
+
// src/tui/use-relations.ts
|
|
5067
|
+
import { useEffect as useEffect6, useMemo as useMemo7, useState as useState12 } from "react";
|
|
5068
|
+
|
|
5069
|
+
// src/plane/relation-view.ts
|
|
5070
|
+
function indexAddEvents(activities) {
|
|
5071
|
+
const byTarget = /* @__PURE__ */ new Map();
|
|
5072
|
+
for (const a of activities) {
|
|
5073
|
+
if (!a.field || !RELATION_TYPES.includes(a.field)) continue;
|
|
5074
|
+
const target = a.oldIdentifier;
|
|
5075
|
+
if (!target) continue;
|
|
5076
|
+
const existing = byTarget.get(target);
|
|
5077
|
+
if (!existing || a.createdAt > existing.relatedAt) {
|
|
5078
|
+
byTarget.set(target, { relatedAt: a.createdAt, key: a.newValue });
|
|
5079
|
+
}
|
|
5080
|
+
}
|
|
5081
|
+
return byTarget;
|
|
5082
|
+
}
|
|
5083
|
+
function buildRelations(relations, activities) {
|
|
5084
|
+
const addEvents = indexAddEvents(activities);
|
|
5085
|
+
const result = [];
|
|
5086
|
+
for (const type of RELATION_TYPES) {
|
|
5087
|
+
for (const targetId of relations[type] ?? []) {
|
|
5088
|
+
const add = addEvents.get(targetId);
|
|
5089
|
+
result.push({ type, targetId, targetKey: add?.key, relatedAt: add?.relatedAt });
|
|
5090
|
+
}
|
|
5091
|
+
}
|
|
5092
|
+
return result;
|
|
5093
|
+
}
|
|
5094
|
+
|
|
5095
|
+
// src/tui/use-relations.ts
|
|
5096
|
+
function useRelations(opts) {
|
|
5097
|
+
const { open: open2, target, activities, ctx, logger } = opts;
|
|
5098
|
+
const [relations, setRelations] = useState12();
|
|
5099
|
+
const [loading, setLoading] = useState12(false);
|
|
5100
|
+
const [targets, setTargets] = useState12({});
|
|
5101
|
+
useEffect6(() => {
|
|
5102
|
+
if (!open2 || !target) {
|
|
5103
|
+
setRelations(void 0);
|
|
5104
|
+
setTargets({});
|
|
5105
|
+
return;
|
|
5106
|
+
}
|
|
5107
|
+
const controller = new AbortController();
|
|
5108
|
+
setLoading(true);
|
|
5109
|
+
const project = {
|
|
5110
|
+
id: target.project_id,
|
|
5111
|
+
identifier: target.project_identifier,
|
|
5112
|
+
name: "",
|
|
5113
|
+
workspace_id: ""
|
|
5114
|
+
};
|
|
5115
|
+
ctx.relations.list(project, target.id, controller.signal).then((current) => {
|
|
5116
|
+
if (!controller.signal.aborted) setRelations(buildRelations(current, activities ?? []));
|
|
5117
|
+
}).catch((err) => {
|
|
5118
|
+
if (controller.signal.aborted) return;
|
|
5119
|
+
logger.warn("relations fetch failed", { issue: target.key, err });
|
|
5120
|
+
setRelations(void 0);
|
|
5121
|
+
}).finally(() => {
|
|
5122
|
+
if (!controller.signal.aborted) setLoading(false);
|
|
5123
|
+
});
|
|
5124
|
+
return () => {
|
|
5125
|
+
controller.abort();
|
|
5126
|
+
};
|
|
5127
|
+
}, [open2, target, activities, ctx, logger]);
|
|
5128
|
+
useEffect6(() => {
|
|
5129
|
+
if (!relations) return;
|
|
5130
|
+
let cancelled = false;
|
|
5131
|
+
const pending = relations.map((r) => r.targetKey).filter((key) => Boolean(key) && !(key in targets));
|
|
5132
|
+
for (const key of pending) {
|
|
5133
|
+
ctx.issues.view(key).then((issue) => {
|
|
5134
|
+
if (!cancelled) setTargets((prev) => ({ ...prev, [key]: issue }));
|
|
5135
|
+
}).catch((err) => {
|
|
5136
|
+
logger.warn("relation target resolve failed", { key, err });
|
|
5137
|
+
});
|
|
5138
|
+
}
|
|
5139
|
+
return () => {
|
|
5140
|
+
cancelled = true;
|
|
5141
|
+
};
|
|
5142
|
+
}, [relations, targets, ctx, logger]);
|
|
5143
|
+
const enriched = useMemo7(
|
|
5144
|
+
() => (relations ?? []).map(
|
|
5145
|
+
(r) => r.targetKey && targets[r.targetKey] ? { ...r, target: targets[r.targetKey] } : r
|
|
5146
|
+
),
|
|
5147
|
+
[relations, targets]
|
|
5148
|
+
);
|
|
5149
|
+
return { relations: enriched, loading };
|
|
5150
|
+
}
|
|
5151
|
+
|
|
5152
|
+
// src/tui/use-detail-stack.ts
|
|
5153
|
+
import { useCallback as useCallback6, useState as useState13 } from "react";
|
|
5154
|
+
function targetFromIssue(issue) {
|
|
5155
|
+
return {
|
|
5156
|
+
id: issue.id,
|
|
5157
|
+
key: issue.key,
|
|
5158
|
+
project_id: issue.project_id,
|
|
5159
|
+
project_identifier: issue.project_identifier
|
|
5160
|
+
};
|
|
5161
|
+
}
|
|
5162
|
+
function useDetailStack() {
|
|
5163
|
+
const [stack, setStack] = useState13([]);
|
|
5164
|
+
const open2 = useCallback6((target) => setStack([target]), []);
|
|
5165
|
+
const push = useCallback6(
|
|
5166
|
+
(target) => setStack((s) => s.at(-1)?.id === target.id ? s : [...s, target]),
|
|
5167
|
+
[]
|
|
5168
|
+
);
|
|
5169
|
+
const pop = useCallback6(() => setStack((s) => s.slice(0, -1)), []);
|
|
5170
|
+
const close = useCallback6(() => setStack([]), []);
|
|
5171
|
+
return {
|
|
5172
|
+
current: stack.at(-1),
|
|
5173
|
+
canGoBack: stack.length > 1,
|
|
5174
|
+
open: open2,
|
|
5175
|
+
push,
|
|
5176
|
+
pop,
|
|
5177
|
+
close
|
|
5178
|
+
};
|
|
5179
|
+
}
|
|
5180
|
+
|
|
4717
5181
|
// src/tui/dashboard.tsx
|
|
4718
5182
|
import { jsx as jsx13, jsxs as jsxs12 } from "react/jsx-runtime";
|
|
4719
5183
|
var NARROW_BREAKPOINT = 100;
|
|
@@ -4863,6 +5327,13 @@ function renderActiveOverlay(opts) {
|
|
|
4863
5327
|
issue: current,
|
|
4864
5328
|
loading: detail.loading,
|
|
4865
5329
|
variant: "modal",
|
|
5330
|
+
mode: opts.detailMode,
|
|
5331
|
+
timeInState: opts.activity.timeInState,
|
|
5332
|
+
stateChanges: opts.activity.stateChanges,
|
|
5333
|
+
activityLoading: opts.activity.loading,
|
|
5334
|
+
relations: opts.relations.relations,
|
|
5335
|
+
relationsSelected: opts.relationsSelected,
|
|
5336
|
+
relationsLoading: opts.relations.loading,
|
|
4866
5337
|
scrollTop: detail.scroll,
|
|
4867
5338
|
viewportRows: opts.detailViewportRows,
|
|
4868
5339
|
height: opts.detailModalHeight
|
|
@@ -4903,19 +5374,22 @@ function Dashboard({ ctx, logger }) {
|
|
|
4903
5374
|
const { exit } = useApp();
|
|
4904
5375
|
const { rows: terminalRows, columns: terminalCols } = useTerminalSize();
|
|
4905
5376
|
const narrow = isNarrowLayout(terminalCols);
|
|
4906
|
-
const views =
|
|
4907
|
-
const defaultProjects =
|
|
4908
|
-
const viewEntries =
|
|
5377
|
+
const views = useMemo8(() => ctx.runtime.profile.views ?? [], [ctx]);
|
|
5378
|
+
const defaultProjects = useMemo8(() => ctx.runtime.profile.defaults?.projects ?? [], [ctx]);
|
|
5379
|
+
const viewEntries = useMemo8(
|
|
4909
5380
|
() => buildViewEntries(views, defaultProjects),
|
|
4910
5381
|
[views, defaultProjects]
|
|
4911
5382
|
);
|
|
4912
|
-
const [viewIdx, setViewIdx] =
|
|
4913
|
-
const [selected, setSelected] =
|
|
4914
|
-
const [statusMessage, setStatusMessage] =
|
|
4915
|
-
const
|
|
4916
|
-
const
|
|
5383
|
+
const [viewIdx, setViewIdx] = useState14(0);
|
|
5384
|
+
const [selected, setSelected] = useState14(0);
|
|
5385
|
+
const [statusMessage, setStatusMessage] = useState14();
|
|
5386
|
+
const stack = useDetailStack();
|
|
5387
|
+
const detailOpen = stack.current !== void 0;
|
|
5388
|
+
const [detailMode, setDetailMode] = useState14("detail");
|
|
5389
|
+
const [relationsSelected, setRelationsSelected] = useState14(0);
|
|
5390
|
+
const [helpOpen, setHelpOpen] = useState14(false);
|
|
4917
5391
|
const activeView = views[viewIdx];
|
|
4918
|
-
const { layout: activeLayout, sort: activeSort } =
|
|
5392
|
+
const { layout: activeLayout, sort: activeSort } = useMemo8(
|
|
4919
5393
|
() => resolveViewPresentation(activeView, ctx.runtime.profile.defaults),
|
|
4920
5394
|
[activeView, ctx]
|
|
4921
5395
|
);
|
|
@@ -4937,7 +5411,7 @@ function Dashboard({ ctx, logger }) {
|
|
|
4937
5411
|
const loading = active.loading;
|
|
4938
5412
|
const partialMessage = active.failedProjects.length > 0 ? `partial: ${active.failedProjects.length} project(s) unavailable (${active.failedProjects.join(", ")})` : void 0;
|
|
4939
5413
|
const error = statusMessage ?? active.error ?? partialMessage;
|
|
4940
|
-
const navbarEntries =
|
|
5414
|
+
const navbarEntries = useMemo8(
|
|
4941
5415
|
() => viewEntries.map((entry, idx) => {
|
|
4942
5416
|
const data = viewsData.byView[idx];
|
|
4943
5417
|
return {
|
|
@@ -4949,7 +5423,7 @@ function Dashboard({ ctx, logger }) {
|
|
|
4949
5423
|
[viewEntries, viewsData.byView]
|
|
4950
5424
|
);
|
|
4951
5425
|
const loadView = viewsData.load;
|
|
4952
|
-
const load =
|
|
5426
|
+
const load = useCallback7(
|
|
4953
5427
|
async (preserveSelection = false) => {
|
|
4954
5428
|
const previousKey = preserveSelection ? selectedKeyRef.current : void 0;
|
|
4955
5429
|
const keys = await loadView(viewIdx);
|
|
@@ -4959,12 +5433,12 @@ function Dashboard({ ctx, logger }) {
|
|
|
4959
5433
|
);
|
|
4960
5434
|
const loadedRef = React7.useRef(viewsData.byView);
|
|
4961
5435
|
loadedRef.current = viewsData.byView;
|
|
4962
|
-
|
|
5436
|
+
useEffect7(() => {
|
|
4963
5437
|
if (!activeView) return;
|
|
4964
5438
|
if (loadedRef.current[viewIdx]?.loaded) setSelected(0);
|
|
4965
5439
|
else void load();
|
|
4966
5440
|
}, [viewIdx, activeView, load]);
|
|
4967
|
-
|
|
5441
|
+
useEffect7(() => {
|
|
4968
5442
|
logger.info("dashboard started", {
|
|
4969
5443
|
profile: ctx.runtime.profile_name,
|
|
4970
5444
|
workspace: ctx.runtime.profile.server.workspace_slug,
|
|
@@ -4986,7 +5460,7 @@ function Dashboard({ ctx, logger }) {
|
|
|
4986
5460
|
logger
|
|
4987
5461
|
});
|
|
4988
5462
|
const currentSummary = filtered[selected];
|
|
4989
|
-
|
|
5463
|
+
useEffect7(() => {
|
|
4990
5464
|
selectedKeyRef.current = currentSummary?.key;
|
|
4991
5465
|
}, [currentSummary]);
|
|
4992
5466
|
const comments = useCommentEditor({
|
|
@@ -5008,7 +5482,7 @@ function Dashboard({ ctx, logger }) {
|
|
|
5008
5482
|
name: "",
|
|
5009
5483
|
workspace_id: ""
|
|
5010
5484
|
});
|
|
5011
|
-
const reconcile =
|
|
5485
|
+
const reconcile = useCallback7(
|
|
5012
5486
|
(updated, touchesFilter) => {
|
|
5013
5487
|
if (touchesFilter) void load(true);
|
|
5014
5488
|
else viewsData.patchIssue(viewIdx, updated);
|
|
@@ -5036,7 +5510,7 @@ function Dashboard({ ctx, logger }) {
|
|
|
5036
5510
|
setStatusMessage(message);
|
|
5037
5511
|
}
|
|
5038
5512
|
});
|
|
5039
|
-
const activeProjects =
|
|
5513
|
+
const activeProjects = useMemo8(
|
|
5040
5514
|
() => resolveViewProjectsLenient(activeView ?? { name: "" }, defaultProjects).projects,
|
|
5041
5515
|
[activeView, defaultProjects]
|
|
5042
5516
|
);
|
|
@@ -5082,11 +5556,11 @@ function Dashboard({ ctx, logger }) {
|
|
|
5082
5556
|
creator.active,
|
|
5083
5557
|
transition.active,
|
|
5084
5558
|
helpOpen,
|
|
5085
|
-
|
|
5559
|
+
detailOpen,
|
|
5086
5560
|
filtering
|
|
5087
5561
|
].some(Boolean);
|
|
5088
5562
|
const intervalMs = autoRefreshIntervalMs(ctx.runtime.profile.defaults?.auto_refresh_seconds);
|
|
5089
|
-
|
|
5563
|
+
useEffect7(() => {
|
|
5090
5564
|
if (intervalMs === void 0 || overlayActive || !activeView) return;
|
|
5091
5565
|
const timer = setInterval(() => {
|
|
5092
5566
|
void load(true);
|
|
@@ -5094,12 +5568,30 @@ function Dashboard({ ctx, logger }) {
|
|
|
5094
5568
|
return () => clearInterval(timer);
|
|
5095
5569
|
}, [intervalMs, overlayActive, activeView, load]);
|
|
5096
5570
|
const detail = useDetailPanel({
|
|
5097
|
-
open:
|
|
5098
|
-
target:
|
|
5571
|
+
open: detailOpen,
|
|
5572
|
+
target: stack.current,
|
|
5099
5573
|
ctx,
|
|
5100
5574
|
logger,
|
|
5101
5575
|
setMessage: setStatusMessage
|
|
5102
5576
|
});
|
|
5577
|
+
const activity = useActivityLog({
|
|
5578
|
+
open: detailOpen,
|
|
5579
|
+
target: stack.current,
|
|
5580
|
+
createdAt: detail.detailed?.created_at,
|
|
5581
|
+
ctx,
|
|
5582
|
+
logger
|
|
5583
|
+
});
|
|
5584
|
+
const relations = useRelations({
|
|
5585
|
+
open: detailOpen,
|
|
5586
|
+
target: stack.current,
|
|
5587
|
+
activities: activity.activities,
|
|
5588
|
+
ctx,
|
|
5589
|
+
logger
|
|
5590
|
+
});
|
|
5591
|
+
useEffect7(() => {
|
|
5592
|
+
setDetailMode("detail");
|
|
5593
|
+
setRelationsSelected(0);
|
|
5594
|
+
}, [stack.current]);
|
|
5103
5595
|
const viewportRows = listViewportRows({
|
|
5104
5596
|
terminalRows,
|
|
5105
5597
|
filtering,
|
|
@@ -5109,7 +5601,7 @@ function Dashboard({ ctx, logger }) {
|
|
|
5109
5601
|
const STATUS_BAR_ROWS = 3;
|
|
5110
5602
|
const detailModalHeight = Math.max(DETAIL_CHROME_ROWS + 3, terminalRows - STATUS_BAR_ROWS);
|
|
5111
5603
|
const detailViewportRows = Math.max(3, detailModalHeight - DETAIL_CHROME_ROWS);
|
|
5112
|
-
const openSelectedInBrowser =
|
|
5604
|
+
const openSelectedInBrowser = useCallback7(() => {
|
|
5113
5605
|
const issue = filtered[selected];
|
|
5114
5606
|
if (!issue) return;
|
|
5115
5607
|
try {
|
|
@@ -5142,7 +5634,9 @@ function Dashboard({ ctx, logger }) {
|
|
|
5142
5634
|
"list.page-up": () => setSelected((s) => Math.max(0, s - viewportRows)),
|
|
5143
5635
|
"list.top": () => setSelected(0),
|
|
5144
5636
|
"list.bottom": () => setSelected(Math.max(0, filtered.length - 1)),
|
|
5145
|
-
"list.open-detail": () =>
|
|
5637
|
+
"list.open-detail": () => {
|
|
5638
|
+
if (currentSummary) stack.open(targetFromIssue(currentSummary));
|
|
5639
|
+
},
|
|
5146
5640
|
"list.open-browser": openSelectedInBrowser,
|
|
5147
5641
|
"list.comment": comments.open,
|
|
5148
5642
|
"list.edit": editor.open,
|
|
@@ -5165,13 +5659,29 @@ function Dashboard({ ctx, logger }) {
|
|
|
5165
5659
|
);
|
|
5166
5660
|
if (!consumed && (input2 === "q" || key.escape)) setHelpOpen(false);
|
|
5167
5661
|
};
|
|
5662
|
+
const openFocusedRelation = useCallback7(() => {
|
|
5663
|
+
const relation = relations.relations[relationsSelected];
|
|
5664
|
+
if (!relation) return;
|
|
5665
|
+
if (relation.target) {
|
|
5666
|
+
stack.push(targetFromIssue(relation.target));
|
|
5667
|
+
return;
|
|
5668
|
+
}
|
|
5669
|
+
if (!relation.targetKey) return;
|
|
5670
|
+
void ctx.issues.view(relation.targetKey).then((issue) => stack.push(targetFromIssue(issue))).catch((err) => {
|
|
5671
|
+
logger.error("open relation failed", { key: relation.targetKey, err });
|
|
5672
|
+
setStatusMessage(`open ${relation.targetKey}: ${err.message}`);
|
|
5673
|
+
});
|
|
5674
|
+
}, [relations.relations, relationsSelected, stack, ctx, logger]);
|
|
5168
5675
|
const handleDetailKey = (input2, key) => {
|
|
5676
|
+
const relationsMode = detailMode === "relations";
|
|
5677
|
+
const relationCount = relations.relations.length;
|
|
5169
5678
|
const detailHandlers = {
|
|
5170
|
-
|
|
5171
|
-
"detail.
|
|
5172
|
-
"detail.scroll-down
|
|
5173
|
-
"detail.scroll-
|
|
5174
|
-
"detail.scroll-up
|
|
5679
|
+
// esc pops the navigation stack: back to the issue we came from, or close.
|
|
5680
|
+
"detail.close": () => stack.pop(),
|
|
5681
|
+
"detail.scroll-down": relationsMode ? () => setRelationsSelected((s) => Math.min(relationCount - 1, s + 1)) : () => detail.scrollBy(1),
|
|
5682
|
+
"detail.scroll-down-alt": relationsMode ? () => setRelationsSelected((s) => Math.min(relationCount - 1, s + 1)) : () => detail.scrollBy(1),
|
|
5683
|
+
"detail.scroll-up": relationsMode ? () => setRelationsSelected((s) => Math.max(0, s - 1)) : () => detail.scrollBy(-1),
|
|
5684
|
+
"detail.scroll-up-alt": relationsMode ? () => setRelationsSelected((s) => Math.max(0, s - 1)) : () => detail.scrollBy(-1),
|
|
5175
5685
|
"detail.page-down": () => detail.scrollBy(detailViewportRows),
|
|
5176
5686
|
"detail.page-up": () => detail.scrollBy(-detailViewportRows),
|
|
5177
5687
|
"detail.top": detail.scrollTop,
|
|
@@ -5179,10 +5689,24 @@ function Dashboard({ ctx, logger }) {
|
|
|
5179
5689
|
"detail.open-browser": openSelectedInBrowser,
|
|
5180
5690
|
"detail.comment": comments.open,
|
|
5181
5691
|
"detail.edit": editor.open,
|
|
5692
|
+
// Toggle the activity / relations bodies; toggling back returns to the
|
|
5693
|
+
// description. Reset the scroll so the new body starts at the top.
|
|
5694
|
+
"detail.activity": () => {
|
|
5695
|
+
setDetailMode((mode) => mode === "activity" ? "detail" : "activity");
|
|
5696
|
+
detail.scrollTop();
|
|
5697
|
+
},
|
|
5698
|
+
"detail.relations": () => {
|
|
5699
|
+
setDetailMode((mode) => mode === "relations" ? "detail" : "relations");
|
|
5700
|
+
setRelationsSelected(0);
|
|
5701
|
+
detail.scrollTop();
|
|
5702
|
+
},
|
|
5703
|
+
// enter only acts in the relations body, opening the focused relation.
|
|
5704
|
+
"detail.relation-open": relationsMode ? openFocusedRelation : () => {
|
|
5705
|
+
},
|
|
5182
5706
|
"global.help": () => setHelpOpen((open2) => !open2),
|
|
5183
5707
|
"global.refresh": () => void load(true),
|
|
5184
5708
|
"global.refresh-all": () => viewsData.refreshAll(),
|
|
5185
|
-
"global.quit": () =>
|
|
5709
|
+
"global.quit": () => stack.close()
|
|
5186
5710
|
};
|
|
5187
5711
|
dispatch(ctx.keybindings, ["detail", "global"], detailHandlers, input2, key);
|
|
5188
5712
|
};
|
|
@@ -5192,7 +5716,7 @@ function Dashboard({ ctx, logger }) {
|
|
|
5192
5716
|
[editor.active, editor.handleKey],
|
|
5193
5717
|
[comments.active, comments.handleKey],
|
|
5194
5718
|
[helpOpen, handleHelpKey],
|
|
5195
|
-
[
|
|
5719
|
+
[detailOpen, handleDetailKey],
|
|
5196
5720
|
[filtering, handleFilterKey]
|
|
5197
5721
|
];
|
|
5198
5722
|
useInput2((input2, key) => {
|
|
@@ -5200,7 +5724,7 @@ function Dashboard({ ctx, logger }) {
|
|
|
5200
5724
|
if (route) return route[1](input2, key);
|
|
5201
5725
|
dispatch(ctx.keybindings, ["global", "list", "view", "filter"], handlers, input2, key);
|
|
5202
5726
|
});
|
|
5203
|
-
const current = detail.detailed ?? currentSummary;
|
|
5727
|
+
const current = detail.detailed ?? (stack.canGoBack ? void 0 : currentSummary);
|
|
5204
5728
|
const statusBarBase = {
|
|
5205
5729
|
profile: ctx.runtime.profile_name,
|
|
5206
5730
|
workspace: ctx.runtime.profile.server.workspace_slug,
|
|
@@ -5213,13 +5737,17 @@ function Dashboard({ ctx, logger }) {
|
|
|
5213
5737
|
total: issues.length,
|
|
5214
5738
|
filtering: Boolean(filter)
|
|
5215
5739
|
});
|
|
5216
|
-
const isDetail =
|
|
5740
|
+
const isDetail = detailOpen;
|
|
5217
5741
|
const overlay = renderActiveOverlay({
|
|
5218
5742
|
transition,
|
|
5219
5743
|
creator,
|
|
5220
5744
|
editor,
|
|
5221
5745
|
comments,
|
|
5222
5746
|
detail,
|
|
5747
|
+
activity,
|
|
5748
|
+
relations,
|
|
5749
|
+
relationsSelected,
|
|
5750
|
+
detailMode,
|
|
5223
5751
|
helpOpen,
|
|
5224
5752
|
isDetail,
|
|
5225
5753
|
currentSummary,
|