plc-cli 0.1.3 → 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 +695 -114
- 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
|
|
@@ -233,6 +233,12 @@ var profileSchema = z.strictObject({
|
|
|
233
233
|
// Profile-wide default sort, inherited by any view that does not declare
|
|
234
234
|
// its own `sort`. Same shape as a view's sort.
|
|
235
235
|
sort: sortSpecSchema.optional(),
|
|
236
|
+
// Global state ordering for `sort: state` and the quick-transition
|
|
237
|
+
// navigation. An ordered list of state slugs (matched case-insensitively,
|
|
238
|
+
// whitespace-collapsed): listed states sort first in this order, unlisted
|
|
239
|
+
// ones after by workflow group. Project states are customizable, so this
|
|
240
|
+
// lets one list rank them across every project.
|
|
241
|
+
state_order: z.array(z.string().min(1)).optional(),
|
|
236
242
|
// Profile-wide default column layout, inherited by views without a layout.
|
|
237
243
|
layout: layoutSchema.optional()
|
|
238
244
|
}).optional(),
|
|
@@ -252,6 +258,7 @@ var planeConfigSchema = z.strictObject({
|
|
|
252
258
|
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
253
259
|
var DEFAULT_CACHE_TTL_SECONDS = 300;
|
|
254
260
|
var STATES_LABELS_TTL_SECONDS = 300;
|
|
261
|
+
var ACTIVITIES_TTL_SECONDS = 60;
|
|
255
262
|
var DEFAULT_CONFIG_PATHS = ["~/.config/plane-cli/config.yaml"];
|
|
256
263
|
|
|
257
264
|
// src/config/load-config.ts
|
|
@@ -980,7 +987,11 @@ var cacheKeys = {
|
|
|
980
987
|
states: (slug, projectId) => workspaceKey(slug, "project", projectId, "states"),
|
|
981
988
|
// Labels are project-scoped too, cached per project id.
|
|
982
989
|
labels: (slug, projectId) => workspaceKey(slug, "project", projectId, "labels"),
|
|
983
|
-
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")
|
|
984
995
|
};
|
|
985
996
|
|
|
986
997
|
// src/plane/projects.ts
|
|
@@ -1059,6 +1070,31 @@ function filtersFingerprint(filters) {
|
|
|
1059
1070
|
return createHash("sha256").update(json).digest("hex").slice(0, 16);
|
|
1060
1071
|
}
|
|
1061
1072
|
|
|
1073
|
+
// src/plane/state-rank.ts
|
|
1074
|
+
var STATE_GROUP_RANK = {
|
|
1075
|
+
backlog: 0,
|
|
1076
|
+
unstarted: 1,
|
|
1077
|
+
started: 2,
|
|
1078
|
+
completed: 3,
|
|
1079
|
+
cancelled: 4
|
|
1080
|
+
};
|
|
1081
|
+
var UNLISTED_OFFSET = 1e6;
|
|
1082
|
+
function normalizeStateSlug(name) {
|
|
1083
|
+
return name.trim().toLowerCase().replace(/\s+/g, " ");
|
|
1084
|
+
}
|
|
1085
|
+
function buildStateRank(stateOrder) {
|
|
1086
|
+
const position = /* @__PURE__ */ new Map();
|
|
1087
|
+
(stateOrder ?? []).forEach((slug, index) => {
|
|
1088
|
+
const normalized = normalizeStateSlug(slug);
|
|
1089
|
+
if (!position.has(normalized)) position.set(normalized, index);
|
|
1090
|
+
});
|
|
1091
|
+
return (state) => {
|
|
1092
|
+
const listed = position.get(normalizeStateSlug(state.name));
|
|
1093
|
+
if (listed !== void 0) return listed;
|
|
1094
|
+
return UNLISTED_OFFSET + STATE_GROUP_RANK[state.group];
|
|
1095
|
+
};
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1062
1098
|
// src/plane/sort-issues.ts
|
|
1063
1099
|
var PRIORITY_RANK = {
|
|
1064
1100
|
urgent: 0,
|
|
@@ -1067,45 +1103,41 @@ var PRIORITY_RANK = {
|
|
|
1067
1103
|
low: 3,
|
|
1068
1104
|
none: 4
|
|
1069
1105
|
};
|
|
1070
|
-
var STATE_GROUP_RANK = {
|
|
1071
|
-
backlog: 0,
|
|
1072
|
-
unstarted: 1,
|
|
1073
|
-
started: 2,
|
|
1074
|
-
completed: 3,
|
|
1075
|
-
cancelled: 4
|
|
1076
|
-
};
|
|
1077
1106
|
var DEFAULT_SORT = [
|
|
1078
1107
|
{ field: "project", direction: "asc" },
|
|
1079
1108
|
{ field: "priority", direction: "desc" },
|
|
1080
1109
|
{ field: "state", direction: "asc" },
|
|
1081
1110
|
{ field: "updated_at", direction: "desc" }
|
|
1082
1111
|
];
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1112
|
+
function ascComparators(stateRank) {
|
|
1113
|
+
return {
|
|
1114
|
+
project: (a, b) => a.project_identifier.localeCompare(b.project_identifier),
|
|
1115
|
+
priority: (a, b) => PRIORITY_RANK[b.priority] - PRIORITY_RANK[a.priority],
|
|
1116
|
+
state: (a, b) => stateRank(a.state) - stateRank(b.state),
|
|
1117
|
+
created_at: (a, b) => a.created_at.localeCompare(b.created_at),
|
|
1118
|
+
updated_at: (a, b) => a.updated_at.localeCompare(b.updated_at),
|
|
1119
|
+
assign: (a, b) => firstAssignee(a).localeCompare(firstAssignee(b))
|
|
1120
|
+
};
|
|
1121
|
+
}
|
|
1091
1122
|
function firstAssignee(issue) {
|
|
1092
1123
|
return issue.assignees[0]?.display_name ?? "";
|
|
1093
1124
|
}
|
|
1094
|
-
function compareKey(a, b, field, direction) {
|
|
1125
|
+
function compareKey(a, b, { field, direction }, comparators) {
|
|
1095
1126
|
if (field === "assign") {
|
|
1096
1127
|
const aUnassigned = a.assignees.length === 0;
|
|
1097
1128
|
const bUnassigned = b.assignees.length === 0;
|
|
1098
1129
|
if (aUnassigned !== bUnassigned) return aUnassigned ? 1 : -1;
|
|
1099
1130
|
if (aUnassigned && bUnassigned) return 0;
|
|
1100
1131
|
}
|
|
1101
|
-
const base =
|
|
1132
|
+
const base = comparators[field](a, b);
|
|
1102
1133
|
return direction === "desc" ? -base : base;
|
|
1103
1134
|
}
|
|
1104
|
-
function sortIssues(issues, sort) {
|
|
1135
|
+
function sortIssues(issues, sort, stateOrder) {
|
|
1105
1136
|
if (!sort || sort.length === 0) return issues;
|
|
1137
|
+
const comparators = ascComparators(buildStateRank(stateOrder));
|
|
1106
1138
|
return [...issues].sort((a, b) => {
|
|
1107
|
-
for (const
|
|
1108
|
-
const cmp = compareKey(a, b,
|
|
1139
|
+
for (const key of sort) {
|
|
1140
|
+
const cmp = compareKey(a, b, key, comparators);
|
|
1109
1141
|
if (cmp !== 0) return cmp;
|
|
1110
1142
|
}
|
|
1111
1143
|
return 0;
|
|
@@ -1412,18 +1444,21 @@ var IssuesService = class {
|
|
|
1412
1444
|
* `defaultsSort` is the profile-level `defaults.sort`; the effective sort is
|
|
1413
1445
|
* resolved once as `view.sort ?? defaultsSort ?? DEFAULT_SORT` and applied
|
|
1414
1446
|
* both as the per-project server hint and the authoritative client-side sort.
|
|
1447
|
+
* `stateOrder` is the profile-level `defaults.state_order`, used by the
|
|
1448
|
+
* client-side `state` sort to rank states by the configured slug order.
|
|
1415
1449
|
*/
|
|
1416
1450
|
// `signal` is threaded down to each per-project fetch so the dashboard can
|
|
1417
1451
|
// abort an in-flight refresh (e.g. when a new one starts before the previous
|
|
1418
1452
|
// resolves), preventing requests from piling up and timing out. The positional
|
|
1419
1453
|
// list mirrors the call sites; the cap is waived rather than reshaping both.
|
|
1420
1454
|
// eslint-disable-next-line max-params
|
|
1421
|
-
async list(projectIdentifiers, view, queryLimit, defaultsSort, signal) {
|
|
1455
|
+
async list(projectIdentifiers, view, queryLimit, defaultsSort, stateOrder, signal) {
|
|
1422
1456
|
const { issues, failedProjects } = await this.listResilient(
|
|
1423
1457
|
projectIdentifiers,
|
|
1424
1458
|
view,
|
|
1425
1459
|
queryLimit,
|
|
1426
1460
|
defaultsSort,
|
|
1461
|
+
stateOrder,
|
|
1427
1462
|
signal
|
|
1428
1463
|
);
|
|
1429
1464
|
if (failedProjects.length > 0) {
|
|
@@ -1442,7 +1477,7 @@ var IssuesService = class {
|
|
|
1442
1477
|
*/
|
|
1443
1478
|
// Same positional shape as `list`; the cap is waived for the same reason.
|
|
1444
1479
|
// eslint-disable-next-line max-params
|
|
1445
|
-
async listResilient(projectIdentifiers, view, queryLimit, defaultsSort, signal) {
|
|
1480
|
+
async listResilient(projectIdentifiers, view, queryLimit, defaultsSort, stateOrder, signal) {
|
|
1446
1481
|
const assigneeIds = await this.resolveAssigneeIds(view);
|
|
1447
1482
|
const sort = resolveSort(view?.sort, defaultsSort);
|
|
1448
1483
|
const effectiveView = view ? { ...view, sort } : { name: "", sort };
|
|
@@ -1461,7 +1496,7 @@ var IssuesService = class {
|
|
|
1461
1496
|
const byGroup = refineByStateGroup(fetched, view?.filters?.state_group);
|
|
1462
1497
|
const byState = refineByStateSearch(byGroup, view?.filters);
|
|
1463
1498
|
const byAssignee = refineByAssignee(byState, assigneeIds);
|
|
1464
|
-
const sorted = sortIssues(byAssignee, sort);
|
|
1499
|
+
const sorted = sortIssues(byAssignee, sort, stateOrder);
|
|
1465
1500
|
const issues = queryLimit !== void 0 ? sorted.slice(0, queryLimit) : sorted;
|
|
1466
1501
|
return { issues, failedProjects };
|
|
1467
1502
|
}
|
|
@@ -1598,6 +1633,104 @@ var LabelsService = class {
|
|
|
1598
1633
|
}
|
|
1599
1634
|
};
|
|
1600
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
|
+
|
|
1601
1734
|
// src/keybindings/load.ts
|
|
1602
1735
|
import { readFile as readFile3 } from "fs/promises";
|
|
1603
1736
|
import YAML3 from "yaml";
|
|
@@ -1719,6 +1852,24 @@ var ACTIONS = [
|
|
|
1719
1852
|
},
|
|
1720
1853
|
{ id: "detail.comment", context: "detail", description: "comment on issue", defaultKey: "c" },
|
|
1721
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
|
+
},
|
|
1722
1873
|
{
|
|
1723
1874
|
id: "detail.close",
|
|
1724
1875
|
context: "detail",
|
|
@@ -2013,6 +2164,8 @@ async function buildContext(flags) {
|
|
|
2013
2164
|
const users = new UsersService(api, cache);
|
|
2014
2165
|
const states = new StatesService(api, cache);
|
|
2015
2166
|
const labels = new LabelsService(api, cache);
|
|
2167
|
+
const activities = new ActivitiesService(api, cache);
|
|
2168
|
+
const relations = new RelationsService(api, cache);
|
|
2016
2169
|
const issues = new IssuesService(projects, workItems, users);
|
|
2017
2170
|
const { bindings: keybindings, sourcePath: keybindingsSourcePath } = await loadKeybindings();
|
|
2018
2171
|
const runtime = {
|
|
@@ -2032,6 +2185,8 @@ async function buildContext(flags) {
|
|
|
2032
2185
|
users,
|
|
2033
2186
|
states,
|
|
2034
2187
|
labels,
|
|
2188
|
+
activities,
|
|
2189
|
+
relations,
|
|
2035
2190
|
keybindings,
|
|
2036
2191
|
keybindingsSourcePath,
|
|
2037
2192
|
theme: resolveTheme(profile.theme),
|
|
@@ -2413,7 +2568,8 @@ function registerIssue(program2) {
|
|
|
2413
2568
|
projects,
|
|
2414
2569
|
view,
|
|
2415
2570
|
limit ?? view?.query_limit,
|
|
2416
|
-
ctx.runtime.profile.defaults?.sort
|
|
2571
|
+
ctx.runtime.profile.defaults?.sort,
|
|
2572
|
+
ctx.runtime.profile.defaults?.state_order
|
|
2417
2573
|
);
|
|
2418
2574
|
process.stdout.write(renderIssues(issues, format, ctx.theme));
|
|
2419
2575
|
process.stdout.write("\n");
|
|
@@ -2633,7 +2789,7 @@ import React9 from "react";
|
|
|
2633
2789
|
import { render } from "ink";
|
|
2634
2790
|
|
|
2635
2791
|
// src/tui/dashboard.tsx
|
|
2636
|
-
import React7, { useCallback as
|
|
2792
|
+
import React7, { useCallback as useCallback7, useEffect as useEffect7, useMemo as useMemo8, useState as useState14 } from "react";
|
|
2637
2793
|
import { Box as Box12, Text as Text12, useApp, useInput as useInput2 } from "ink";
|
|
2638
2794
|
|
|
2639
2795
|
// src/tui/theme/context.tsx
|
|
@@ -2930,6 +3086,16 @@ var PRIORITY_LETTER = {
|
|
|
2930
3086
|
low: "L",
|
|
2931
3087
|
none: "\xB7"
|
|
2932
3088
|
};
|
|
3089
|
+
var SORT_FIELD_COLUMN = {
|
|
3090
|
+
priority: "priority",
|
|
3091
|
+
state: "state",
|
|
3092
|
+
assign: "assign"
|
|
3093
|
+
};
|
|
3094
|
+
function sortIndicator(column, sort) {
|
|
3095
|
+
const primary = sort?.[0];
|
|
3096
|
+
if (!primary || SORT_FIELD_COLUMN[primary.field] !== column) return "";
|
|
3097
|
+
return primary.direction === "asc" ? " \u2191" : " \u2193";
|
|
3098
|
+
}
|
|
2933
3099
|
function computeViewport(total, selected, rows, previousStart = 0) {
|
|
2934
3100
|
if (rows <= 0 || total === 0) return { start: 0, end: 0 };
|
|
2935
3101
|
if (rows >= total) return { start: 0, end: total };
|
|
@@ -2964,10 +3130,16 @@ function IssueList(props) {
|
|
|
2964
3130
|
return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", borderStyle: "round", paddingX: 1, flexGrow: 1, children: [
|
|
2965
3131
|
/* @__PURE__ */ jsxs4(Box4, { columnGap: 1, children: [
|
|
2966
3132
|
/* @__PURE__ */ jsx5(Box4, { ...cell("key", cols.keyWidth), children: /* @__PURE__ */ jsx5(Text4, { bold: true, children: alignText("KEY", cols.keyWidth, cols.align.key) }) }),
|
|
2967
|
-
/* @__PURE__ */ jsx5(Box4, { ...cell("priority", cols.priorityWidth), children: /* @__PURE__ */ jsx5(Text4, { bold: true, children: cols.compactPriority ? "PR" :
|
|
2968
|
-
cols.showState ? /* @__PURE__ */ jsx5(Box4, { ...cell("state", cols.stateWidth), children: /* @__PURE__ */
|
|
3133
|
+
/* @__PURE__ */ jsx5(Box4, { ...cell("priority", cols.priorityWidth), children: /* @__PURE__ */ jsx5(Text4, { bold: true, children: cols.compactPriority ? "PR" : `PRIORITY${sortIndicator("priority", props.sort)}` }) }),
|
|
3134
|
+
cols.showState ? /* @__PURE__ */ jsx5(Box4, { ...cell("state", cols.stateWidth), children: /* @__PURE__ */ jsxs4(Text4, { bold: true, children: [
|
|
3135
|
+
"STATE",
|
|
3136
|
+
sortIndicator("state", props.sort)
|
|
3137
|
+
] }) }) : null,
|
|
2969
3138
|
/* @__PURE__ */ jsx5(Box4, { ...cell("title", cols.title), children: /* @__PURE__ */ jsx5(Text4, { bold: true, children: "TITLE" }) }),
|
|
2970
|
-
cols.showAssign ? /* @__PURE__ */ jsx5(Box4, { ...cell("assign", cols.assignWidth), children: /* @__PURE__ */
|
|
3139
|
+
cols.showAssign ? /* @__PURE__ */ jsx5(Box4, { ...cell("assign", cols.assignWidth), children: /* @__PURE__ */ jsxs4(Text4, { bold: true, children: [
|
|
3140
|
+
"ASSIGN",
|
|
3141
|
+
sortIndicator("assign", props.sort)
|
|
3142
|
+
] }) }) : null
|
|
2971
3143
|
] }),
|
|
2972
3144
|
hiddenAbove > 0 ? /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
|
|
2973
3145
|
"\u2191 ",
|
|
@@ -3011,7 +3183,7 @@ function IssueList(props) {
|
|
|
3011
3183
|
}
|
|
3012
3184
|
|
|
3013
3185
|
// src/tui/issue-detail.tsx
|
|
3014
|
-
import { useMemo } from "react";
|
|
3186
|
+
import React5, { useMemo } from "react";
|
|
3015
3187
|
import { Box as Box5, Text as Text5 } from "ink";
|
|
3016
3188
|
|
|
3017
3189
|
// src/utils/markdown-to-ansi.ts
|
|
@@ -3168,18 +3340,67 @@ function splitAnsiIntoLines(text, width) {
|
|
|
3168
3340
|
return rows;
|
|
3169
3341
|
}
|
|
3170
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
|
+
|
|
3171
3377
|
// src/tui/issue-detail.tsx
|
|
3172
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
|
+
}
|
|
3173
3387
|
var MODAL_WIDTH = 100;
|
|
3174
3388
|
var PANEL_WIDTH = 50;
|
|
3175
3389
|
var HORIZONTAL_CHROME = 4;
|
|
3176
3390
|
var DETAIL_CHROME_ROWS = 16;
|
|
3177
|
-
function IssueMeta({
|
|
3391
|
+
function IssueMeta({
|
|
3392
|
+
issue,
|
|
3393
|
+
timeInState
|
|
3394
|
+
}) {
|
|
3178
3395
|
const theme = useTheme();
|
|
3179
3396
|
return /* @__PURE__ */ jsxs5(Box5, { marginTop: 1, flexDirection: "column", flexShrink: 0, children: [
|
|
3180
3397
|
/* @__PURE__ */ jsxs5(Text5, { children: [
|
|
3181
3398
|
"state: ",
|
|
3182
|
-
/* @__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
|
|
3183
3404
|
] }),
|
|
3184
3405
|
/* @__PURE__ */ jsxs5(Text5, { children: [
|
|
3185
3406
|
"priority: ",
|
|
@@ -3199,12 +3420,12 @@ function IssueMeta({ issue }) {
|
|
|
3199
3420
|
] })
|
|
3200
3421
|
] });
|
|
3201
3422
|
}
|
|
3202
|
-
function
|
|
3423
|
+
function ScrollableBody(props) {
|
|
3203
3424
|
let content;
|
|
3204
|
-
if (props.loading &&
|
|
3205
|
-
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 });
|
|
3206
3427
|
} else if (props.visible.length === 0 && props.hiddenAbove === 0 && props.hiddenBelow === 0) {
|
|
3207
|
-
content = /* @__PURE__ */ jsx6(Text5, { dimColor: true, children:
|
|
3428
|
+
content = /* @__PURE__ */ jsx6(Text5, { dimColor: true, children: props.emptyLabel });
|
|
3208
3429
|
} else {
|
|
3209
3430
|
content = /* @__PURE__ */ jsxs5(Fragment2, { children: [
|
|
3210
3431
|
props.hiddenAbove > 0 ? /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
|
|
@@ -3222,60 +3443,173 @@ function DescriptionBody(props) {
|
|
|
3222
3443
|
}
|
|
3223
3444
|
return /* @__PURE__ */ jsx6(Box5, { marginTop: 1, flexDirection: "column", overflow: "hidden", children: content });
|
|
3224
3445
|
}
|
|
3225
|
-
function
|
|
3226
|
-
|
|
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;
|
|
3227
3489
|
const end = Math.min(scrollTop + viewportRows, total);
|
|
3228
|
-
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
|
+
);
|
|
3229
3551
|
}
|
|
3230
3552
|
function IssueDetail(props) {
|
|
3231
3553
|
const theme = useTheme();
|
|
3232
3554
|
const variant = props.variant ?? "panel";
|
|
3555
|
+
const mode = props.mode ?? "detail";
|
|
3233
3556
|
const width = variant === "modal" ? MODAL_WIDTH : PANEL_WIDTH;
|
|
3234
3557
|
const contentWidth = width - HORIZONTAL_CHROME;
|
|
3235
3558
|
const description = props.issue?.description;
|
|
3236
|
-
const
|
|
3237
|
-
const
|
|
3238
|
-
() =>
|
|
3239
|
-
[
|
|
3559
|
+
const stateChanges = props.stateChanges;
|
|
3560
|
+
const descriptionLines = useMemo(
|
|
3561
|
+
() => description ? splitAnsiIntoLines(markdownToAnsi(description), contentWidth) : [],
|
|
3562
|
+
[description, contentWidth]
|
|
3240
3563
|
);
|
|
3564
|
+
const activityLines = useMemo(() => {
|
|
3565
|
+
const now = Date.now();
|
|
3566
|
+
return (stateChanges ?? []).map((a) => formatStateChange(a, now)).reverse();
|
|
3567
|
+
}, [stateChanges]);
|
|
3241
3568
|
if (!props.issue) {
|
|
3242
3569
|
return /* @__PURE__ */ jsx6(Box5, { borderStyle: "round", paddingX: 1, width, children: /* @__PURE__ */ jsx6(Text5, { dimColor: true, children: "select an issue" }) });
|
|
3243
3570
|
}
|
|
3244
3571
|
const i = props.issue;
|
|
3245
|
-
const
|
|
3246
|
-
const
|
|
3247
|
-
|
|
3248
|
-
|
|
3572
|
+
const isActivity = mode === "activity";
|
|
3573
|
+
const textBody = resolveBody(
|
|
3574
|
+
isActivity ? "activity" : "detail",
|
|
3575
|
+
isActivity ? activityLines : descriptionLines,
|
|
3576
|
+
props
|
|
3249
3577
|
);
|
|
3250
|
-
const
|
|
3578
|
+
const viewportRows = props.viewportRows ?? textBody.lines.length;
|
|
3579
|
+
const relationsViewport = Math.max(1, viewportRows);
|
|
3251
3580
|
return /* @__PURE__ */ jsxs5(
|
|
3252
3581
|
Box5,
|
|
3253
3582
|
{
|
|
3254
3583
|
flexDirection: "column",
|
|
3255
|
-
|
|
3256
|
-
borderColor: variant === "modal" ? theme.accent : void 0,
|
|
3584
|
+
...frameStyle(variant, theme.accent),
|
|
3257
3585
|
paddingX: 1,
|
|
3258
|
-
paddingY: variant === "modal" ? 1 : 0,
|
|
3259
3586
|
width,
|
|
3260
3587
|
height: props.height,
|
|
3261
3588
|
flexShrink: 0,
|
|
3262
3589
|
overflow: "hidden",
|
|
3263
3590
|
children: [
|
|
3264
|
-
/* @__PURE__ */
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
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
|
+
),
|
|
3268
3602
|
/* @__PURE__ */ jsx6(Text5, { wrap: "truncate", children: i.name }),
|
|
3269
|
-
/* @__PURE__ */ jsx6(IssueMeta, { issue: i }),
|
|
3603
|
+
/* @__PURE__ */ jsx6(IssueMeta, { issue: i, timeInState: props.timeInState }),
|
|
3270
3604
|
/* @__PURE__ */ jsx6(
|
|
3271
|
-
|
|
3605
|
+
DetailBody,
|
|
3272
3606
|
{
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3607
|
+
mode,
|
|
3608
|
+
textBody,
|
|
3609
|
+
relations: props.relations ?? [],
|
|
3610
|
+
relationsSelected: props.relationsSelected ?? 0,
|
|
3611
|
+
relationsLoading: props.relationsLoading ?? false,
|
|
3612
|
+
relationsViewport
|
|
3279
3613
|
}
|
|
3280
3614
|
)
|
|
3281
3615
|
]
|
|
@@ -4372,7 +4706,10 @@ var EMPTY_VIEW_DATA = {
|
|
|
4372
4706
|
failedProjects: []
|
|
4373
4707
|
};
|
|
4374
4708
|
function useViewsData(opts) {
|
|
4375
|
-
const { views, issuesService,
|
|
4709
|
+
const { views, issuesService, defaults, logger } = opts;
|
|
4710
|
+
const defaultProjects = defaults?.projects;
|
|
4711
|
+
const defaultsSort = defaults?.sort;
|
|
4712
|
+
const stateOrder = defaults?.state_order;
|
|
4376
4713
|
const [byView, setByView] = useState6(() => views.map(() => EMPTY_VIEW_DATA));
|
|
4377
4714
|
const viewsRef = useRef4(views);
|
|
4378
4715
|
viewsRef.current = views;
|
|
@@ -4414,6 +4751,7 @@ function useViewsData(opts) {
|
|
|
4414
4751
|
view,
|
|
4415
4752
|
view.query_limit ?? 100,
|
|
4416
4753
|
defaultsSort,
|
|
4754
|
+
stateOrder,
|
|
4417
4755
|
controller.signal
|
|
4418
4756
|
);
|
|
4419
4757
|
patch(viewIdx, {
|
|
@@ -4446,7 +4784,7 @@ function useViewsData(opts) {
|
|
|
4446
4784
|
}
|
|
4447
4785
|
}
|
|
4448
4786
|
},
|
|
4449
|
-
[issuesService, defaultProjects, defaultsSort, logger, patch]
|
|
4787
|
+
[issuesService, defaultProjects, defaultsSort, stateOrder, logger, patch]
|
|
4450
4788
|
);
|
|
4451
4789
|
const refreshAll = useCallback4(() => {
|
|
4452
4790
|
const indices = viewsRef.current.map((_, idx) => idx);
|
|
@@ -4534,23 +4872,17 @@ function useIssueFilter(opts) {
|
|
|
4534
4872
|
import { useCallback as useCallback5, useState as useState9 } from "react";
|
|
4535
4873
|
|
|
4536
4874
|
// src/plane/state-order.ts
|
|
4537
|
-
|
|
4538
|
-
|
|
4539
|
-
"unstarted",
|
|
4540
|
-
"started",
|
|
4541
|
-
"completed",
|
|
4542
|
-
"cancelled"
|
|
4543
|
-
];
|
|
4544
|
-
function orderStates(states) {
|
|
4875
|
+
function orderStates(states, stateOrder) {
|
|
4876
|
+
const rank = buildStateRank(stateOrder);
|
|
4545
4877
|
return states.map((state, index) => ({ state, index })).sort((a, b) => {
|
|
4546
|
-
const
|
|
4547
|
-
const
|
|
4548
|
-
if (
|
|
4878
|
+
const ra = rank(a.state);
|
|
4879
|
+
const rb = rank(b.state);
|
|
4880
|
+
if (ra !== rb) return ra - rb;
|
|
4549
4881
|
return a.index - b.index;
|
|
4550
4882
|
}).map((entry) => entry.state);
|
|
4551
4883
|
}
|
|
4552
|
-
function neighbourState(states, currentId, direction) {
|
|
4553
|
-
const ordered = orderStates(states);
|
|
4884
|
+
function neighbourState(states, currentId, direction, stateOrder) {
|
|
4885
|
+
const ordered = orderStates(states, stateOrder);
|
|
4554
4886
|
const idx = ordered.findIndex((s) => s.id === currentId);
|
|
4555
4887
|
if (idx < 0) return void 0;
|
|
4556
4888
|
const target = idx + direction;
|
|
@@ -4581,7 +4913,8 @@ function useQuickTransition(opts) {
|
|
|
4581
4913
|
if (!issue) return;
|
|
4582
4914
|
try {
|
|
4583
4915
|
const states = await ctx.states.list(projectOf(issue));
|
|
4584
|
-
const
|
|
4916
|
+
const stateOrder = ctx.runtime.profile.defaults?.state_order;
|
|
4917
|
+
const next = neighbourState(states, issue.state.id, direction, stateOrder);
|
|
4585
4918
|
if (!next) {
|
|
4586
4919
|
setMessage(`${issue.key}: already at the ${direction === 1 ? "last" : "first"} state`);
|
|
4587
4920
|
return;
|
|
@@ -4668,6 +5001,183 @@ function useDetailPanel(opts) {
|
|
|
4668
5001
|
};
|
|
4669
5002
|
}
|
|
4670
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
|
+
|
|
4671
5181
|
// src/tui/dashboard.tsx
|
|
4672
5182
|
import { jsx as jsx13, jsxs as jsxs12 } from "react/jsx-runtime";
|
|
4673
5183
|
var NARROW_BREAKPOINT = 100;
|
|
@@ -4817,6 +5327,13 @@ function renderActiveOverlay(opts) {
|
|
|
4817
5327
|
issue: current,
|
|
4818
5328
|
loading: detail.loading,
|
|
4819
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,
|
|
4820
5337
|
scrollTop: detail.scroll,
|
|
4821
5338
|
viewportRows: opts.detailViewportRows,
|
|
4822
5339
|
height: opts.detailModalHeight
|
|
@@ -4847,32 +5364,40 @@ function overlayStatusPosition(opts) {
|
|
|
4847
5364
|
if (opts.isDetail && !opts.current) return void 0;
|
|
4848
5365
|
return opts.listPosition;
|
|
4849
5366
|
}
|
|
5367
|
+
function resolveViewPresentation(view, defaults) {
|
|
5368
|
+
return {
|
|
5369
|
+
layout: resolveLayout(view?.layout, defaults?.layout),
|
|
5370
|
+
sort: resolveSort(view?.sort, defaults?.sort)
|
|
5371
|
+
};
|
|
5372
|
+
}
|
|
4850
5373
|
function Dashboard({ ctx, logger }) {
|
|
4851
5374
|
const { exit } = useApp();
|
|
4852
5375
|
const { rows: terminalRows, columns: terminalCols } = useTerminalSize();
|
|
4853
5376
|
const narrow = isNarrowLayout(terminalCols);
|
|
4854
|
-
const views =
|
|
4855
|
-
const defaultProjects =
|
|
4856
|
-
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(
|
|
4857
5380
|
() => buildViewEntries(views, defaultProjects),
|
|
4858
5381
|
[views, defaultProjects]
|
|
4859
5382
|
);
|
|
4860
|
-
const [viewIdx, setViewIdx] =
|
|
4861
|
-
const [selected, setSelected] =
|
|
4862
|
-
const [statusMessage, setStatusMessage] =
|
|
4863
|
-
const
|
|
4864
|
-
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);
|
|
4865
5391
|
const activeView = views[viewIdx];
|
|
4866
|
-
const activeLayout =
|
|
4867
|
-
() =>
|
|
5392
|
+
const { layout: activeLayout, sort: activeSort } = useMemo8(
|
|
5393
|
+
() => resolveViewPresentation(activeView, ctx.runtime.profile.defaults),
|
|
4868
5394
|
[activeView, ctx]
|
|
4869
5395
|
);
|
|
4870
5396
|
const selectedKeyRef = React7.useRef(void 0);
|
|
4871
5397
|
const viewsData = useViewsData({
|
|
4872
5398
|
views,
|
|
4873
5399
|
issuesService: ctx.issues,
|
|
4874
|
-
|
|
4875
|
-
defaultsSort: ctx.runtime.profile.defaults?.sort,
|
|
5400
|
+
defaults: ctx.runtime.profile.defaults,
|
|
4876
5401
|
logger
|
|
4877
5402
|
});
|
|
4878
5403
|
const active = viewsData.byView[viewIdx] ?? {
|
|
@@ -4886,7 +5411,7 @@ function Dashboard({ ctx, logger }) {
|
|
|
4886
5411
|
const loading = active.loading;
|
|
4887
5412
|
const partialMessage = active.failedProjects.length > 0 ? `partial: ${active.failedProjects.length} project(s) unavailable (${active.failedProjects.join(", ")})` : void 0;
|
|
4888
5413
|
const error = statusMessage ?? active.error ?? partialMessage;
|
|
4889
|
-
const navbarEntries =
|
|
5414
|
+
const navbarEntries = useMemo8(
|
|
4890
5415
|
() => viewEntries.map((entry, idx) => {
|
|
4891
5416
|
const data = viewsData.byView[idx];
|
|
4892
5417
|
return {
|
|
@@ -4898,7 +5423,7 @@ function Dashboard({ ctx, logger }) {
|
|
|
4898
5423
|
[viewEntries, viewsData.byView]
|
|
4899
5424
|
);
|
|
4900
5425
|
const loadView = viewsData.load;
|
|
4901
|
-
const load =
|
|
5426
|
+
const load = useCallback7(
|
|
4902
5427
|
async (preserveSelection = false) => {
|
|
4903
5428
|
const previousKey = preserveSelection ? selectedKeyRef.current : void 0;
|
|
4904
5429
|
const keys = await loadView(viewIdx);
|
|
@@ -4908,12 +5433,12 @@ function Dashboard({ ctx, logger }) {
|
|
|
4908
5433
|
);
|
|
4909
5434
|
const loadedRef = React7.useRef(viewsData.byView);
|
|
4910
5435
|
loadedRef.current = viewsData.byView;
|
|
4911
|
-
|
|
5436
|
+
useEffect7(() => {
|
|
4912
5437
|
if (!activeView) return;
|
|
4913
5438
|
if (loadedRef.current[viewIdx]?.loaded) setSelected(0);
|
|
4914
5439
|
else void load();
|
|
4915
5440
|
}, [viewIdx, activeView, load]);
|
|
4916
|
-
|
|
5441
|
+
useEffect7(() => {
|
|
4917
5442
|
logger.info("dashboard started", {
|
|
4918
5443
|
profile: ctx.runtime.profile_name,
|
|
4919
5444
|
workspace: ctx.runtime.profile.server.workspace_slug,
|
|
@@ -4935,7 +5460,7 @@ function Dashboard({ ctx, logger }) {
|
|
|
4935
5460
|
logger
|
|
4936
5461
|
});
|
|
4937
5462
|
const currentSummary = filtered[selected];
|
|
4938
|
-
|
|
5463
|
+
useEffect7(() => {
|
|
4939
5464
|
selectedKeyRef.current = currentSummary?.key;
|
|
4940
5465
|
}, [currentSummary]);
|
|
4941
5466
|
const comments = useCommentEditor({
|
|
@@ -4957,7 +5482,7 @@ function Dashboard({ ctx, logger }) {
|
|
|
4957
5482
|
name: "",
|
|
4958
5483
|
workspace_id: ""
|
|
4959
5484
|
});
|
|
4960
|
-
const reconcile =
|
|
5485
|
+
const reconcile = useCallback7(
|
|
4961
5486
|
(updated, touchesFilter) => {
|
|
4962
5487
|
if (touchesFilter) void load(true);
|
|
4963
5488
|
else viewsData.patchIssue(viewIdx, updated);
|
|
@@ -4985,7 +5510,7 @@ function Dashboard({ ctx, logger }) {
|
|
|
4985
5510
|
setStatusMessage(message);
|
|
4986
5511
|
}
|
|
4987
5512
|
});
|
|
4988
|
-
const activeProjects =
|
|
5513
|
+
const activeProjects = useMemo8(
|
|
4989
5514
|
() => resolveViewProjectsLenient(activeView ?? { name: "" }, defaultProjects).projects,
|
|
4990
5515
|
[activeView, defaultProjects]
|
|
4991
5516
|
);
|
|
@@ -5031,11 +5556,11 @@ function Dashboard({ ctx, logger }) {
|
|
|
5031
5556
|
creator.active,
|
|
5032
5557
|
transition.active,
|
|
5033
5558
|
helpOpen,
|
|
5034
|
-
|
|
5559
|
+
detailOpen,
|
|
5035
5560
|
filtering
|
|
5036
5561
|
].some(Boolean);
|
|
5037
5562
|
const intervalMs = autoRefreshIntervalMs(ctx.runtime.profile.defaults?.auto_refresh_seconds);
|
|
5038
|
-
|
|
5563
|
+
useEffect7(() => {
|
|
5039
5564
|
if (intervalMs === void 0 || overlayActive || !activeView) return;
|
|
5040
5565
|
const timer = setInterval(() => {
|
|
5041
5566
|
void load(true);
|
|
@@ -5043,12 +5568,30 @@ function Dashboard({ ctx, logger }) {
|
|
|
5043
5568
|
return () => clearInterval(timer);
|
|
5044
5569
|
}, [intervalMs, overlayActive, activeView, load]);
|
|
5045
5570
|
const detail = useDetailPanel({
|
|
5046
|
-
open:
|
|
5047
|
-
target:
|
|
5571
|
+
open: detailOpen,
|
|
5572
|
+
target: stack.current,
|
|
5048
5573
|
ctx,
|
|
5049
5574
|
logger,
|
|
5050
5575
|
setMessage: setStatusMessage
|
|
5051
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]);
|
|
5052
5595
|
const viewportRows = listViewportRows({
|
|
5053
5596
|
terminalRows,
|
|
5054
5597
|
filtering,
|
|
@@ -5058,7 +5601,7 @@ function Dashboard({ ctx, logger }) {
|
|
|
5058
5601
|
const STATUS_BAR_ROWS = 3;
|
|
5059
5602
|
const detailModalHeight = Math.max(DETAIL_CHROME_ROWS + 3, terminalRows - STATUS_BAR_ROWS);
|
|
5060
5603
|
const detailViewportRows = Math.max(3, detailModalHeight - DETAIL_CHROME_ROWS);
|
|
5061
|
-
const openSelectedInBrowser =
|
|
5604
|
+
const openSelectedInBrowser = useCallback7(() => {
|
|
5062
5605
|
const issue = filtered[selected];
|
|
5063
5606
|
if (!issue) return;
|
|
5064
5607
|
try {
|
|
@@ -5091,7 +5634,9 @@ function Dashboard({ ctx, logger }) {
|
|
|
5091
5634
|
"list.page-up": () => setSelected((s) => Math.max(0, s - viewportRows)),
|
|
5092
5635
|
"list.top": () => setSelected(0),
|
|
5093
5636
|
"list.bottom": () => setSelected(Math.max(0, filtered.length - 1)),
|
|
5094
|
-
"list.open-detail": () =>
|
|
5637
|
+
"list.open-detail": () => {
|
|
5638
|
+
if (currentSummary) stack.open(targetFromIssue(currentSummary));
|
|
5639
|
+
},
|
|
5095
5640
|
"list.open-browser": openSelectedInBrowser,
|
|
5096
5641
|
"list.comment": comments.open,
|
|
5097
5642
|
"list.edit": editor.open,
|
|
@@ -5114,13 +5659,29 @@ function Dashboard({ ctx, logger }) {
|
|
|
5114
5659
|
);
|
|
5115
5660
|
if (!consumed && (input2 === "q" || key.escape)) setHelpOpen(false);
|
|
5116
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]);
|
|
5117
5675
|
const handleDetailKey = (input2, key) => {
|
|
5676
|
+
const relationsMode = detailMode === "relations";
|
|
5677
|
+
const relationCount = relations.relations.length;
|
|
5118
5678
|
const detailHandlers = {
|
|
5119
|
-
|
|
5120
|
-
"detail.
|
|
5121
|
-
"detail.scroll-down
|
|
5122
|
-
"detail.scroll-
|
|
5123
|
-
"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),
|
|
5124
5685
|
"detail.page-down": () => detail.scrollBy(detailViewportRows),
|
|
5125
5686
|
"detail.page-up": () => detail.scrollBy(-detailViewportRows),
|
|
5126
5687
|
"detail.top": detail.scrollTop,
|
|
@@ -5128,10 +5689,24 @@ function Dashboard({ ctx, logger }) {
|
|
|
5128
5689
|
"detail.open-browser": openSelectedInBrowser,
|
|
5129
5690
|
"detail.comment": comments.open,
|
|
5130
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
|
+
},
|
|
5131
5706
|
"global.help": () => setHelpOpen((open2) => !open2),
|
|
5132
5707
|
"global.refresh": () => void load(true),
|
|
5133
5708
|
"global.refresh-all": () => viewsData.refreshAll(),
|
|
5134
|
-
"global.quit": () =>
|
|
5709
|
+
"global.quit": () => stack.close()
|
|
5135
5710
|
};
|
|
5136
5711
|
dispatch(ctx.keybindings, ["detail", "global"], detailHandlers, input2, key);
|
|
5137
5712
|
};
|
|
@@ -5141,7 +5716,7 @@ function Dashboard({ ctx, logger }) {
|
|
|
5141
5716
|
[editor.active, editor.handleKey],
|
|
5142
5717
|
[comments.active, comments.handleKey],
|
|
5143
5718
|
[helpOpen, handleHelpKey],
|
|
5144
|
-
[
|
|
5719
|
+
[detailOpen, handleDetailKey],
|
|
5145
5720
|
[filtering, handleFilterKey]
|
|
5146
5721
|
];
|
|
5147
5722
|
useInput2((input2, key) => {
|
|
@@ -5149,7 +5724,7 @@ function Dashboard({ ctx, logger }) {
|
|
|
5149
5724
|
if (route) return route[1](input2, key);
|
|
5150
5725
|
dispatch(ctx.keybindings, ["global", "list", "view", "filter"], handlers, input2, key);
|
|
5151
5726
|
});
|
|
5152
|
-
const current = detail.detailed ?? currentSummary;
|
|
5727
|
+
const current = detail.detailed ?? (stack.canGoBack ? void 0 : currentSummary);
|
|
5153
5728
|
const statusBarBase = {
|
|
5154
5729
|
profile: ctx.runtime.profile_name,
|
|
5155
5730
|
workspace: ctx.runtime.profile.server.workspace_slug,
|
|
@@ -5162,13 +5737,17 @@ function Dashboard({ ctx, logger }) {
|
|
|
5162
5737
|
total: issues.length,
|
|
5163
5738
|
filtering: Boolean(filter)
|
|
5164
5739
|
});
|
|
5165
|
-
const isDetail =
|
|
5740
|
+
const isDetail = detailOpen;
|
|
5166
5741
|
const overlay = renderActiveOverlay({
|
|
5167
5742
|
transition,
|
|
5168
5743
|
creator,
|
|
5169
5744
|
editor,
|
|
5170
5745
|
comments,
|
|
5171
5746
|
detail,
|
|
5747
|
+
activity,
|
|
5748
|
+
relations,
|
|
5749
|
+
relationsSelected,
|
|
5750
|
+
detailMode,
|
|
5172
5751
|
helpOpen,
|
|
5173
5752
|
isDetail,
|
|
5174
5753
|
currentSummary,
|
|
@@ -5198,6 +5777,7 @@ function Dashboard({ ctx, logger }) {
|
|
|
5198
5777
|
filtering,
|
|
5199
5778
|
viewportRows,
|
|
5200
5779
|
layout: activeLayout,
|
|
5780
|
+
sort: activeSort,
|
|
5201
5781
|
loading,
|
|
5202
5782
|
statusBar: /* @__PURE__ */ jsx13(StatusBar, { ...statusBarBase, loading, position: listPosition })
|
|
5203
5783
|
}
|
|
@@ -5225,6 +5805,7 @@ function ListLayout(props) {
|
|
|
5225
5805
|
viewportRows: props.viewportRows,
|
|
5226
5806
|
width: props.narrow ? props.width : props.width - SIDE_PANEL_WIDTH,
|
|
5227
5807
|
layout: props.layout,
|
|
5808
|
+
sort: props.sort,
|
|
5228
5809
|
loading: props.loading
|
|
5229
5810
|
}
|
|
5230
5811
|
),
|