mintree 0.4.0 → 0.4.2
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/commands/dashboard.js +86 -34
- package/dist/lib/dashboard.js +9 -0
- package/dist/lib/priority.d.ts +35 -0
- package/dist/lib/priority.js +44 -0
- package/dist/lib/providers/github.js +2 -0
- package/dist/lib/providers/linear.js +5 -0
- package/dist/lib/providers/types.d.ts +1 -0
- package/package.json +1 -1
|
@@ -16,9 +16,24 @@ import { buildCreateMarkers, emitMarkers } from "../lib/markers.js";
|
|
|
16
16
|
import { readMetadata } from "../lib/metadata.js";
|
|
17
17
|
import { createProvider } from "../lib/providers/index.js";
|
|
18
18
|
import { loadDashboard } from "../lib/dashboard.js";
|
|
19
|
+
import { priorityDisplay } from "../lib/priority.js";
|
|
19
20
|
const require = createRequire(import.meta.url);
|
|
20
21
|
const { version: mintreeVersion } = require("../../package.json");
|
|
21
22
|
export const description = "Interactive dashboard listing open issues assigned to you with worktree + session state";
|
|
23
|
+
function isOrphan(d) {
|
|
24
|
+
return d.orphan === true;
|
|
25
|
+
}
|
|
26
|
+
function tabIssues(issues, tab) {
|
|
27
|
+
return issues.filter((d) => (tab === "issues" ? !isOrphan(d) : isOrphan(d)));
|
|
28
|
+
}
|
|
29
|
+
function currentSelected(s) {
|
|
30
|
+
const displayed = tabIssues(s.issues, s.activeTab);
|
|
31
|
+
const selectedIndex = s.activeTab === "issues" ? s.issuesIndex : s.worktreesIndex;
|
|
32
|
+
return { displayed, selectedIndex };
|
|
33
|
+
}
|
|
34
|
+
function withSelectedIndex(s, next) {
|
|
35
|
+
return s.activeTab === "issues" ? { ...s, issuesIndex: next } : { ...s, worktreesIndex: next };
|
|
36
|
+
}
|
|
22
37
|
// xterm/iTerm/etc switch to the alternate screen buffer with these escape
|
|
23
38
|
// codes. Using the buffer means the dashboard owns the whole window for its
|
|
24
39
|
// lifetime, and the previous shell content reappears unchanged the moment
|
|
@@ -152,8 +167,10 @@ function useTerminalSize() {
|
|
|
152
167
|
}, [stdout]);
|
|
153
168
|
return size;
|
|
154
169
|
}
|
|
155
|
-
function HeaderRow({ repoName, claudeVersion, issueCount, updateAvailable, }) {
|
|
156
|
-
|
|
170
|
+
function HeaderRow({ repoName, claudeVersion, issueCount, worktreeCount, activeTab, updateAvailable, }) {
|
|
171
|
+
const issuesLabel = ` Issues (${issueCount}) `;
|
|
172
|
+
const worktreesLabel = ` Worktrees (${worktreeCount}) `;
|
|
173
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: "green", children: "mintree" }), _jsx(Text, { dimColor: true, children: ` v${mintreeVersion}` }), updateAvailable && _jsx(Text, { color: "yellow", children: " (*)" }), claudeVersion && _jsx(Text, { dimColor: true, children: ` · claude ${claudeVersion}` }), repoName && _jsx(Text, { dimColor: true, children: ` · ${repoName}` })] }), _jsxs(Box, { children: [activeTab === "issues" ? (_jsx(Text, { bold: true, backgroundColor: "cyan", color: "black", children: issuesLabel })) : (_jsx(Text, { dimColor: true, children: issuesLabel })), _jsx(Text, { children: " " }), activeTab === "worktrees" ? (_jsx(Text, { bold: true, backgroundColor: "cyan", color: "black", children: worktreesLabel })) : (_jsx(Text, { dimColor: true, children: worktreesLabel })), _jsx(Text, { dimColor: true, children: " ← / → switch tab" })] })] }));
|
|
157
174
|
}
|
|
158
175
|
function FooterRow({ phase, overlayKind, latestVersion, listWidth, }) {
|
|
159
176
|
if (phase === "error") {
|
|
@@ -204,13 +221,17 @@ function IssueListRow({ d, selected, identifierWidth, rowWidth, }) {
|
|
|
204
221
|
// Status-coloured leading dot — same convention as santree. Falls back to
|
|
205
222
|
// gray when the issue has no project board membership.
|
|
206
223
|
const dotColor = d.project?.statusColor ?? "gray";
|
|
224
|
+
// Compact priority glyph (Linear only; GitHub rows render a blank). The
|
|
225
|
+
// fixed single-width icon keeps the ids aligned whether or not a row has a
|
|
226
|
+
// priority. See lib/priority.ts.
|
|
227
|
+
const prio = priorityDisplay(d.issue.priority);
|
|
207
228
|
const title = d.issue.title;
|
|
208
229
|
// The leading-dot Text and the rest are nested under a single Text so the
|
|
209
230
|
// selection background paints the whole row in one contiguous block.
|
|
210
231
|
// `wrap="truncate"` clamps the row to a single line and Ink renders an
|
|
211
232
|
// ellipsis at the cut. The outer Box has a fixed width so the wrap
|
|
212
233
|
// behaviour knows where to truncate.
|
|
213
|
-
return (_jsx(Box, { width: rowWidth, children: _jsxs(Text, { wrap: "truncate", backgroundColor: selected ? "blue" : undefined, color: selected ? "white" : undefined, children: [" ", _jsx(Text, { color: selected ? "white" : dotColor, children: "\u25CF" }), `
|
|
234
|
+
return (_jsx(Box, { width: rowWidth, children: _jsxs(Text, { wrap: "truncate", backgroundColor: selected ? "blue" : undefined, color: selected ? "white" : undefined, children: [" ", _jsx(Text, { color: selected ? "white" : dotColor, children: "\u25CF" }), " ", _jsx(Text, { color: selected ? "white" : prio.color, children: prio.icon }), ` ${idText} ${title}`] }) }));
|
|
214
235
|
}
|
|
215
236
|
// A project board header — the top level of the grouped issue list. Mirrors
|
|
216
237
|
// the bold project name + dim count seen in the santree dashboard.
|
|
@@ -646,18 +667,32 @@ export default function Dashboard() {
|
|
|
646
667
|
return;
|
|
647
668
|
}
|
|
648
669
|
setState((prev) => {
|
|
649
|
-
const
|
|
650
|
-
const
|
|
651
|
-
const
|
|
652
|
-
const
|
|
653
|
-
const
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
const
|
|
670
|
+
const prevReady = prev.phase === "ready" ? prev : null;
|
|
671
|
+
const activeTab = prevReady?.activeTab ?? "issues";
|
|
672
|
+
const previousIssuesIndex = prevReady?.issuesIndex ?? 0;
|
|
673
|
+
const previousWorktreesIndex = prevReady?.worktreesIndex ?? 0;
|
|
674
|
+
const previousOverlay = prevReady?.overlay ?? null;
|
|
675
|
+
const previousToast = prevReady?.toast ?? null;
|
|
676
|
+
const previousScroll = prevReady?.detailScrollOffset ?? 0;
|
|
677
|
+
const issuesList = tabIssues(issues, "issues");
|
|
678
|
+
const worktreesList = tabIssues(issues, "worktrees");
|
|
679
|
+
const issuesIndex = Math.min(previousIssuesIndex, Math.max(0, issuesList.length - 1));
|
|
680
|
+
const worktreesIndex = Math.min(previousWorktreesIndex, Math.max(0, worktreesList.length - 1));
|
|
681
|
+
// Preserve scroll only when the active tab's selected issue still
|
|
682
|
+
// resolves to the same row — clamping or list churn means the user
|
|
683
|
+
// is now reading something else.
|
|
684
|
+
const prevDisplayed = prevReady ? tabIssues(prevReady.issues, activeTab) : [];
|
|
685
|
+
const nextDisplayed = activeTab === "issues" ? issuesList : worktreesList;
|
|
686
|
+
const prevSelectedId = prevDisplayed[activeTab === "issues" ? previousIssuesIndex : previousWorktreesIndex]?.issue
|
|
687
|
+
.id ?? null;
|
|
688
|
+
const nextSelectedId = nextDisplayed[activeTab === "issues" ? issuesIndex : worktreesIndex]?.issue.id ?? null;
|
|
689
|
+
const detailScrollOffset = prevSelectedId !== null && prevSelectedId === nextSelectedId ? previousScroll : 0;
|
|
657
690
|
return {
|
|
658
691
|
phase: "ready",
|
|
659
692
|
issues,
|
|
660
|
-
|
|
693
|
+
activeTab,
|
|
694
|
+
issuesIndex,
|
|
695
|
+
worktreesIndex,
|
|
661
696
|
detailScrollOffset,
|
|
662
697
|
refreshing: false,
|
|
663
698
|
overlay: previousOverlay,
|
|
@@ -714,10 +749,11 @@ export default function Dashboard() {
|
|
|
714
749
|
if (prev.overlay)
|
|
715
750
|
return prev; // overlay pauses scroll routing
|
|
716
751
|
if (inLeftPane) {
|
|
717
|
-
const
|
|
718
|
-
|
|
752
|
+
const { displayed, selectedIndex } = currentSelected(prev);
|
|
753
|
+
const next = Math.max(0, Math.min(displayed.length - 1, selectedIndex + delta));
|
|
754
|
+
if (next === selectedIndex)
|
|
719
755
|
return prev;
|
|
720
|
-
return { ...prev,
|
|
756
|
+
return { ...withSelectedIndex(prev, next), detailScrollOffset: 0 };
|
|
721
757
|
}
|
|
722
758
|
const next = Math.max(0, prev.detailScrollOffset + delta);
|
|
723
759
|
if (next === prev.detailScrollOffset)
|
|
@@ -788,20 +824,23 @@ export default function Dashboard() {
|
|
|
788
824
|
}
|
|
789
825
|
if (state.phase !== "ready")
|
|
790
826
|
return;
|
|
827
|
+
if (key.leftArrow || key.rightArrow) {
|
|
828
|
+
// Two tabs only — either arrow toggles. Per-tab indices are
|
|
829
|
+
// preserved, so the user returns to the row they left.
|
|
830
|
+
const next = state.activeTab === "issues" ? "worktrees" : "issues";
|
|
831
|
+
setState({ ...state, activeTab: next, detailScrollOffset: 0 });
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
791
834
|
if (key.upArrow || input === "k") {
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
detailScrollOffset: 0,
|
|
796
|
-
});
|
|
835
|
+
const { selectedIndex } = currentSelected(state);
|
|
836
|
+
const nextIndex = Math.max(0, selectedIndex - 1);
|
|
837
|
+
setState({ ...withSelectedIndex(state, nextIndex), detailScrollOffset: 0 });
|
|
797
838
|
return;
|
|
798
839
|
}
|
|
799
840
|
if (key.downArrow || input === "j") {
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
detailScrollOffset: 0,
|
|
804
|
-
});
|
|
841
|
+
const { displayed, selectedIndex } = currentSelected(state);
|
|
842
|
+
const nextIndex = Math.min(Math.max(0, displayed.length - 1), selectedIndex + 1);
|
|
843
|
+
setState({ ...withSelectedIndex(state, nextIndex), detailScrollOffset: 0 });
|
|
805
844
|
return;
|
|
806
845
|
}
|
|
807
846
|
if (key.pageUp) {
|
|
@@ -824,7 +863,8 @@ export default function Dashboard() {
|
|
|
824
863
|
return;
|
|
825
864
|
}
|
|
826
865
|
if (input === "o") {
|
|
827
|
-
const
|
|
866
|
+
const { displayed, selectedIndex } = currentSelected(state);
|
|
867
|
+
const issue = displayed[selectedIndex];
|
|
828
868
|
// Orphan rows carry an empty URL — nothing to open. Skip silently
|
|
829
869
|
// rather than asking the OS to open an empty string.
|
|
830
870
|
if (issue && issue.issue.url)
|
|
@@ -832,7 +872,8 @@ export default function Dashboard() {
|
|
|
832
872
|
return;
|
|
833
873
|
}
|
|
834
874
|
if (input === "w") {
|
|
835
|
-
const
|
|
875
|
+
const { displayed, selectedIndex } = currentSelected(state);
|
|
876
|
+
const issue = displayed[selectedIndex];
|
|
836
877
|
if (!issue)
|
|
837
878
|
return;
|
|
838
879
|
if (issue.worktree) {
|
|
@@ -843,7 +884,8 @@ export default function Dashboard() {
|
|
|
843
884
|
return;
|
|
844
885
|
}
|
|
845
886
|
if (key.return) {
|
|
846
|
-
const
|
|
887
|
+
const { displayed, selectedIndex } = currentSelected(state);
|
|
888
|
+
const issue = displayed[selectedIndex];
|
|
847
889
|
if (!issue)
|
|
848
890
|
return;
|
|
849
891
|
if (issue.worktree) {
|
|
@@ -859,7 +901,8 @@ export default function Dashboard() {
|
|
|
859
901
|
return;
|
|
860
902
|
}
|
|
861
903
|
if (input === "d") {
|
|
862
|
-
const
|
|
904
|
+
const { displayed, selectedIndex } = currentSelected(state);
|
|
905
|
+
const issue = displayed[selectedIndex];
|
|
863
906
|
if (!issue || !issue.worktree)
|
|
864
907
|
return;
|
|
865
908
|
setState({
|
|
@@ -1129,8 +1172,11 @@ export default function Dashboard() {
|
|
|
1129
1172
|
if (state.phase === "error") {
|
|
1130
1173
|
return (_jsxs(Box, { width: columns, height: rows, flexDirection: "column", borderStyle: "round", borderColor: "red", paddingX: 1, children: [_jsxs(Text, { color: "red", bold: true, children: ["\u2717 ", state.message] }), state.hint && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "yellow", children: ["\u21B3 ", state.hint] }) })), _jsx(Box, { marginTop: 1, children: _jsx(FooterRow, { phase: "error" }) })] }));
|
|
1131
1174
|
}
|
|
1132
|
-
const { issues,
|
|
1133
|
-
const
|
|
1175
|
+
const { issues, refreshing, overlay, toast, activeTab } = state;
|
|
1176
|
+
const { displayed, selectedIndex } = currentSelected(state);
|
|
1177
|
+
const selected = displayed[selectedIndex] ?? null;
|
|
1178
|
+
const issuesTabCount = issues.reduce((n, d) => (isOrphan(d) ? n : n + 1), 0);
|
|
1179
|
+
const worktreesTabCount = issues.length - issuesTabCount;
|
|
1134
1180
|
const onOverlayDescChange = (next) => {
|
|
1135
1181
|
if (state.phase !== "ready" || !state.overlay)
|
|
1136
1182
|
return;
|
|
@@ -1157,7 +1203,7 @@ export default function Dashboard() {
|
|
|
1157
1203
|
const listWidthPct = 0.5;
|
|
1158
1204
|
const listWidth = Math.max(32, Math.floor(columns * listWidthPct));
|
|
1159
1205
|
const detailWidth = columns - listWidth - 2; // border slack
|
|
1160
|
-
const identifierWidth = Math.max(3, ...
|
|
1206
|
+
const identifierWidth = Math.max(3, ...displayed.map((d) => d.issue.id.length));
|
|
1161
1207
|
// Reserve rows: header (2), top borders (1), footer (3).
|
|
1162
1208
|
const listVisibleRows = Math.max(3, rows - 9);
|
|
1163
1209
|
// Detail pane content height inside the bordered box. Header eats 2 rows,
|
|
@@ -1171,8 +1217,14 @@ export default function Dashboard() {
|
|
|
1171
1217
|
// Grouped list: build the project/status header rows interleaved with
|
|
1172
1218
|
// issue rows, then split into a sticky header region (the selected issue's
|
|
1173
1219
|
// project + Status, pinned to the top) and a windowed scrollable body.
|
|
1174
|
-
|
|
1220
|
+
// The Worktrees tab renders flat — the tab title already labels the group,
|
|
1221
|
+
// so the per-project headers would just be visual noise.
|
|
1222
|
+
const listRows = activeTab === "issues"
|
|
1223
|
+
? buildListRows(displayed)
|
|
1224
|
+
: displayed.map((d, index) => ({ kind: "issue", d, index }));
|
|
1175
1225
|
const listView = windowListRows(listRows, selectedIndex, listVisibleRows);
|
|
1176
1226
|
const listContentWidth = Math.max(8, listWidth - 4);
|
|
1177
|
-
return (_jsxs(Box, { flexDirection: "column", width: columns, height: rows, children: [_jsx(Box, { paddingX: 1, paddingTop: 0, flexDirection: "column", children: _jsx(HeaderRow, { repoName: repoName, claudeVersion: claudeVersion, issueCount:
|
|
1227
|
+
return (_jsxs(Box, { flexDirection: "column", width: columns, height: rows, children: [_jsx(Box, { paddingX: 1, paddingTop: 0, flexDirection: "column", children: _jsx(HeaderRow, { repoName: repoName, claudeVersion: claudeVersion, issueCount: issuesTabCount, worktreeCount: worktreesTabCount, activeTab: activeTab, updateAvailable: latestVersion !== null }) }), overlay ? (_jsx(Box, { flexGrow: 1, flexDirection: "column", borderStyle: "round", borderColor: overlay.kind === "remove" ? "yellow" : "cyan", children: overlay.kind === "create" ? (_jsx(CreateOverlayView, { overlay: overlay, onDescChange: onOverlayDescChange, onPromptChange: onOverlayPromptChange })) : (_jsx(RemoveOverlayView, { overlay: overlay })) })) : (_jsxs(Box, { flexGrow: 1, flexDirection: "row", children: [_jsx(Box, { width: listWidth, flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, children: displayed.length === 0 ? (_jsx(Text, { dimColor: true, children: activeTab === "issues"
|
|
1228
|
+
? "No open issues assigned to you in this repo."
|
|
1229
|
+
: "No orphaned worktrees — anything in `.mintree/worktrees/` matches an open issue." })) : (_jsxs(_Fragment, { children: [listView.sticky.map((row, i) => (_jsx(ListRowView, { row: row, selectedIndex: selectedIndex, identifierWidth: identifierWidth, width: listContentWidth }, `sticky-${i}`))), listView.issuesAbove > 0 && (_jsxs(Text, { dimColor: true, children: ["\u2191 ", listView.issuesAbove, " more above"] })), listView.body.map((row, i) => (_jsx(ListRowView, { row: row, selectedIndex: selectedIndex, identifierWidth: identifierWidth, width: listContentWidth }, `body-${i}`))), listView.issuesBelow > 0 && (_jsxs(Text, { dimColor: true, children: ["\u2193 ", listView.issuesBelow, " more below"] }))] })) }), _jsx(Box, { width: detailWidth, flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, children: _jsx(DetailPane, { d: selected, contentWidth: detailWidth - 4, contentHeight: detailContentHeight, scrollOffset: state.detailScrollOffset }) })] })), _jsxs(Box, { paddingX: 1, flexDirection: "column", children: [toast && (_jsx(Box, { children: _jsxs(Text, { color: toast.kind === "success" ? "green" : toast.kind === "error" ? "red" : "cyan", children: [toast.kind === "success" ? "✓ " : toast.kind === "error" ? "✗ " : "· ", toast.text] }) })), refreshing && (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { dimColor: true, children: " refreshing" })] })), _jsx(FooterRow, { phase: "ready", overlayKind: overlay?.kind, latestVersion: latestVersion, listWidth: listWidth })] })] }));
|
|
1178
1230
|
}
|
package/dist/lib/dashboard.js
CHANGED
|
@@ -3,6 +3,7 @@ import * as path from "path";
|
|
|
3
3
|
import { listWorktrees, getWorktreesDir, isDirty, getAheadBehind, } from "./git.js";
|
|
4
4
|
import { readMetadata } from "./metadata.js";
|
|
5
5
|
import { fetchPrForBranch } from "./pr.js";
|
|
6
|
+
import { prioritySortRank } from "./priority.js";
|
|
6
7
|
import { createProvider } from "./providers/index.js";
|
|
7
8
|
/**
|
|
8
9
|
* Builds a map from issue id (the canonical string — "100" on GitHub,
|
|
@@ -95,6 +96,13 @@ function sortGroupedIssues(issues, configuredUrl) {
|
|
|
95
96
|
return a.project.statusOrder - b.project.statusOrder;
|
|
96
97
|
}
|
|
97
98
|
}
|
|
99
|
+
// Within a status group, surface higher-priority issues first
|
|
100
|
+
// (Urgent → Low; "no priority" sinks to the bottom). Orphans and
|
|
101
|
+
// GitHub rows have null priority and so fall through to the date sort.
|
|
102
|
+
const pa = prioritySortRank(a.issue.priority);
|
|
103
|
+
const pb = prioritySortRank(b.issue.priority);
|
|
104
|
+
if (pa !== pb)
|
|
105
|
+
return pa - pb;
|
|
98
106
|
// Newest-first for issues — id is a numeric-or-prefixed string. Numeric
|
|
99
107
|
// compare falls back to localeCompare for non-numeric ids (Linear's
|
|
100
108
|
// "FE-123" form).
|
|
@@ -137,6 +145,7 @@ function buildOrphanRows(worktreesByIssue, assignedIds, sessionLookup, prByBranc
|
|
|
137
145
|
body: "",
|
|
138
146
|
createdAt: "",
|
|
139
147
|
updatedAt: "",
|
|
148
|
+
priority: null,
|
|
140
149
|
},
|
|
141
150
|
worktree,
|
|
142
151
|
session: sessionLookup(issueId),
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Issue priority, normalised across providers.
|
|
3
|
+
*
|
|
4
|
+
* Linear exposes a native `priority` field on the 0-4 scale:
|
|
5
|
+
* 0 = No priority, 1 = Urgent, 2 = High, 3 = Medium, 4 = Low.
|
|
6
|
+
* GitHub Issues has no native priority concept, so its provider always yields
|
|
7
|
+
* `null` here — the dashboard simply renders no priority glyph for those rows.
|
|
8
|
+
*
|
|
9
|
+
* `ProviderIssue.priority` stores the raw Linear number (or null), and these
|
|
10
|
+
* helpers turn it into a compact dashboard glyph and a sort rank. Keeping the
|
|
11
|
+
* mapping in one module means the render path (dashboard.tsx) and the sort
|
|
12
|
+
* path (dashboard.ts) stay in lock-step.
|
|
13
|
+
*/
|
|
14
|
+
export type PriorityValue = number | null | undefined;
|
|
15
|
+
export type PriorityDisplay = {
|
|
16
|
+
/** Human label, e.g. "Urgent". Empty string when there's no priority. */
|
|
17
|
+
label: string;
|
|
18
|
+
/** Single-width glyph for the list row. A space when there's no priority. */
|
|
19
|
+
icon: string;
|
|
20
|
+
/** Ink-renderable colour name for the glyph. */
|
|
21
|
+
color: string;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Maps a raw priority value to its dashboard glyph. Urgent reads as a bold
|
|
25
|
+
* red bang; High/Medium/Low use arrows that step down in weight and colour.
|
|
26
|
+
* "No priority" (0) and null both render as a blank, keeping rows aligned
|
|
27
|
+
* without drawing the eye.
|
|
28
|
+
*/
|
|
29
|
+
export declare function priorityDisplay(priority: PriorityValue): PriorityDisplay;
|
|
30
|
+
/**
|
|
31
|
+
* Sort rank for "highest priority first". Urgent (1) sorts before Low (4);
|
|
32
|
+
* "No priority" (0) and null sort last. Used as a tie-break inside a status
|
|
33
|
+
* group before the newest-first fallback.
|
|
34
|
+
*/
|
|
35
|
+
export declare function prioritySortRank(priority: PriorityValue): number;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Issue priority, normalised across providers.
|
|
3
|
+
*
|
|
4
|
+
* Linear exposes a native `priority` field on the 0-4 scale:
|
|
5
|
+
* 0 = No priority, 1 = Urgent, 2 = High, 3 = Medium, 4 = Low.
|
|
6
|
+
* GitHub Issues has no native priority concept, so its provider always yields
|
|
7
|
+
* `null` here — the dashboard simply renders no priority glyph for those rows.
|
|
8
|
+
*
|
|
9
|
+
* `ProviderIssue.priority` stores the raw Linear number (or null), and these
|
|
10
|
+
* helpers turn it into a compact dashboard glyph and a sort rank. Keeping the
|
|
11
|
+
* mapping in one module means the render path (dashboard.tsx) and the sort
|
|
12
|
+
* path (dashboard.ts) stay in lock-step.
|
|
13
|
+
*/
|
|
14
|
+
const NONE = { label: "", icon: " ", color: "gray" };
|
|
15
|
+
/**
|
|
16
|
+
* Maps a raw priority value to its dashboard glyph. Urgent reads as a bold
|
|
17
|
+
* red bang; High/Medium/Low use arrows that step down in weight and colour.
|
|
18
|
+
* "No priority" (0) and null both render as a blank, keeping rows aligned
|
|
19
|
+
* without drawing the eye.
|
|
20
|
+
*/
|
|
21
|
+
export function priorityDisplay(priority) {
|
|
22
|
+
switch (priority) {
|
|
23
|
+
case 1:
|
|
24
|
+
return { label: "Urgent", icon: "!", color: "red" };
|
|
25
|
+
case 2:
|
|
26
|
+
return { label: "High", icon: "↑", color: "red" };
|
|
27
|
+
case 3:
|
|
28
|
+
return { label: "Medium", icon: "=", color: "yellow" };
|
|
29
|
+
case 4:
|
|
30
|
+
return { label: "Low", icon: "↓", color: "blue" };
|
|
31
|
+
default:
|
|
32
|
+
return NONE;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Sort rank for "highest priority first". Urgent (1) sorts before Low (4);
|
|
37
|
+
* "No priority" (0) and null sort last. Used as a tie-break inside a status
|
|
38
|
+
* group before the newest-first fallback.
|
|
39
|
+
*/
|
|
40
|
+
export function prioritySortRank(priority) {
|
|
41
|
+
if (priority == null || priority === 0)
|
|
42
|
+
return Number.POSITIVE_INFINITY;
|
|
43
|
+
return priority;
|
|
44
|
+
}
|
|
@@ -271,6 +271,7 @@ const BOOTSTRAP_QUERY = /* GraphQL */ `
|
|
|
271
271
|
title
|
|
272
272
|
description
|
|
273
273
|
url
|
|
274
|
+
priority
|
|
274
275
|
createdAt
|
|
275
276
|
updatedAt
|
|
276
277
|
team {
|
|
@@ -329,6 +330,10 @@ function mapIssueToProviderIssue(wi) {
|
|
|
329
330
|
body: wi.description ?? "",
|
|
330
331
|
createdAt: wi.createdAt ?? "",
|
|
331
332
|
updatedAt: wi.updatedAt ?? "",
|
|
333
|
+
// Linear sends 0 for "No priority"; normalise it (and any missing
|
|
334
|
+
// value) to null so the dashboard treats it the same as GitHub's
|
|
335
|
+
// no-priority rows.
|
|
336
|
+
priority: wi.priority && wi.priority > 0 ? wi.priority : null,
|
|
332
337
|
};
|
|
333
338
|
}
|
|
334
339
|
export class LinearProvider {
|
package/package.json
CHANGED