@arnavpisces/sutra 1.0.7 → 1.0.9
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/api/jira-client.d.ts +33 -1
- package/dist/api/jira-client.d.ts.map +1 -1
- package/dist/api/jira-client.js +132 -23
- package/dist/api/jira-client.js.map +1 -1
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +93 -6
- package/dist/app.js.map +1 -1
- package/dist/components/common/EditableTextBox.d.ts.map +1 -1
- package/dist/components/common/EditableTextBox.js +58 -0
- package/dist/components/common/EditableTextBox.js.map +1 -1
- package/dist/components/common/FuzzySelect.d.ts +2 -1
- package/dist/components/common/FuzzySelect.d.ts.map +1 -1
- package/dist/components/common/FuzzySelect.js +6 -2
- package/dist/components/common/FuzzySelect.js.map +1 -1
- package/dist/components/common/SearchBar.js +1 -1
- package/dist/components/common/SearchBar.js.map +1 -1
- package/dist/components/common/TextEditor.js +1 -1
- package/dist/components/common/TextEditor.js.map +1 -1
- package/dist/components/common/ThemeModal.d.ts +9 -0
- package/dist/components/common/ThemeModal.d.ts.map +1 -0
- package/dist/components/common/ThemeModal.js +34 -0
- package/dist/components/common/ThemeModal.js.map +1 -0
- package/dist/components/common/WordTextInput.d.ts +17 -0
- package/dist/components/common/WordTextInput.d.ts.map +1 -0
- package/dist/components/common/WordTextInput.js +142 -0
- package/dist/components/common/WordTextInput.js.map +1 -0
- package/dist/components/confluence/PageAttachments.js +1 -1
- package/dist/components/confluence/PageAttachments.js.map +1 -1
- package/dist/components/confluence/PageBrowser.js +1 -1
- package/dist/components/confluence/PageBrowser.js.map +1 -1
- package/dist/components/confluence/PageEditor.js +1 -1
- package/dist/components/confluence/PageEditor.js.map +1 -1
- package/dist/components/confluence/PageLabels.js +1 -1
- package/dist/components/confluence/PageLabels.js.map +1 -1
- package/dist/components/jira/CommentForm.js +1 -1
- package/dist/components/jira/CommentForm.js.map +1 -1
- package/dist/components/jira/CreateTicket.d.ts +4 -1
- package/dist/components/jira/CreateTicket.d.ts.map +1 -1
- package/dist/components/jira/CreateTicket.js +28 -10
- package/dist/components/jira/CreateTicket.js.map +1 -1
- package/dist/components/jira/IssueList.d.ts.map +1 -1
- package/dist/components/jira/IssueList.js +117 -12
- package/dist/components/jira/IssueList.js.map +1 -1
- package/dist/components/jira/JiraIssueRow.d.ts.map +1 -1
- package/dist/components/jira/JiraIssueRow.js +4 -2
- package/dist/components/jira/JiraIssueRow.js.map +1 -1
- package/dist/components/jira/JiraView.d.ts +1 -1
- package/dist/components/jira/JiraView.d.ts.map +1 -1
- package/dist/components/jira/JiraView.js +115 -40
- package/dist/components/jira/JiraView.js.map +1 -1
- package/dist/components/jira/JqlSearch.d.ts.map +1 -1
- package/dist/components/jira/JqlSearch.js +50 -2
- package/dist/components/jira/JqlSearch.js.map +1 -1
- package/dist/components/jira/QuickFilters.d.ts +7 -2
- package/dist/components/jira/QuickFilters.d.ts.map +1 -1
- package/dist/components/jira/QuickFilters.js +121 -36
- package/dist/components/jira/QuickFilters.js.map +1 -1
- package/dist/components/jira/TicketDetail.d.ts +11 -3
- package/dist/components/jira/TicketDetail.d.ts.map +1 -1
- package/dist/components/jira/TicketDetail.js +594 -44
- package/dist/components/jira/TicketDetail.js.map +1 -1
- package/dist/components/jira/TicketEditor.js +1 -1
- package/dist/components/jira/TicketEditor.js.map +1 -1
- package/dist/components/jira/TicketList.d.ts.map +1 -1
- package/dist/components/jira/TicketList.js +159 -11
- package/dist/components/jira/TicketList.js.map +1 -1
- package/dist/config/config-manager.d.ts +2 -0
- package/dist/config/config-manager.d.ts.map +1 -1
- package/dist/config/config-manager.js +10 -0
- package/dist/config/config-manager.js.map +1 -1
- package/dist/config/types.d.ts +3 -0
- package/dist/config/types.d.ts.map +1 -1
- package/dist/config/types.js +1 -0
- package/dist/config/types.js.map +1 -1
- package/dist/index.js +22 -2
- package/dist/index.js.map +1 -1
- package/dist/theme/te.d.ts +12 -1
- package/dist/theme/te.d.ts.map +1 -1
- package/dist/theme/te.js +180 -12
- package/dist/theme/te.js.map +1 -1
- package/dist/utils/auto-updater.d.ts +14 -1
- package/dist/utils/auto-updater.d.ts.map +1 -1
- package/dist/utils/auto-updater.js +113 -22
- package/dist/utils/auto-updater.js.map +1 -1
- package/dist/utils/external-editor.d.ts.map +1 -1
- package/dist/utils/external-editor.js +24 -3
- package/dist/utils/external-editor.js.map +1 -1
- package/dist/utils/jira-colors.d.ts.map +1 -1
- package/dist/utils/jira-colors.js +21 -20
- package/dist/utils/jira-colors.js.map +1 -1
- package/package.json +2 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useState, useEffect } from 'react';
|
|
2
|
+
import { useState, useEffect, useRef } from 'react';
|
|
3
3
|
import { Box, Text, useInput, useStdout } from 'ink';
|
|
4
|
-
import TextInput from '
|
|
4
|
+
import TextInput from '../common/WordTextInput.js';
|
|
5
5
|
import { AdfConverter } from '../../formatters/adf-converter.js';
|
|
6
6
|
import { SelectableItem } from '../common/SelectableItem.js';
|
|
7
7
|
import { ShortcutHints } from '../common/ShortcutHints.js';
|
|
@@ -14,7 +14,215 @@ import { join } from 'path';
|
|
|
14
14
|
import { highlightMarkdownLine, detectCodeBlock, highlightCodeLine } from '../../utils/markdown-highlighter.js';
|
|
15
15
|
import { getJiraStatusColor, getJiraTypeColor, getJiraPriorityColor } from '../../utils/jira-colors.js';
|
|
16
16
|
import { te } from '../../theme/te.js';
|
|
17
|
-
|
|
17
|
+
function getRequiredTransitionFields(transition) {
|
|
18
|
+
if (!transition?.fields)
|
|
19
|
+
return [];
|
|
20
|
+
return Object.entries(transition.fields)
|
|
21
|
+
.filter(([, definition]) => {
|
|
22
|
+
if (!definition?.required)
|
|
23
|
+
return false;
|
|
24
|
+
const operations = Array.isArray(definition.operations) ? definition.operations : [];
|
|
25
|
+
const canSet = operations.length === 0 || operations.includes('set');
|
|
26
|
+
return canSet;
|
|
27
|
+
})
|
|
28
|
+
.map(([fieldId, definition]) => ({
|
|
29
|
+
id: fieldId,
|
|
30
|
+
name: definition.name || fieldId,
|
|
31
|
+
definition,
|
|
32
|
+
}));
|
|
33
|
+
}
|
|
34
|
+
function buildFieldValueFromAllowed(fieldId, rawValue) {
|
|
35
|
+
if (!rawValue || typeof rawValue !== 'object') {
|
|
36
|
+
return rawValue;
|
|
37
|
+
}
|
|
38
|
+
const schemaType = String(rawValue?.schema?.type || '').toLowerCase();
|
|
39
|
+
if (schemaType === 'user' && rawValue.accountId) {
|
|
40
|
+
return { accountId: rawValue.accountId };
|
|
41
|
+
}
|
|
42
|
+
if (rawValue.id !== undefined && rawValue.id !== null && rawValue.id !== '') {
|
|
43
|
+
return { id: String(rawValue.id) };
|
|
44
|
+
}
|
|
45
|
+
if (rawValue.value !== undefined) {
|
|
46
|
+
return { value: rawValue.value };
|
|
47
|
+
}
|
|
48
|
+
if (rawValue.name !== undefined) {
|
|
49
|
+
return { name: rawValue.name };
|
|
50
|
+
}
|
|
51
|
+
if (rawValue.key !== undefined) {
|
|
52
|
+
return { key: rawValue.key };
|
|
53
|
+
}
|
|
54
|
+
// Fallback to full object for custom transitions expecting richer payloads.
|
|
55
|
+
return rawValue;
|
|
56
|
+
}
|
|
57
|
+
function formatAllowedValueLabel(rawValue) {
|
|
58
|
+
if (!rawValue || typeof rawValue !== 'object') {
|
|
59
|
+
return String(rawValue ?? '');
|
|
60
|
+
}
|
|
61
|
+
if (typeof rawValue.displayName === 'string' && rawValue.displayName.trim()) {
|
|
62
|
+
return rawValue.displayName;
|
|
63
|
+
}
|
|
64
|
+
if (typeof rawValue.name === 'string' && rawValue.name.trim()) {
|
|
65
|
+
return rawValue.name;
|
|
66
|
+
}
|
|
67
|
+
if (typeof rawValue.value === 'string' && rawValue.value.trim()) {
|
|
68
|
+
return rawValue.value;
|
|
69
|
+
}
|
|
70
|
+
if (typeof rawValue.key === 'string' && rawValue.key.trim()) {
|
|
71
|
+
return rawValue.key;
|
|
72
|
+
}
|
|
73
|
+
if (rawValue.id !== undefined && rawValue.id !== null) {
|
|
74
|
+
return String(rawValue.id);
|
|
75
|
+
}
|
|
76
|
+
return JSON.stringify(rawValue);
|
|
77
|
+
}
|
|
78
|
+
function buildFieldValueFromText(fieldId, definition, rawInput) {
|
|
79
|
+
const input = rawInput.trim();
|
|
80
|
+
const schemaType = String(definition?.schema?.type || '').toLowerCase();
|
|
81
|
+
const schemaSystem = String(definition?.schema?.system || '').toLowerCase();
|
|
82
|
+
const schemaCustom = String(definition?.schema?.custom || '').toLowerCase();
|
|
83
|
+
const requiresAdf = schemaType === 'doc' ||
|
|
84
|
+
schemaType === 'textarea' ||
|
|
85
|
+
schemaSystem === 'description' ||
|
|
86
|
+
schemaCustom.includes(':textarea') ||
|
|
87
|
+
schemaCustom.includes('richtext');
|
|
88
|
+
const asAdfDoc = (text) => ({
|
|
89
|
+
type: 'doc',
|
|
90
|
+
version: 1,
|
|
91
|
+
content: [
|
|
92
|
+
{
|
|
93
|
+
type: 'paragraph',
|
|
94
|
+
content: [
|
|
95
|
+
{
|
|
96
|
+
type: 'text',
|
|
97
|
+
text,
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
},
|
|
101
|
+
],
|
|
102
|
+
});
|
|
103
|
+
if (schemaType === 'number') {
|
|
104
|
+
const parsed = Number(input);
|
|
105
|
+
return Number.isFinite(parsed) ? parsed : input;
|
|
106
|
+
}
|
|
107
|
+
if (schemaType === 'user') {
|
|
108
|
+
return { accountId: input };
|
|
109
|
+
}
|
|
110
|
+
if (schemaType === 'option' ||
|
|
111
|
+
schemaType === 'priority' ||
|
|
112
|
+
schemaType === 'resolution' ||
|
|
113
|
+
schemaType === 'version' ||
|
|
114
|
+
schemaType === 'component' ||
|
|
115
|
+
schemaSystem === 'resolution' ||
|
|
116
|
+
schemaSystem === 'priority') {
|
|
117
|
+
return /^\d+$/.test(input) ? { id: input } : { name: input };
|
|
118
|
+
}
|
|
119
|
+
if (schemaType === 'array') {
|
|
120
|
+
const values = input
|
|
121
|
+
.split(',')
|
|
122
|
+
.map((part) => part.trim())
|
|
123
|
+
.filter(Boolean);
|
|
124
|
+
const itemType = String(definition?.schema?.items || '').toLowerCase();
|
|
125
|
+
if (itemType === 'number') {
|
|
126
|
+
return values.map((v) => {
|
|
127
|
+
const parsed = Number(v);
|
|
128
|
+
return Number.isFinite(parsed) ? parsed : v;
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
if (itemType === 'option' || itemType === 'version' || itemType === 'component') {
|
|
132
|
+
return values.map((v) => (/^\d+$/.test(v) ? { id: v } : { name: v }));
|
|
133
|
+
}
|
|
134
|
+
if (itemType === 'user') {
|
|
135
|
+
return values.map((v) => ({ accountId: v }));
|
|
136
|
+
}
|
|
137
|
+
return values;
|
|
138
|
+
}
|
|
139
|
+
if (requiresAdf) {
|
|
140
|
+
return asAdfDoc(input);
|
|
141
|
+
}
|
|
142
|
+
// URL and string-like custom fields use raw string.
|
|
143
|
+
if (schemaType === 'string' || schemaCustom.includes('url')) {
|
|
144
|
+
return input;
|
|
145
|
+
}
|
|
146
|
+
return input;
|
|
147
|
+
}
|
|
148
|
+
function getTransitionFieldInputHint(definition) {
|
|
149
|
+
const schemaType = String(definition?.schema?.type || '').toLowerCase();
|
|
150
|
+
const schemaCustom = String(definition?.schema?.custom || '').toLowerCase();
|
|
151
|
+
if ((definition.allowedValues || []).length > 0) {
|
|
152
|
+
return 'Pick a value with ↑/↓ and press Enter.';
|
|
153
|
+
}
|
|
154
|
+
if (schemaCustom.includes('url')) {
|
|
155
|
+
return 'Enter a full URL (for example https://example.com/doc).';
|
|
156
|
+
}
|
|
157
|
+
if (schemaType === 'number') {
|
|
158
|
+
return 'Enter a numeric value.';
|
|
159
|
+
}
|
|
160
|
+
if (schemaType === 'array') {
|
|
161
|
+
return 'Enter comma-separated values.';
|
|
162
|
+
}
|
|
163
|
+
if (schemaType === 'user') {
|
|
164
|
+
return 'Type a Jira user name and select from suggestions (or enter accountId).';
|
|
165
|
+
}
|
|
166
|
+
return 'Enter a value and press Enter.';
|
|
167
|
+
}
|
|
168
|
+
function normalizeFieldToken(value) {
|
|
169
|
+
return value.trim().toLowerCase();
|
|
170
|
+
}
|
|
171
|
+
function extractRequiredFieldNamesFromError(errorMessage) {
|
|
172
|
+
const names = new Set();
|
|
173
|
+
const trimmed = errorMessage.trim();
|
|
174
|
+
const payloadStart = trimmed.indexOf('{');
|
|
175
|
+
const payload = payloadStart >= 0 ? trimmed.slice(payloadStart) : '';
|
|
176
|
+
let errorMessages = [];
|
|
177
|
+
let errorEntries = [];
|
|
178
|
+
if (payload) {
|
|
179
|
+
try {
|
|
180
|
+
const parsed = JSON.parse(payload);
|
|
181
|
+
if (Array.isArray(parsed.errorMessages)) {
|
|
182
|
+
errorMessages = parsed.errorMessages.filter((entry) => typeof entry === 'string');
|
|
183
|
+
}
|
|
184
|
+
if (parsed.errors && typeof parsed.errors === 'object') {
|
|
185
|
+
errorEntries = Object.entries(parsed.errors).filter((entry) => typeof entry[1] === 'string');
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
// Ignore parse issues and fallback to regex scanning on plain text.
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
const combinedMessages = [...errorMessages];
|
|
193
|
+
if (combinedMessages.length === 0) {
|
|
194
|
+
combinedMessages.push(trimmed);
|
|
195
|
+
}
|
|
196
|
+
for (const message of combinedMessages) {
|
|
197
|
+
const match = message.match(/field\s+(.+?)\s+is required/i);
|
|
198
|
+
if (match && match[1]) {
|
|
199
|
+
names.add(match[1].trim());
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
for (const [fieldKey, message] of errorEntries) {
|
|
203
|
+
if (/required/i.test(message)) {
|
|
204
|
+
names.add(fieldKey);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return [...names];
|
|
208
|
+
}
|
|
209
|
+
function buildQueueForNamedRequiredFields(fields, requiredNames) {
|
|
210
|
+
const wanted = new Set(requiredNames.map(normalizeFieldToken));
|
|
211
|
+
const queue = [];
|
|
212
|
+
for (const [fieldId, definition] of Object.entries(fields || {})) {
|
|
213
|
+
const nameToken = normalizeFieldToken(definition?.name || '');
|
|
214
|
+
const idToken = normalizeFieldToken(fieldId);
|
|
215
|
+
if (!wanted.has(nameToken) && !wanted.has(idToken))
|
|
216
|
+
continue;
|
|
217
|
+
queue.push({
|
|
218
|
+
id: fieldId,
|
|
219
|
+
name: definition.name || fieldId,
|
|
220
|
+
definition,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
return queue;
|
|
224
|
+
}
|
|
225
|
+
export function TicketDetail({ issue, baseUrl, currentAccountId, transitions = [], priorities = [], onSaveTitle, onSaveDescription, onSavePriority, onAssignToMe, onAddComment, onUpdateComment, onTransition, onRefreshTransitions, onSearchUsers, onResolveTransitionFields, onFetchComments, onDownloadAttachment, onUploadAttachment, onBookmarkChanged, onCreateChildTicket, onRefresh, onBack }) {
|
|
18
226
|
const { stdout } = useStdout();
|
|
19
227
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
20
228
|
const [mode, setMode] = useState('view');
|
|
@@ -22,6 +230,16 @@ export function TicketDetail({ issue, baseUrl, currentAccountId, transitions = [
|
|
|
22
230
|
const [editingCommentId, setEditingCommentId] = useState(null);
|
|
23
231
|
const [selectedCommentIndex, setSelectedCommentIndex] = useState(0);
|
|
24
232
|
const [selectedTransitionIndex, setSelectedTransitionIndex] = useState(0);
|
|
233
|
+
const [transitionFieldsQueue, setTransitionFieldsQueue] = useState([]);
|
|
234
|
+
const [transitionFieldCursor, setTransitionFieldCursor] = useState(0);
|
|
235
|
+
const [transitionFieldValues, setTransitionFieldValues] = useState({});
|
|
236
|
+
const [transitionFieldInput, setTransitionFieldInput] = useState('');
|
|
237
|
+
const [transitionAllowedIndex, setTransitionAllowedIndex] = useState(0);
|
|
238
|
+
const [transitionUserOptions, setTransitionUserOptions] = useState([]);
|
|
239
|
+
const [transitionUserIndex, setTransitionUserIndex] = useState(0);
|
|
240
|
+
const [transitionUserLoading, setTransitionUserLoading] = useState(false);
|
|
241
|
+
const [pendingTransitionId, setPendingTransitionId] = useState(null);
|
|
242
|
+
const [transitionLoading, setTransitionLoading] = useState(false);
|
|
25
243
|
const [selectedPriorityIndex, setSelectedPriorityIndex] = useState(0);
|
|
26
244
|
const [saving, setSaving] = useState(false);
|
|
27
245
|
const [error, setError] = useState(null);
|
|
@@ -31,6 +249,10 @@ export function TicketDetail({ issue, baseUrl, currentAccountId, transitions = [
|
|
|
31
249
|
const [allComments, setAllComments] = useState([]);
|
|
32
250
|
const [attachmentsMessage, setAttachmentsMessage] = useState(null);
|
|
33
251
|
const [uploadPath, setUploadPath] = useState('');
|
|
252
|
+
const [isExternalEditing, setIsExternalEditing] = useState(false);
|
|
253
|
+
const [ignoreEscapeUntil, setIgnoreEscapeUntil] = useState(0);
|
|
254
|
+
const externalEditorBusyRef = useRef(false);
|
|
255
|
+
const userSearchCacheRef = useRef(new Map());
|
|
34
256
|
const description = AdfConverter.adfToMarkdown(issue.fields.description);
|
|
35
257
|
const parentIssue = issue.fields.parent;
|
|
36
258
|
const parentKey = parentIssue?.key || '';
|
|
@@ -40,10 +262,79 @@ export function TicketDetail({ issue, baseUrl, currentAccountId, transitions = [
|
|
|
40
262
|
const assigneeIsMe = Boolean(currentAccountId && currentAssigneeAccountId === currentAccountId);
|
|
41
263
|
const comments = issue.fields.comment?.comments || [];
|
|
42
264
|
const attachments = issue.fields.attachment || [];
|
|
265
|
+
const isEpicIssue = String(issue.fields.issuetype?.name || '').trim().toLowerCase() === 'epic';
|
|
43
266
|
const terminalRows = stdout?.rows || 24;
|
|
44
267
|
const compactViewport = terminalRows <= 32;
|
|
45
268
|
const previewWidth = Math.max(20, (stdout?.columns || 80) - 10);
|
|
46
269
|
const descriptionPreviewLines = compactViewport ? 3 : 5;
|
|
270
|
+
const activeTransition = pendingTransitionId
|
|
271
|
+
? transitions.find((transition) => transition.id === pendingTransitionId)
|
|
272
|
+
: null;
|
|
273
|
+
const currentTransitionField = transitionFieldsQueue[transitionFieldCursor];
|
|
274
|
+
const currentTransitionAllowedValues = Array.isArray(currentTransitionField?.definition?.allowedValues)
|
|
275
|
+
? currentTransitionField.definition.allowedValues
|
|
276
|
+
: [];
|
|
277
|
+
const isCurrentTransitionFieldSelect = currentTransitionAllowedValues.length > 0;
|
|
278
|
+
const currentTransitionSchemaType = String(currentTransitionField?.definition?.schema?.type || '').toLowerCase();
|
|
279
|
+
const currentTransitionSchemaItems = String(currentTransitionField?.definition?.schema?.items || '').toLowerCase();
|
|
280
|
+
const isCurrentTransitionUserField = !isCurrentTransitionFieldSelect && (currentTransitionSchemaType === 'user' ||
|
|
281
|
+
(currentTransitionSchemaType === 'array' && currentTransitionSchemaItems === 'user'));
|
|
282
|
+
useEffect(() => {
|
|
283
|
+
if (selectedTransitionIndex < transitions.length)
|
|
284
|
+
return;
|
|
285
|
+
setSelectedTransitionIndex(Math.max(0, transitions.length - 1));
|
|
286
|
+
}, [selectedTransitionIndex, transitions.length]);
|
|
287
|
+
useEffect(() => {
|
|
288
|
+
if (mode !== 'transition-fields' || !isCurrentTransitionUserField || !onSearchUsers) {
|
|
289
|
+
setTransitionUserLoading(false);
|
|
290
|
+
setTransitionUserOptions([]);
|
|
291
|
+
setTransitionUserIndex(0);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
const query = transitionFieldInput.trim();
|
|
295
|
+
if (!query) {
|
|
296
|
+
setTransitionUserLoading(false);
|
|
297
|
+
setTransitionUserOptions([]);
|
|
298
|
+
setTransitionUserIndex(0);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
const cacheKey = query.toLowerCase();
|
|
302
|
+
const cached = userSearchCacheRef.current.get(cacheKey);
|
|
303
|
+
if (cached) {
|
|
304
|
+
setTransitionUserOptions(cached);
|
|
305
|
+
setTransitionUserIndex(0);
|
|
306
|
+
setTransitionUserLoading(false);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
let cancelled = false;
|
|
310
|
+
const timer = setTimeout(async () => {
|
|
311
|
+
setTransitionUserLoading(true);
|
|
312
|
+
try {
|
|
313
|
+
const users = await onSearchUsers(query);
|
|
314
|
+
if (cancelled)
|
|
315
|
+
return;
|
|
316
|
+
const normalized = (users || []).slice(0, 8);
|
|
317
|
+
userSearchCacheRef.current.set(cacheKey, normalized);
|
|
318
|
+
setTransitionUserOptions(normalized);
|
|
319
|
+
setTransitionUserIndex(0);
|
|
320
|
+
}
|
|
321
|
+
catch {
|
|
322
|
+
if (!cancelled) {
|
|
323
|
+
setTransitionUserOptions([]);
|
|
324
|
+
setTransitionUserIndex(0);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
finally {
|
|
328
|
+
if (!cancelled) {
|
|
329
|
+
setTransitionUserLoading(false);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}, 180);
|
|
333
|
+
return () => {
|
|
334
|
+
cancelled = true;
|
|
335
|
+
clearTimeout(timer);
|
|
336
|
+
};
|
|
337
|
+
}, [mode, isCurrentTransitionUserField, onSearchUsers, transitionFieldInput, currentTransitionField?.id]);
|
|
47
338
|
const renderMarkdownPreview = (markdown, maxLines) => {
|
|
48
339
|
const lines = markdown.split('\n');
|
|
49
340
|
return lines.slice(0, maxLines).map((line, i) => {
|
|
@@ -58,7 +349,11 @@ export function TicketDetail({ issue, baseUrl, currentAccountId, transitions = [
|
|
|
58
349
|
// Get user's own comments that can be edited
|
|
59
350
|
const myComments = comments.filter((c) => currentAccountId && c.author?.accountId === currentAccountId);
|
|
60
351
|
// Selectable items in view mode.
|
|
61
|
-
const selectableItems = ['status', 'assignee', 'priority', 'title'
|
|
352
|
+
const selectableItems = ['status', 'assignee', 'priority', 'title'];
|
|
353
|
+
if (isEpicIssue) {
|
|
354
|
+
selectableItems.push('add-child-ticket');
|
|
355
|
+
}
|
|
356
|
+
selectableItems.push('description', 'comments', 'attachments', 'add-comment');
|
|
62
357
|
if (myComments.length > 0) {
|
|
63
358
|
selectableItems.push('edit-comment');
|
|
64
359
|
}
|
|
@@ -87,22 +382,46 @@ export function TicketDetail({ issue, baseUrl, currentAccountId, transitions = [
|
|
|
87
382
|
.finally(() => setCommentsLoading(false));
|
|
88
383
|
}
|
|
89
384
|
}, [mode, onFetchComments]);
|
|
385
|
+
useEffect(() => {
|
|
386
|
+
setPendingTransitionId(null);
|
|
387
|
+
setTransitionFieldsQueue([]);
|
|
388
|
+
setTransitionFieldCursor(0);
|
|
389
|
+
setTransitionFieldValues({});
|
|
390
|
+
setTransitionFieldInput('');
|
|
391
|
+
setTransitionAllowedIndex(0);
|
|
392
|
+
}, [issue.key]);
|
|
90
393
|
// Handle keyboard input
|
|
91
394
|
useInput((input, key) => {
|
|
395
|
+
if (Date.now() < ignoreEscapeUntil) {
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
92
398
|
// External editor for long-form fields
|
|
93
399
|
if ((key.ctrl && input === 'e') && (mode === 'edit-description' || mode === 'add-comment' || mode === 'edit-comment')) {
|
|
94
|
-
(
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
400
|
+
if (externalEditorBusyRef.current || isExternalEditing) {
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
externalEditorBusyRef.current = true;
|
|
404
|
+
setIsExternalEditing(true);
|
|
405
|
+
setSaving(true);
|
|
406
|
+
void (async () => {
|
|
407
|
+
try {
|
|
408
|
+
const result = await openExternalEditor({
|
|
409
|
+
content: editValue,
|
|
410
|
+
extension: 'md',
|
|
411
|
+
});
|
|
412
|
+
if (result.success && result.content !== undefined) {
|
|
413
|
+
setEditValue(result.content);
|
|
414
|
+
}
|
|
415
|
+
else if (result.error) {
|
|
416
|
+
setError(result.error);
|
|
417
|
+
}
|
|
103
418
|
}
|
|
104
|
-
|
|
105
|
-
|
|
419
|
+
finally {
|
|
420
|
+
setSaving(false);
|
|
421
|
+
setIsExternalEditing(false);
|
|
422
|
+
externalEditorBusyRef.current = false;
|
|
423
|
+
// Ignore trailing ESC emitted by editor quit sequences.
|
|
424
|
+
setIgnoreEscapeUntil(Date.now() + 300);
|
|
106
425
|
}
|
|
107
426
|
})();
|
|
108
427
|
return;
|
|
@@ -124,6 +443,12 @@ export function TicketDetail({ issue, baseUrl, currentAccountId, transitions = [
|
|
|
124
443
|
}
|
|
125
444
|
// Global Escape Handler
|
|
126
445
|
if (key.escape) {
|
|
446
|
+
if (mode === 'transition-fields') {
|
|
447
|
+
setMode('select-status');
|
|
448
|
+
resetTransitionFieldFlow();
|
|
449
|
+
setError(null);
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
127
452
|
if (mode !== 'view') {
|
|
128
453
|
handleCancel();
|
|
129
454
|
}
|
|
@@ -196,6 +521,9 @@ export function TicketDetail({ issue, baseUrl, currentAccountId, transitions = [
|
|
|
196
521
|
}
|
|
197
522
|
// Status selection mode
|
|
198
523
|
if (mode === 'select-status') {
|
|
524
|
+
if (transitionLoading) {
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
199
527
|
if (key.upArrow) {
|
|
200
528
|
setSelectedTransitionIndex(prev => Math.max(0, prev - 1));
|
|
201
529
|
}
|
|
@@ -203,7 +531,29 @@ export function TicketDetail({ issue, baseUrl, currentAccountId, transitions = [
|
|
|
203
531
|
setSelectedTransitionIndex(prev => Math.min(transitions.length - 1, prev + 1));
|
|
204
532
|
}
|
|
205
533
|
else if (key.return && transitions.length > 0) {
|
|
206
|
-
|
|
534
|
+
void beginTransition();
|
|
535
|
+
}
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
if (mode === 'transition-fields') {
|
|
539
|
+
if (isCurrentTransitionFieldSelect) {
|
|
540
|
+
if (key.upArrow) {
|
|
541
|
+
setTransitionAllowedIndex((prev) => Math.max(0, prev - 1));
|
|
542
|
+
}
|
|
543
|
+
else if (key.downArrow) {
|
|
544
|
+
setTransitionAllowedIndex((prev) => Math.min(currentTransitionAllowedValues.length - 1, prev + 1));
|
|
545
|
+
}
|
|
546
|
+
else if (key.return) {
|
|
547
|
+
submitCurrentTransitionAllowedField();
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
else if (isCurrentTransitionUserField && transitionUserOptions.length > 0) {
|
|
551
|
+
if (key.upArrow) {
|
|
552
|
+
setTransitionUserIndex((prev) => Math.max(0, prev - 1));
|
|
553
|
+
}
|
|
554
|
+
else if (key.downArrow) {
|
|
555
|
+
setTransitionUserIndex((prev) => Math.min(transitionUserOptions.length - 1, prev + 1));
|
|
556
|
+
}
|
|
207
557
|
}
|
|
208
558
|
return;
|
|
209
559
|
}
|
|
@@ -237,7 +587,7 @@ export function TicketDetail({ issue, baseUrl, currentAccountId, transitions = [
|
|
|
237
587
|
else if (key.return) {
|
|
238
588
|
handleSelect();
|
|
239
589
|
}
|
|
240
|
-
});
|
|
590
|
+
}, { isActive: !isExternalEditing });
|
|
241
591
|
const handleCopyUrl = async () => {
|
|
242
592
|
try {
|
|
243
593
|
// Remove trailing slash from baseUrl to prevent double slashes
|
|
@@ -258,15 +608,209 @@ export function TicketDetail({ issue, baseUrl, currentAccountId, transitions = [
|
|
|
258
608
|
setCopyMessage('✗ Copy failed');
|
|
259
609
|
}
|
|
260
610
|
};
|
|
611
|
+
function resetTransitionFieldFlow() {
|
|
612
|
+
setPendingTransitionId(null);
|
|
613
|
+
setTransitionFieldsQueue([]);
|
|
614
|
+
setTransitionFieldCursor(0);
|
|
615
|
+
setTransitionFieldValues({});
|
|
616
|
+
setTransitionFieldInput('');
|
|
617
|
+
setTransitionAllowedIndex(0);
|
|
618
|
+
setTransitionUserLoading(false);
|
|
619
|
+
setTransitionUserOptions([]);
|
|
620
|
+
setTransitionUserIndex(0);
|
|
621
|
+
}
|
|
622
|
+
async function applyTransition(transitionId, fields) {
|
|
623
|
+
if (saving || !transitionId)
|
|
624
|
+
return;
|
|
625
|
+
setSaving(true);
|
|
626
|
+
setError(null);
|
|
627
|
+
try {
|
|
628
|
+
if (onTransition) {
|
|
629
|
+
await onTransition(transitionId, fields);
|
|
630
|
+
}
|
|
631
|
+
if (onRefresh)
|
|
632
|
+
await onRefresh();
|
|
633
|
+
if (onRefreshTransitions)
|
|
634
|
+
await onRefreshTransitions();
|
|
635
|
+
setMode('view');
|
|
636
|
+
resetTransitionFieldFlow();
|
|
637
|
+
}
|
|
638
|
+
catch (err) {
|
|
639
|
+
const errorMessage = err instanceof Error ? err.message : 'Status change failed';
|
|
640
|
+
const requiredFieldNames = extractRequiredFieldNamesFromError(errorMessage);
|
|
641
|
+
if (requiredFieldNames.length > 0 && onResolveTransitionFields) {
|
|
642
|
+
try {
|
|
643
|
+
const resolvedFields = await onResolveTransitionFields(transitionId, requiredFieldNames);
|
|
644
|
+
const queue = buildQueueForNamedRequiredFields(resolvedFields, requiredFieldNames);
|
|
645
|
+
if (queue.length > 0) {
|
|
646
|
+
const firstField = queue[0];
|
|
647
|
+
setPendingTransitionId(transitionId);
|
|
648
|
+
setTransitionFieldsQueue(queue);
|
|
649
|
+
setTransitionFieldCursor(0);
|
|
650
|
+
setTransitionFieldValues(fields || {});
|
|
651
|
+
setTransitionAllowedIndex(0);
|
|
652
|
+
if (Array.isArray(firstField?.definition?.allowedValues) &&
|
|
653
|
+
firstField.definition.allowedValues.length > 0) {
|
|
654
|
+
setTransitionFieldInput('');
|
|
655
|
+
}
|
|
656
|
+
else if (typeof firstField?.definition?.defaultValue === 'string') {
|
|
657
|
+
setTransitionFieldInput(firstField.definition.defaultValue);
|
|
658
|
+
}
|
|
659
|
+
else {
|
|
660
|
+
setTransitionFieldInput('');
|
|
661
|
+
}
|
|
662
|
+
setMode('transition-fields');
|
|
663
|
+
setError(`Fill required field${queue.length > 1 ? 's' : ''} to continue.`);
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
catch {
|
|
668
|
+
// Fall through to existing error handling.
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
if (onRefreshTransitions) {
|
|
672
|
+
try {
|
|
673
|
+
await onRefreshTransitions();
|
|
674
|
+
}
|
|
675
|
+
catch {
|
|
676
|
+
// Keep showing the original transition error if refresh fails.
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
setError(errorMessage);
|
|
680
|
+
setMode('select-status');
|
|
681
|
+
resetTransitionFieldFlow();
|
|
682
|
+
}
|
|
683
|
+
finally {
|
|
684
|
+
setSaving(false);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
async function beginTransition() {
|
|
688
|
+
if (transitionLoading)
|
|
689
|
+
return;
|
|
690
|
+
const selectedTransition = transitions[selectedTransitionIndex];
|
|
691
|
+
if (!selectedTransition)
|
|
692
|
+
return;
|
|
693
|
+
let transitionWithFields = selectedTransition;
|
|
694
|
+
if (onResolveTransitionFields) {
|
|
695
|
+
try {
|
|
696
|
+
const resolvedFields = await onResolveTransitionFields(selectedTransition.id);
|
|
697
|
+
if (Object.keys(resolvedFields).length > 0) {
|
|
698
|
+
transitionWithFields = {
|
|
699
|
+
...selectedTransition,
|
|
700
|
+
fields: resolvedFields,
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
catch {
|
|
705
|
+
// Ignore metadata fetch issues and fallback to current transition payload.
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
const requiredFields = getRequiredTransitionFields(transitionWithFields);
|
|
709
|
+
if (requiredFields.length === 0) {
|
|
710
|
+
await applyTransition(selectedTransition.id);
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
setPendingTransitionId(transitionWithFields.id);
|
|
714
|
+
setTransitionFieldsQueue(requiredFields);
|
|
715
|
+
setTransitionFieldCursor(0);
|
|
716
|
+
setTransitionFieldValues({});
|
|
717
|
+
setTransitionAllowedIndex(0);
|
|
718
|
+
if (Array.isArray(requiredFields[0]?.definition?.allowedValues) &&
|
|
719
|
+
requiredFields[0].definition.allowedValues.length > 0) {
|
|
720
|
+
setTransitionFieldInput('');
|
|
721
|
+
}
|
|
722
|
+
else if (typeof requiredFields[0]?.definition?.defaultValue === 'string') {
|
|
723
|
+
setTransitionFieldInput(requiredFields[0].definition.defaultValue);
|
|
724
|
+
}
|
|
725
|
+
else {
|
|
726
|
+
setTransitionFieldInput('');
|
|
727
|
+
}
|
|
728
|
+
setMode('transition-fields');
|
|
729
|
+
}
|
|
730
|
+
async function openStatusSelector() {
|
|
731
|
+
setError(null);
|
|
732
|
+
resetTransitionFieldFlow();
|
|
733
|
+
if (onRefreshTransitions) {
|
|
734
|
+
setTransitionLoading(true);
|
|
735
|
+
try {
|
|
736
|
+
await onRefreshTransitions();
|
|
737
|
+
}
|
|
738
|
+
catch (err) {
|
|
739
|
+
setError(err instanceof Error ? err.message : 'Failed to refresh available transitions');
|
|
740
|
+
}
|
|
741
|
+
finally {
|
|
742
|
+
setTransitionLoading(false);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
setSelectedTransitionIndex(0);
|
|
746
|
+
setMode('select-status');
|
|
747
|
+
}
|
|
748
|
+
function commitCurrentTransitionField(value) {
|
|
749
|
+
const activeField = currentTransitionField;
|
|
750
|
+
if (!activeField || !pendingTransitionId)
|
|
751
|
+
return;
|
|
752
|
+
const nextValues = {
|
|
753
|
+
...transitionFieldValues,
|
|
754
|
+
[activeField.id]: value,
|
|
755
|
+
};
|
|
756
|
+
setTransitionFieldValues(nextValues);
|
|
757
|
+
setError(null);
|
|
758
|
+
if (transitionFieldCursor >= transitionFieldsQueue.length - 1) {
|
|
759
|
+
void applyTransition(pendingTransitionId, nextValues);
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
const nextCursor = transitionFieldCursor + 1;
|
|
763
|
+
const nextField = transitionFieldsQueue[nextCursor];
|
|
764
|
+
setTransitionFieldCursor(nextCursor);
|
|
765
|
+
setTransitionAllowedIndex(0);
|
|
766
|
+
if (Array.isArray(nextField?.definition?.allowedValues) && nextField.definition.allowedValues.length > 0) {
|
|
767
|
+
setTransitionFieldInput('');
|
|
768
|
+
}
|
|
769
|
+
else if (typeof nextField?.definition?.defaultValue === 'string') {
|
|
770
|
+
setTransitionFieldInput(nextField.definition.defaultValue);
|
|
771
|
+
}
|
|
772
|
+
else {
|
|
773
|
+
setTransitionFieldInput('');
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
function submitCurrentTransitionTextField() {
|
|
777
|
+
if (!currentTransitionField)
|
|
778
|
+
return;
|
|
779
|
+
const rawInput = transitionFieldInput.trim();
|
|
780
|
+
if (!rawInput) {
|
|
781
|
+
setError(`${currentTransitionField.name} is required.`);
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
const parsed = buildFieldValueFromText(currentTransitionField.id, currentTransitionField.definition, rawInput);
|
|
785
|
+
commitCurrentTransitionField(parsed);
|
|
786
|
+
}
|
|
787
|
+
function submitCurrentTransitionInput() {
|
|
788
|
+
if (isCurrentTransitionUserField && transitionUserOptions.length > 0) {
|
|
789
|
+
const selected = transitionUserOptions[transitionUserIndex] || transitionUserOptions[0];
|
|
790
|
+
if (selected?.accountId) {
|
|
791
|
+
commitCurrentTransitionField({ accountId: selected.accountId });
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
submitCurrentTransitionTextField();
|
|
796
|
+
}
|
|
797
|
+
function submitCurrentTransitionAllowedField() {
|
|
798
|
+
if (!currentTransitionField || currentTransitionAllowedValues.length === 0)
|
|
799
|
+
return;
|
|
800
|
+
const selected = currentTransitionAllowedValues[transitionAllowedIndex];
|
|
801
|
+
if (!selected) {
|
|
802
|
+
setError(`${currentTransitionField.name} is required.`);
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
const parsed = buildFieldValueFromAllowed(currentTransitionField.id, selected);
|
|
806
|
+
commitCurrentTransitionField(parsed);
|
|
807
|
+
}
|
|
261
808
|
const handleSelect = () => {
|
|
262
809
|
setError(null);
|
|
263
810
|
const item = selectableItems[selectedIndex];
|
|
264
811
|
switch (item) {
|
|
265
812
|
case 'status':
|
|
266
|
-
|
|
267
|
-
setSelectedTransitionIndex(0);
|
|
268
|
-
setMode('select-status');
|
|
269
|
-
}
|
|
813
|
+
void openStatusSelector();
|
|
270
814
|
break;
|
|
271
815
|
case 'priority':
|
|
272
816
|
if (priorities.length > 0) {
|
|
@@ -286,6 +830,9 @@ export function TicketDetail({ issue, baseUrl, currentAccountId, transitions = [
|
|
|
286
830
|
setEditValue(issue.fields.summary);
|
|
287
831
|
setMode('edit-title');
|
|
288
832
|
break;
|
|
833
|
+
case 'add-child-ticket':
|
|
834
|
+
onCreateChildTicket?.();
|
|
835
|
+
break;
|
|
289
836
|
case 'description':
|
|
290
837
|
setEditValue(description.slice(0, 500));
|
|
291
838
|
setMode('edit-description');
|
|
@@ -310,26 +857,6 @@ export function TicketDetail({ issue, baseUrl, currentAccountId, transitions = [
|
|
|
310
857
|
break;
|
|
311
858
|
}
|
|
312
859
|
};
|
|
313
|
-
const handleTransition = async () => {
|
|
314
|
-
if (saving || transitions.length === 0)
|
|
315
|
-
return;
|
|
316
|
-
setSaving(true);
|
|
317
|
-
setError(null);
|
|
318
|
-
try {
|
|
319
|
-
if (onTransition) {
|
|
320
|
-
await onTransition(transitions[selectedTransitionIndex].id);
|
|
321
|
-
}
|
|
322
|
-
if (onRefresh)
|
|
323
|
-
await onRefresh();
|
|
324
|
-
setMode('view');
|
|
325
|
-
}
|
|
326
|
-
catch (err) {
|
|
327
|
-
setError(err instanceof Error ? err.message : 'Status change failed');
|
|
328
|
-
}
|
|
329
|
-
finally {
|
|
330
|
-
setSaving(false);
|
|
331
|
-
}
|
|
332
|
-
};
|
|
333
860
|
const handlePriorityUpdate = async () => {
|
|
334
861
|
if (saving || priorities.length === 0 || !onSavePriority)
|
|
335
862
|
return;
|
|
@@ -416,10 +943,12 @@ export function TicketDetail({ issue, baseUrl, currentAccountId, transitions = [
|
|
|
416
943
|
};
|
|
417
944
|
const handleCancel = () => {
|
|
418
945
|
setMode('view');
|
|
946
|
+
setTransitionLoading(false);
|
|
419
947
|
setEditValue('');
|
|
420
948
|
setEditingCommentId(null);
|
|
421
949
|
setError(null);
|
|
422
950
|
setSelectedIndex(0);
|
|
951
|
+
resetTransitionFieldFlow();
|
|
423
952
|
};
|
|
424
953
|
const handleDownloadAttachment = async (attachment) => {
|
|
425
954
|
if (!onDownloadAttachment)
|
|
@@ -438,12 +967,33 @@ export function TicketDetail({ issue, baseUrl, currentAccountId, transitions = [
|
|
|
438
967
|
};
|
|
439
968
|
// Render status selection mode
|
|
440
969
|
if (mode === 'select-status') {
|
|
441
|
-
return (_jsxs(Box, { flexDirection: "column", width: "100%", children: [error && _jsxs(Text, { color: "red", children: ["Error: ", error] }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: te.accent, paddingX: 1, marginY: 1, children: [_jsxs(Text, { bold: true, color: te.accentAlt, children: ["\uD83D\uDCCB Change Status (Current: ", issue.fields.status.name, ")"] }),
|
|
970
|
+
return (_jsxs(Box, { flexDirection: "column", width: "100%", children: [error && _jsxs(Text, { color: "red", children: ["Error: ", error] }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: te.accent, paddingX: 1, marginY: 1, children: [_jsxs(Text, { bold: true, color: te.accentAlt, children: ["\uD83D\uDCCB Change Status (Current: ", issue.fields.status.name, ")"] }), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [transitionLoading && _jsx(Text, { color: te.muted, children: "Refreshing available transitions..." }), !transitionLoading && transitions.length === 0 && (_jsx(Text, { color: te.warning, children: "No status transitions are available from the current state." })), !transitionLoading && transitions.map((t, i) => {
|
|
971
|
+
const requiredCount = getRequiredTransitionFields(t).length;
|
|
972
|
+
return (_jsxs(Text, { color: i === selectedTransitionIndex ? te.accentAlt : te.fg, bold: i === selectedTransitionIndex, children: [i === selectedTransitionIndex ? '▶ ' : ' ', t.name, " \u2192 ", t.to.name, requiredCount > 0 ? ` (${requiredCount} required field${requiredCount > 1 ? 's' : ''})` : ''] }, t.id));
|
|
973
|
+
})] })] }), saving && _jsx(Text, { dimColor: true, children: "Updating status..." }), _jsx(Box, { marginTop: 1, children: _jsx(ShortcutHints, { hints: [
|
|
442
974
|
{ key: '↑/↓', label: 'Navigate' },
|
|
443
975
|
{ key: 'Enter', label: 'Apply' },
|
|
444
976
|
{ key: 'Escape', label: 'Cancel' },
|
|
445
977
|
] }) })] }));
|
|
446
978
|
}
|
|
979
|
+
if (mode === 'transition-fields') {
|
|
980
|
+
if (!activeTransition || !currentTransitionField) {
|
|
981
|
+
return (_jsxs(Box, { flexDirection: "column", width: "100%", children: [error && _jsxs(Text, { color: "red", children: ["Error: ", error] }), _jsx(Text, { color: te.warning, children: "Missing transition field metadata. Press Escape and retry." })] }));
|
|
982
|
+
}
|
|
983
|
+
return (_jsxs(Box, { flexDirection: "column", width: "100%", children: [error && _jsxs(Text, { color: "red", children: ["Error: ", error] }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: te.accent, paddingX: 1, marginY: 1, children: [_jsxs(Text, { bold: true, color: te.accentAlt, children: ["\uD83D\uDCCB Required Field (", transitionFieldCursor + 1, "/", transitionFieldsQueue.length, ")"] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: "white", children: currentTransitionField.name }), _jsx(Text, { dimColor: true, children: getTransitionFieldInputHint(currentTransitionField.definition) })] }), isCurrentTransitionFieldSelect ? (_jsx(Box, { marginTop: 1, flexDirection: "column", children: currentTransitionAllowedValues.map((option, idx) => (_jsxs(Text, { color: idx === transitionAllowedIndex ? te.accentAlt : te.fg, bold: idx === transitionAllowedIndex, children: [idx === transitionAllowedIndex ? '▶ ' : ' ', formatAllowedValueLabel(option)] }, `${currentTransitionField.id}-${idx}`))) })) : (_jsx(Box, { marginTop: 1, borderStyle: "round", borderColor: te.accent, paddingX: 1, children: _jsx(TextInput, { value: transitionFieldInput, onChange: setTransitionFieldInput, onSubmit: submitCurrentTransitionInput, placeholder: `Enter ${currentTransitionField.name}...`, focus: !saving && !isExternalEditing }) })), !isCurrentTransitionFieldSelect && isCurrentTransitionUserField && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [transitionUserLoading && _jsx(Text, { color: te.muted, children: "Searching users..." }), !transitionUserLoading && transitionUserOptions.length > 0 && transitionUserOptions.map((user, idx) => (_jsxs(Text, { color: idx === transitionUserIndex ? te.accentAlt : te.fg, bold: idx === transitionUserIndex, children: [idx === transitionUserIndex ? '▶ ' : ' ', user.displayName, user.emailAddress ? ` (${user.emailAddress})` : ''] }, `${currentTransitionField.id}:${user.accountId}`))), !transitionUserLoading && transitionFieldInput.trim().length > 0 && transitionUserOptions.length === 0 && (_jsx(Text, { color: te.muted, children: "No matching users found. Press Enter to use the typed value." }))] }))] }), saving && _jsx(Text, { dimColor: true, children: "Updating status..." }), _jsx(Box, { marginTop: 1, children: _jsx(ShortcutHints, { hints: isCurrentTransitionFieldSelect
|
|
984
|
+
? [
|
|
985
|
+
{ key: '↑/↓', label: 'Choose' },
|
|
986
|
+
{ key: 'Enter', label: 'Next' },
|
|
987
|
+
{ key: 'Escape', label: 'Cancel' },
|
|
988
|
+
]
|
|
989
|
+
: [
|
|
990
|
+
...(isCurrentTransitionUserField
|
|
991
|
+
? [{ key: '↑/↓', label: 'User' }]
|
|
992
|
+
: []),
|
|
993
|
+
{ key: 'Enter', label: 'Next' },
|
|
994
|
+
{ key: 'Escape', label: 'Cancel' },
|
|
995
|
+
] }) })] }));
|
|
996
|
+
}
|
|
447
997
|
// Render comment selection mode
|
|
448
998
|
if (mode === 'select-comment') {
|
|
449
999
|
return (_jsxs(Box, { flexDirection: "column", width: "100%", children: [error && _jsxs(Text, { color: "red", children: ["Error: ", error] }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: te.accent, paddingX: 1, marginY: 1, children: [_jsx(Text, { bold: true, color: te.accentAlt, children: "Select Comment to Edit:" }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: myComments.map((c, i) => (_jsx(Box, { flexDirection: "column", marginBottom: 1, children: _jsxs(Text, { color: i === selectedCommentIndex ? te.accentAlt : te.fg, children: [i === selectedCommentIndex ? '▶ ' : ' ', c.created.split('T')[0], " - ", AdfConverter.adfToMarkdown(c.body).slice(0, 50), "..."] }) }, c.id || i))) })] }), _jsx(Box, { marginTop: 1, children: _jsx(ShortcutHints, { hints: [
|
|
@@ -508,14 +1058,14 @@ export function TicketDetail({ issue, baseUrl, currentAccountId, transitions = [
|
|
|
508
1058
|
'add-comment': 'Add Comment',
|
|
509
1059
|
'edit-comment': 'Edit Comment',
|
|
510
1060
|
};
|
|
511
|
-
return (_jsxs(Box, { flexDirection: "column", width: "100%", children: [error && _jsxs(Text, { color: "red", children: ["Error: ", error] }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: te.accent, paddingX: 1, marginY: 1, children: [_jsxs(Text, { bold: true, color: te.accentAlt, children: ["\u270E ", modeLabels[mode]] }), _jsx(Box, { marginTop: 1, children: _jsx(TextInput, { value: editValue, onChange: setEditValue, onSubmit: handleSave }) })] }), saving && _jsx(Text, { dimColor: true, children: "Saving..." }), _jsx(Box, { marginTop: 1, children: _jsx(ShortcutHints, { hints: [
|
|
1061
|
+
return (_jsxs(Box, { flexDirection: "column", width: "100%", children: [error && _jsxs(Text, { color: "red", children: ["Error: ", error] }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: te.accent, paddingX: 1, marginY: 1, children: [_jsxs(Text, { bold: true, color: te.accentAlt, children: ["\u270E ", modeLabels[mode]] }), _jsx(Box, { marginTop: 1, children: _jsx(TextInput, { value: editValue, onChange: setEditValue, onSubmit: handleSave, focus: !saving && !isExternalEditing }) })] }), saving && _jsx(Text, { dimColor: true, children: "Saving..." }), _jsx(Box, { marginTop: 1, children: _jsx(ShortcutHints, { hints: [
|
|
512
1062
|
{ key: 'Enter', label: 'Save' },
|
|
513
1063
|
{ key: 'Ctrl+E', label: '$EDITOR' },
|
|
514
1064
|
{ key: 'Escape', label: 'Cancel' },
|
|
515
1065
|
] }) })] }));
|
|
516
1066
|
}
|
|
517
1067
|
// Render view mode with selectable items
|
|
518
|
-
return (_jsxs(Box, { flexDirection: "column", width: "100%", children: [error && _jsxs(Text, { color: "red", children: ["Error: ", error] }), copyMessage && _jsx(Text, { color: "green", children: copyMessage }), _jsxs(Box, { children: [
|
|
1068
|
+
return (_jsxs(Box, { flexDirection: "column", width: "100%", children: [error && _jsxs(Text, { color: "red", children: ["Error: ", error] }), copyMessage && _jsx(Text, { color: "green", children: copyMessage }), _jsxs(Box, { children: [_jsxs(Text, { bold: true, backgroundColor: te.accent, color: "black", children: [' ', issue.key, ' '] }), _jsxs(Text, { color: getJiraStatusColor(issue.fields.status?.name), children: [" ", issue.fields.status?.name] }), issue.fields.issuetype?.name && (_jsxs(Text, { color: getJiraTypeColor(issue.fields.issuetype.name), children: [" \u00B7 ", issue.fields.issuetype.name] })), issue.fields.priority?.name && (_jsxs(Text, { color: getJiraPriorityColor(issue.fields.priority.name), children: [" \u00B7 ", issue.fields.priority.name] }))] }), parentKey && (_jsx(SelectableItem, { label: "PARENT TICKET", content: _jsxs(Box, { children: [_jsx(Text, { color: te.accentAlt, children: parentKey }), parentSummary ? _jsxs(Text, { color: te.fg, children: [" ", ' - ', parentSummary] }) : null] }), isSelected: false, actionLabel: "[LINKED]", compact: true })), _jsx(SelectableItem, { label: "STATUS", content: _jsx(Text, { color: getJiraStatusColor(issue.fields.status?.name), children: issue.fields.status.name }), isSelected: isSelectedItem('status'), actionLabel: transitions.length > 0 ? "[CHANGE]" : "[VIEW]", compact: true }), _jsx(SelectableItem, { label: "ASSIGNEE", content: _jsxs(Text, { color: assigneeIsMe ? te.success : te.fg, children: [currentAssignee, assigneeIsMe ? ' (you)' : ''] }), isSelected: isSelectedItem('assignee'), actionLabel: assigneeIsMe ? "[YOU]" : "[ASSIGN]", compact: true }), _jsx(SelectableItem, { label: "PRIORITY", content: _jsx(Text, { color: getJiraPriorityColor(issue.fields.priority?.name), children: issue.fields.priority?.name || 'None' }), isSelected: isSelectedItem('priority'), actionLabel: priorities.length > 0 ? "[CHANGE]" : "[VIEW]", compact: true }), _jsx(SelectableItem, { label: "TITLE", content: _jsx(Text, { color: "white", children: issue.fields.summary }), isSelected: isSelectedItem('title'), actionLabel: "[EDIT]" }), isEpicIssue && (_jsx(SelectableItem, { label: "ADD CHILD TICKET", content: "Create a ticket linked to this epic", isSelected: isSelectedItem('add-child-ticket'), actionLabel: "[CREATE]", compact: true })), _jsx(SelectableItem, { label: "DESCRIPTION", content: _jsxs(Box, { flexDirection: "column", children: [renderMarkdownPreview(description, isSelectedItem('description') ? descriptionPreviewLines : 1), isSelectedItem('description') && description.split('\n').length > descriptionPreviewLines && (_jsxs(Text, { dimColor: true, children: ["... (", description.split('\n').length - descriptionPreviewLines, " more lines)"] }))] }), isSelected: isSelectedItem('description'), actionLabel: "[EDIT]" }), _jsx(SelectableItem, { label: "COMMENTS", content: `${comments.length} comment${comments.length !== 1 ? 's' : ''}`, isSelected: isSelectedItem('comments'), actionLabel: "[VIEW]", compact: true }), _jsx(SelectableItem, { label: "ATTACHMENTS", content: `${attachments.length} attachment${attachments.length !== 1 ? 's' : ''}`, isSelected: isSelectedItem('attachments'), actionLabel: "[VIEW]", compact: true }), _jsx(SelectableItem, { label: "ADD COMMENT", isSelected: isSelectedItem('add-comment'), actionLabel: "[ENTER]", compact: true }), myComments.length > 0 && (_jsx(SelectableItem, { label: "EDIT MY COMMENT", content: `(${myComments.length} comment${myComments.length > 1 ? 's' : ''})`, isSelected: isSelectedItem('edit-comment'), actionLabel: "[EDIT]", compact: true })), _jsx(Box, { marginTop: 1, children: _jsx(ShortcutHints, { hints: [
|
|
519
1069
|
{ key: '↑/↓', label: 'Navigate' },
|
|
520
1070
|
{ key: 'Enter', label: 'Select' },
|
|
521
1071
|
{ key: 'Ctrl+O', label: 'Open' },
|