@sascha384/tic 1.11.0 → 1.13.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/.claude-plugin/plugin.json +1 -1
- package/README.md +1 -1
- package/dist/app.d.ts +7 -0
- package/dist/app.js +9 -0
- package/dist/app.js.map +1 -1
- package/dist/backends/ado/index.d.ts +6 -1
- package/dist/backends/ado/index.js +29 -1
- package/dist/backends/ado/index.js.map +1 -1
- package/dist/backends/github/index.d.ts +6 -1
- package/dist/backends/github/index.js +30 -1
- package/dist/backends/github/index.js.map +1 -1
- package/dist/backends/gitlab/index.d.ts +10 -1
- package/dist/backends/gitlab/index.js +175 -0
- package/dist/backends/gitlab/index.js.map +1 -1
- package/dist/backends/jira/index.d.ts +6 -1
- package/dist/backends/jira/index.js +30 -1
- package/dist/backends/jira/index.js.map +1 -1
- package/dist/backends/local/index.d.ts +6 -1
- package/dist/backends/local/index.js +37 -0
- package/dist/backends/local/index.js.map +1 -1
- package/dist/backends/local/templates.d.ts +7 -0
- package/dist/backends/local/templates.js +99 -0
- package/dist/backends/local/templates.js.map +1 -0
- package/dist/backends/types.d.ts +23 -1
- package/dist/backends/types.js.map +1 -1
- package/dist/components/HelpScreen.js +25 -5
- package/dist/components/HelpScreen.js.map +1 -1
- package/dist/components/Settings.js +97 -6
- package/dist/components/Settings.js.map +1 -1
- package/dist/components/TemplatePicker.d.ts +8 -0
- package/dist/components/TemplatePicker.js +24 -0
- package/dist/components/TemplatePicker.js.map +1 -0
- package/dist/components/WorkItemForm.js +388 -23
- package/dist/components/WorkItemForm.js.map +1 -1
- package/dist/components/WorkItemList.js +29 -3
- package/dist/components/WorkItemList.js.map +1 -1
- package/dist/components/formSnapshot.d.ts +16 -0
- package/dist/components/formSnapshot.js +17 -0
- package/dist/components/formSnapshot.js.map +1 -0
- package/dist/sync/SyncManager.js +37 -0
- package/dist/sync/SyncManager.js.map +1 -1
- package/dist/sync/types.d.ts +3 -1
- package/dist/types.d.ts +13 -0
- package/package.json +1 -1
|
@@ -10,22 +10,25 @@ import { SyncQueueStore } from '../sync/queue.js';
|
|
|
10
10
|
import { useScrollViewport } from '../hooks/useScrollViewport.js';
|
|
11
11
|
import { useBackendData } from '../hooks/useBackendData.js';
|
|
12
12
|
import { openInEditor } from '../editor.js';
|
|
13
|
+
import { slugifyTemplateName } from '../backends/local/templates.js';
|
|
14
|
+
import { createSnapshot, isSnapshotEqual, } from './formSnapshot.js';
|
|
13
15
|
const SELECT_FIELDS = ['type', 'status', 'iteration', 'priority'];
|
|
14
16
|
const PRIORITIES = ['low', 'medium', 'high', 'critical'];
|
|
15
17
|
export function WorkItemForm() {
|
|
16
|
-
const { backend, syncManager, navigate, navigateToHelp, selectedWorkItemId, activeType, pushWorkItem, popWorkItem, } = useAppState();
|
|
18
|
+
const { backend, syncManager, navigate, navigateToHelp, selectedWorkItemId, activeType, activeTemplate, setActiveTemplate, formMode, setFormMode, editingTemplateSlug, setEditingTemplateSlug, pushWorkItem, popWorkItem, } = useAppState();
|
|
17
19
|
const queueStore = useMemo(() => {
|
|
18
20
|
if (!syncManager)
|
|
19
21
|
return null;
|
|
20
22
|
return new SyncQueueStore(process.cwd());
|
|
21
23
|
}, [syncManager]);
|
|
22
|
-
const queueWrite = async (action, itemId,
|
|
24
|
+
const queueWrite = async (action, itemId, extra) => {
|
|
23
25
|
if (queueStore) {
|
|
24
26
|
await queueStore.append({
|
|
25
27
|
action,
|
|
26
28
|
itemId,
|
|
27
29
|
timestamp: new Date().toISOString(),
|
|
28
|
-
...(commentData ? { commentData } : {}),
|
|
30
|
+
...(extra?.commentData ? { commentData: extra.commentData } : {}),
|
|
31
|
+
...(extra?.templateSlug ? { templateSlug: extra.templateSlug } : {}),
|
|
29
32
|
});
|
|
30
33
|
syncManager?.pushPending().catch(() => { });
|
|
31
34
|
}
|
|
@@ -82,6 +85,29 @@ export function WorkItemForm() {
|
|
|
82
85
|
};
|
|
83
86
|
}, [selectedWorkItemId, backend, capabilities.relationships]);
|
|
84
87
|
const fields = useMemo(() => {
|
|
88
|
+
if (formMode === 'template') {
|
|
89
|
+
const tf = capabilities.templateFields;
|
|
90
|
+
const all = ['title'];
|
|
91
|
+
if (tf.type)
|
|
92
|
+
all.push('type');
|
|
93
|
+
if (tf.status)
|
|
94
|
+
all.push('status');
|
|
95
|
+
if (tf.iteration)
|
|
96
|
+
all.push('iteration');
|
|
97
|
+
if (tf.priority)
|
|
98
|
+
all.push('priority');
|
|
99
|
+
if (tf.assignee)
|
|
100
|
+
all.push('assignee');
|
|
101
|
+
if (tf.labels)
|
|
102
|
+
all.push('labels');
|
|
103
|
+
if (tf.description)
|
|
104
|
+
all.push('description');
|
|
105
|
+
if (tf.parent)
|
|
106
|
+
all.push('parent');
|
|
107
|
+
if (tf.dependsOn)
|
|
108
|
+
all.push('dependsOn');
|
|
109
|
+
return all;
|
|
110
|
+
}
|
|
85
111
|
const all = ['title'];
|
|
86
112
|
if (capabilities.customTypes)
|
|
87
113
|
all.push('type');
|
|
@@ -113,7 +139,14 @@ export function WorkItemForm() {
|
|
|
113
139
|
}
|
|
114
140
|
}
|
|
115
141
|
return all;
|
|
116
|
-
}, [
|
|
142
|
+
}, [
|
|
143
|
+
formMode,
|
|
144
|
+
capabilities,
|
|
145
|
+
selectedWorkItemId,
|
|
146
|
+
existingItem,
|
|
147
|
+
children,
|
|
148
|
+
dependents,
|
|
149
|
+
]);
|
|
117
150
|
const [title, setTitle] = useState('');
|
|
118
151
|
const [type, setType] = useState(activeType ?? types[0] ?? '');
|
|
119
152
|
const [status, setStatus] = useState(statuses[0] ?? '');
|
|
@@ -126,6 +159,22 @@ export function WorkItemForm() {
|
|
|
126
159
|
const [dependsOn, setDependsOn] = useState('');
|
|
127
160
|
const [newComment, setNewComment] = useState('');
|
|
128
161
|
const [comments, setComments] = useState([]);
|
|
162
|
+
const [initialSnapshot, setInitialSnapshot] = useState(null);
|
|
163
|
+
const currentValues = createSnapshot({
|
|
164
|
+
title,
|
|
165
|
+
type,
|
|
166
|
+
status,
|
|
167
|
+
iteration,
|
|
168
|
+
priority,
|
|
169
|
+
assignee,
|
|
170
|
+
labels,
|
|
171
|
+
description,
|
|
172
|
+
parentId,
|
|
173
|
+
dependsOn,
|
|
174
|
+
newComment,
|
|
175
|
+
});
|
|
176
|
+
const isDirty = initialSnapshot !== null &&
|
|
177
|
+
!isSnapshotEqual(initialSnapshot, currentValues);
|
|
129
178
|
// Sync form fields when the existing item finishes loading
|
|
130
179
|
useEffect(() => {
|
|
131
180
|
if (!existingItem)
|
|
@@ -153,7 +202,120 @@ export function WorkItemForm() {
|
|
|
153
202
|
})
|
|
154
203
|
.join(', ') ?? '');
|
|
155
204
|
setComments(existingItem.comments ?? []);
|
|
205
|
+
setInitialSnapshot(createSnapshot({
|
|
206
|
+
title: existingItem.title,
|
|
207
|
+
type: existingItem.type,
|
|
208
|
+
status: existingItem.status,
|
|
209
|
+
iteration: existingItem.iteration,
|
|
210
|
+
priority: existingItem.priority ?? 'medium',
|
|
211
|
+
assignee: existingItem.assignee ?? '',
|
|
212
|
+
labels: existingItem.labels.join(', '),
|
|
213
|
+
description: existingItem.description ?? '',
|
|
214
|
+
parentId: existingItem.parent !== null && existingItem.parent !== undefined
|
|
215
|
+
? (() => {
|
|
216
|
+
const pi = allItems.find((i) => i.id === existingItem.parent);
|
|
217
|
+
return pi
|
|
218
|
+
? `#${existingItem.parent} - ${pi.title}`
|
|
219
|
+
: String(existingItem.parent);
|
|
220
|
+
})()
|
|
221
|
+
: '',
|
|
222
|
+
dependsOn: existingItem.dependsOn
|
|
223
|
+
?.map((depId) => {
|
|
224
|
+
const depItem = allItems.find((i) => i.id === depId);
|
|
225
|
+
return depItem ? `#${depId} - ${depItem.title}` : depId;
|
|
226
|
+
})
|
|
227
|
+
.join(', ') ?? '',
|
|
228
|
+
newComment: '',
|
|
229
|
+
}));
|
|
156
230
|
}, [existingItem]);
|
|
231
|
+
// Prefill from template (create mode only)
|
|
232
|
+
useEffect(() => {
|
|
233
|
+
if (selectedWorkItemId !== null || !activeTemplate)
|
|
234
|
+
return;
|
|
235
|
+
if (activeTemplate.type != null)
|
|
236
|
+
setType(activeTemplate.type);
|
|
237
|
+
if (activeTemplate.status != null)
|
|
238
|
+
setStatus(activeTemplate.status);
|
|
239
|
+
if (activeTemplate.priority != null)
|
|
240
|
+
setPriority(activeTemplate.priority);
|
|
241
|
+
if (activeTemplate.assignee != null)
|
|
242
|
+
setAssignee(activeTemplate.assignee);
|
|
243
|
+
if (activeTemplate.labels != null)
|
|
244
|
+
setLabels(activeTemplate.labels.join(', '));
|
|
245
|
+
if (activeTemplate.iteration != null)
|
|
246
|
+
setIteration(activeTemplate.iteration);
|
|
247
|
+
if (activeTemplate.description != null)
|
|
248
|
+
setDescription(activeTemplate.description);
|
|
249
|
+
if (activeTemplate.parent != null)
|
|
250
|
+
setParentId(String(activeTemplate.parent));
|
|
251
|
+
if (activeTemplate.dependsOn != null)
|
|
252
|
+
setDependsOn(activeTemplate.dependsOn.join(', '));
|
|
253
|
+
}, [activeTemplate, selectedWorkItemId]);
|
|
254
|
+
// Capture initial snapshot for new items once config finishes loading
|
|
255
|
+
useEffect(() => {
|
|
256
|
+
if (selectedWorkItemId !== null ||
|
|
257
|
+
configLoading ||
|
|
258
|
+
initialSnapshot !== null)
|
|
259
|
+
return;
|
|
260
|
+
setInitialSnapshot(createSnapshot({
|
|
261
|
+
title,
|
|
262
|
+
type,
|
|
263
|
+
status,
|
|
264
|
+
iteration,
|
|
265
|
+
priority,
|
|
266
|
+
assignee,
|
|
267
|
+
labels,
|
|
268
|
+
description,
|
|
269
|
+
parentId,
|
|
270
|
+
dependsOn,
|
|
271
|
+
newComment,
|
|
272
|
+
}));
|
|
273
|
+
}, [selectedWorkItemId, configLoading]);
|
|
274
|
+
// Load existing template for editing
|
|
275
|
+
useEffect(() => {
|
|
276
|
+
if (formMode !== 'template' || !editingTemplateSlug)
|
|
277
|
+
return;
|
|
278
|
+
let cancelled = false;
|
|
279
|
+
void backend.getTemplate(editingTemplateSlug).then((t) => {
|
|
280
|
+
if (cancelled)
|
|
281
|
+
return;
|
|
282
|
+
setTitle(t.name);
|
|
283
|
+
if (t.type != null)
|
|
284
|
+
setType(t.type);
|
|
285
|
+
if (t.status != null)
|
|
286
|
+
setStatus(t.status);
|
|
287
|
+
if (t.priority != null)
|
|
288
|
+
setPriority(t.priority);
|
|
289
|
+
if (t.assignee != null)
|
|
290
|
+
setAssignee(t.assignee);
|
|
291
|
+
if (t.labels != null)
|
|
292
|
+
setLabels(t.labels.join(', '));
|
|
293
|
+
if (t.iteration != null)
|
|
294
|
+
setIteration(t.iteration);
|
|
295
|
+
if (t.description != null)
|
|
296
|
+
setDescription(t.description);
|
|
297
|
+
if (t.parent != null)
|
|
298
|
+
setParentId(String(t.parent));
|
|
299
|
+
if (t.dependsOn != null)
|
|
300
|
+
setDependsOn(t.dependsOn.join(', '));
|
|
301
|
+
setInitialSnapshot(createSnapshot({
|
|
302
|
+
title: t.name,
|
|
303
|
+
type: t.type ?? type,
|
|
304
|
+
status: t.status ?? status,
|
|
305
|
+
iteration: t.iteration ?? iteration,
|
|
306
|
+
priority: t.priority ?? priority,
|
|
307
|
+
assignee: t.assignee ?? assignee,
|
|
308
|
+
labels: t.labels != null ? t.labels.join(', ') : labels,
|
|
309
|
+
description: t.description ?? description,
|
|
310
|
+
parentId: t.parent != null ? String(t.parent) : parentId,
|
|
311
|
+
dependsOn: t.dependsOn != null ? t.dependsOn.join(', ') : dependsOn,
|
|
312
|
+
newComment: '',
|
|
313
|
+
}));
|
|
314
|
+
});
|
|
315
|
+
return () => {
|
|
316
|
+
cancelled = true;
|
|
317
|
+
};
|
|
318
|
+
}, [formMode, editingTemplateSlug, backend]);
|
|
157
319
|
const parentSuggestions = useMemo(() => {
|
|
158
320
|
return allItems
|
|
159
321
|
.filter((item) => item.id !== selectedWorkItemId)
|
|
@@ -161,6 +323,9 @@ export function WorkItemForm() {
|
|
|
161
323
|
}, [allItems, selectedWorkItemId]);
|
|
162
324
|
const [focusedField, setFocusedField] = useState(0);
|
|
163
325
|
const [editing, setEditing] = useState(false);
|
|
326
|
+
const [preEditValue, setPreEditValue] = useState('');
|
|
327
|
+
const [showDirtyPrompt, setShowDirtyPrompt] = useState(false);
|
|
328
|
+
const [pendingRelNav, setPendingRelNav] = useState(null);
|
|
164
329
|
const [saving, setSaving] = useState(false);
|
|
165
330
|
useEffect(() => {
|
|
166
331
|
setFocusedField(0);
|
|
@@ -193,6 +358,45 @@ export function WorkItemForm() {
|
|
|
193
358
|
return match ? match[1] : trimmed;
|
|
194
359
|
})
|
|
195
360
|
.filter((s) => s.length > 0);
|
|
361
|
+
if (formMode === 'template') {
|
|
362
|
+
const template = {
|
|
363
|
+
slug: editingTemplateSlug ?? slugifyTemplateName(title),
|
|
364
|
+
name: title || 'Untitled Template',
|
|
365
|
+
};
|
|
366
|
+
if (type)
|
|
367
|
+
template.type = type;
|
|
368
|
+
if (status)
|
|
369
|
+
template.status = status;
|
|
370
|
+
if (priority !== 'medium')
|
|
371
|
+
template.priority = priority;
|
|
372
|
+
if (assignee)
|
|
373
|
+
template.assignee = assignee;
|
|
374
|
+
if (parsedLabels.length > 0)
|
|
375
|
+
template.labels = parsedLabels;
|
|
376
|
+
if (iteration)
|
|
377
|
+
template.iteration = iteration;
|
|
378
|
+
if (description)
|
|
379
|
+
template.description = description;
|
|
380
|
+
if (parsedParent)
|
|
381
|
+
template.parent = parsedParent;
|
|
382
|
+
if (parsedDependsOn.length > 0)
|
|
383
|
+
template.dependsOn = parsedDependsOn;
|
|
384
|
+
if (editingTemplateSlug) {
|
|
385
|
+
await backend.updateTemplate(editingTemplateSlug, template);
|
|
386
|
+
await queueWrite('template-update', template.slug, {
|
|
387
|
+
templateSlug: template.slug,
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
else {
|
|
391
|
+
await backend.createTemplate(template);
|
|
392
|
+
await queueWrite('template-create', template.slug, {
|
|
393
|
+
templateSlug: template.slug,
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
setFormMode('item');
|
|
397
|
+
setEditingTemplateSlug(null);
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
196
400
|
if (selectedWorkItemId !== null) {
|
|
197
401
|
await backend.cachedUpdateWorkItem(selectedWorkItemId, {
|
|
198
402
|
title,
|
|
@@ -213,8 +417,7 @@ export function WorkItemForm() {
|
|
|
213
417
|
body: newComment.trim(),
|
|
214
418
|
});
|
|
215
419
|
await queueWrite('comment', selectedWorkItemId, {
|
|
216
|
-
author: 'me',
|
|
217
|
-
body: newComment.trim(),
|
|
420
|
+
commentData: { author: 'me', body: newComment.trim() },
|
|
218
421
|
});
|
|
219
422
|
setComments((prev) => [...prev, added]);
|
|
220
423
|
setNewComment('');
|
|
@@ -240,21 +443,106 @@ export function WorkItemForm() {
|
|
|
240
443
|
body: newComment.trim(),
|
|
241
444
|
});
|
|
242
445
|
await queueWrite('comment', created.id, {
|
|
243
|
-
author: 'me',
|
|
244
|
-
body: newComment.trim(),
|
|
446
|
+
commentData: { author: 'me', body: newComment.trim() },
|
|
245
447
|
});
|
|
246
448
|
}
|
|
449
|
+
setActiveTemplate(null);
|
|
247
450
|
}
|
|
451
|
+
setInitialSnapshot(createSnapshot({
|
|
452
|
+
title,
|
|
453
|
+
type,
|
|
454
|
+
status,
|
|
455
|
+
iteration,
|
|
456
|
+
priority,
|
|
457
|
+
assignee,
|
|
458
|
+
labels,
|
|
459
|
+
description,
|
|
460
|
+
parentId,
|
|
461
|
+
dependsOn,
|
|
462
|
+
newComment: '',
|
|
463
|
+
}));
|
|
248
464
|
}
|
|
249
465
|
useInput((_input, key) => {
|
|
250
|
-
//
|
|
466
|
+
// Dirty prompt overlay — capture s/d/esc only
|
|
467
|
+
if (showDirtyPrompt) {
|
|
468
|
+
if (_input === 's' && (selectedWorkItemId !== null || title.trim())) {
|
|
469
|
+
void (async () => {
|
|
470
|
+
await save();
|
|
471
|
+
if (pendingRelNav) {
|
|
472
|
+
pushWorkItem(pendingRelNav);
|
|
473
|
+
setPendingRelNav(null);
|
|
474
|
+
}
|
|
475
|
+
else if (formMode === 'template') {
|
|
476
|
+
setFormMode('item');
|
|
477
|
+
setEditingTemplateSlug(null);
|
|
478
|
+
navigate('settings');
|
|
479
|
+
}
|
|
480
|
+
else {
|
|
481
|
+
const prev = popWorkItem();
|
|
482
|
+
if (prev === null)
|
|
483
|
+
navigate('list');
|
|
484
|
+
}
|
|
485
|
+
})();
|
|
486
|
+
setShowDirtyPrompt(false);
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
if (_input === 'd') {
|
|
490
|
+
// Discard: navigate back without saving
|
|
491
|
+
if (pendingRelNav) {
|
|
492
|
+
pushWorkItem(pendingRelNav);
|
|
493
|
+
setPendingRelNav(null);
|
|
494
|
+
}
|
|
495
|
+
else if (formMode === 'template') {
|
|
496
|
+
setFormMode('item');
|
|
497
|
+
setEditingTemplateSlug(null);
|
|
498
|
+
navigate('settings');
|
|
499
|
+
}
|
|
500
|
+
else {
|
|
501
|
+
const prev = popWorkItem();
|
|
502
|
+
if (prev === null)
|
|
503
|
+
navigate('list');
|
|
504
|
+
}
|
|
505
|
+
setShowDirtyPrompt(false);
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
if (key.escape) {
|
|
509
|
+
setShowDirtyPrompt(false);
|
|
510
|
+
setPendingRelNav(null);
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
// Ignore all other keys while prompt is showing
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
// Esc in navigation mode
|
|
251
517
|
if (key.escape && !editing) {
|
|
252
|
-
if (
|
|
253
|
-
|
|
518
|
+
if (configLoading || itemLoading || saving) {
|
|
519
|
+
// Allow escape even while loading (no save)
|
|
520
|
+
if (formMode === 'template') {
|
|
521
|
+
setFormMode('item');
|
|
522
|
+
setEditingTemplateSlug(null);
|
|
523
|
+
navigate('settings');
|
|
524
|
+
}
|
|
525
|
+
else {
|
|
526
|
+
const prev = popWorkItem();
|
|
527
|
+
if (prev === null)
|
|
528
|
+
navigate('list');
|
|
529
|
+
}
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
if (isDirty) {
|
|
533
|
+
setShowDirtyPrompt(true);
|
|
534
|
+
return;
|
|
254
535
|
}
|
|
255
|
-
|
|
256
|
-
if (
|
|
257
|
-
|
|
536
|
+
// Clean — just go back
|
|
537
|
+
if (formMode === 'template') {
|
|
538
|
+
setFormMode('item');
|
|
539
|
+
setEditingTemplateSlug(null);
|
|
540
|
+
navigate('settings');
|
|
541
|
+
}
|
|
542
|
+
else {
|
|
543
|
+
const prev = popWorkItem();
|
|
544
|
+
if (prev === null)
|
|
545
|
+
navigate('list');
|
|
258
546
|
}
|
|
259
547
|
return;
|
|
260
548
|
}
|
|
@@ -265,6 +553,24 @@ export function WorkItemForm() {
|
|
|
265
553
|
navigateToHelp();
|
|
266
554
|
return;
|
|
267
555
|
}
|
|
556
|
+
// Ctrl+S: save and go back
|
|
557
|
+
if (key.ctrl && _input === 's') {
|
|
558
|
+
setSaving(true);
|
|
559
|
+
void (async () => {
|
|
560
|
+
await save();
|
|
561
|
+
if (formMode === 'template') {
|
|
562
|
+
setFormMode('item');
|
|
563
|
+
setEditingTemplateSlug(null);
|
|
564
|
+
navigate('settings');
|
|
565
|
+
}
|
|
566
|
+
else {
|
|
567
|
+
const prev = popWorkItem();
|
|
568
|
+
if (prev === null)
|
|
569
|
+
navigate('list');
|
|
570
|
+
}
|
|
571
|
+
})();
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
268
574
|
if (key.upArrow) {
|
|
269
575
|
setFocusedField((f) => Math.max(0, f - 1));
|
|
270
576
|
}
|
|
@@ -284,11 +590,13 @@ export function WorkItemForm() {
|
|
|
284
590
|
targetId = currentField.slice('rel-dependent-'.length);
|
|
285
591
|
}
|
|
286
592
|
if (targetId) {
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
593
|
+
if (isDirty) {
|
|
594
|
+
setPendingRelNav(targetId);
|
|
595
|
+
setShowDirtyPrompt(true);
|
|
596
|
+
}
|
|
597
|
+
else {
|
|
290
598
|
pushWorkItem(targetId);
|
|
291
|
-
}
|
|
599
|
+
}
|
|
292
600
|
}
|
|
293
601
|
}
|
|
294
602
|
else if (currentField === 'description') {
|
|
@@ -303,12 +611,55 @@ export function WorkItemForm() {
|
|
|
303
611
|
}
|
|
304
612
|
}
|
|
305
613
|
else {
|
|
614
|
+
// Capture current value before editing for revert on Esc
|
|
615
|
+
const fieldValue = (() => {
|
|
616
|
+
switch (currentField) {
|
|
617
|
+
case 'title':
|
|
618
|
+
return title;
|
|
619
|
+
case 'assignee':
|
|
620
|
+
return assignee;
|
|
621
|
+
case 'labels':
|
|
622
|
+
return labels;
|
|
623
|
+
case 'parent':
|
|
624
|
+
return parentId;
|
|
625
|
+
case 'dependsOn':
|
|
626
|
+
return dependsOn;
|
|
627
|
+
case 'comments':
|
|
628
|
+
return newComment;
|
|
629
|
+
default:
|
|
630
|
+
return '';
|
|
631
|
+
}
|
|
632
|
+
})();
|
|
633
|
+
setPreEditValue(fieldValue);
|
|
306
634
|
setEditing(true);
|
|
307
635
|
}
|
|
308
636
|
}
|
|
309
637
|
}
|
|
310
638
|
else {
|
|
311
639
|
if (key.escape) {
|
|
640
|
+
// Revert field to value before editing started
|
|
641
|
+
switch (currentField) {
|
|
642
|
+
case 'title':
|
|
643
|
+
setTitle(preEditValue);
|
|
644
|
+
break;
|
|
645
|
+
case 'assignee':
|
|
646
|
+
setAssignee(preEditValue);
|
|
647
|
+
break;
|
|
648
|
+
case 'labels':
|
|
649
|
+
setLabels(preEditValue);
|
|
650
|
+
break;
|
|
651
|
+
case 'parent':
|
|
652
|
+
setParentId(preEditValue);
|
|
653
|
+
break;
|
|
654
|
+
case 'dependsOn':
|
|
655
|
+
setDependsOn(preEditValue);
|
|
656
|
+
break;
|
|
657
|
+
case 'comments':
|
|
658
|
+
setNewComment(preEditValue);
|
|
659
|
+
break;
|
|
660
|
+
// Select fields (type, status, iteration, priority) already
|
|
661
|
+
// require Enter to confirm, so Esc naturally discards
|
|
662
|
+
}
|
|
312
663
|
setEditing(false);
|
|
313
664
|
}
|
|
314
665
|
}
|
|
@@ -390,7 +741,9 @@ export function WorkItemForm() {
|
|
|
390
741
|
function renderField(field, index) {
|
|
391
742
|
const focused = index === focusedField;
|
|
392
743
|
const isEditing = focused && editing;
|
|
393
|
-
const label = field
|
|
744
|
+
const label = formMode === 'template' && field === 'title'
|
|
745
|
+
? 'Name'
|
|
746
|
+
: field.charAt(0).toUpperCase() + field.slice(1);
|
|
394
747
|
const cursor = focused ? '>' : ' ';
|
|
395
748
|
if (field === 'comments') {
|
|
396
749
|
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsxs(Text, { color: focused ? 'cyan' : undefined, children: [cursor, " "] }), _jsxs(Text, { bold: focused, color: focused ? 'cyan' : undefined, children: [label, ":"] })] }), comments.map((c, ci) => (_jsx(Box, { marginLeft: 4, children: _jsxs(Text, { dimColor: true, children: ["[", c.date, "] ", c.author, ": ", c.body] }) }, ci))), _jsx(Box, { marginLeft: 4, children: isEditing ? (_jsxs(Box, { children: [_jsx(Text, { color: "green", children: "New: " }), _jsx(TextInput, { value: newComment, onChange: setNewComment, focus: true, onSubmit: () => {
|
|
@@ -494,10 +847,18 @@ export function WorkItemForm() {
|
|
|
494
847
|
if (configLoading || itemLoading) {
|
|
495
848
|
return (_jsx(Box, { children: _jsx(Text, { dimColor: true, children: "Loading..." }) }));
|
|
496
849
|
}
|
|
497
|
-
const mode =
|
|
498
|
-
|
|
850
|
+
const mode = formMode === 'template'
|
|
851
|
+
? editingTemplateSlug
|
|
852
|
+
? 'Edit Template'
|
|
853
|
+
: 'Create Template'
|
|
854
|
+
: selectedWorkItemId !== null
|
|
855
|
+
? 'Edit'
|
|
856
|
+
: 'Create';
|
|
857
|
+
const typeLabel = formMode === 'template' ? '' : type.charAt(0).toUpperCase() + type.slice(1);
|
|
499
858
|
const isFieldVisible = (index) => index >= viewport.start && index < viewport.end;
|
|
500
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { bold: true, color: "cyan", children: [mode,
|
|
859
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { bold: true, color: "cyan", children: [mode, typeLabel ? ` ${typeLabel}` : '', formMode !== 'template' && selectedWorkItemId !== null
|
|
860
|
+
? ` #${selectedWorkItemId}`
|
|
861
|
+
: ''] }) }), fields.map((field, index) => {
|
|
501
862
|
if (field === 'rel-parent' ||
|
|
502
863
|
field.startsWith('rel-child-') ||
|
|
503
864
|
field.startsWith('rel-dependent-')) {
|
|
@@ -510,6 +871,10 @@ export function WorkItemForm() {
|
|
|
510
871
|
capabilities.relationships &&
|
|
511
872
|
fields.some((f, i) => f.startsWith('rel-') && isFieldVisible(i)) && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { bold: true, dimColor: true, children: "Relationships:" }), existingItem?.parent &&
|
|
512
873
|
fields.some((f, i) => f === 'rel-parent' && isFieldVisible(i)) && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginLeft: 2, children: _jsx(Text, { dimColor: true, children: "Parent:" }) }), fields.map((field, index) => field === 'rel-parent' && isFieldVisible(index) ? (_jsx(Box, { marginLeft: 2, children: renderRelationshipField(field, index) }, field)) : null)] })), fields.some((f, i) => f.startsWith('rel-child-') && isFieldVisible(i)) && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginLeft: 2, children: _jsx(Text, { dimColor: true, children: "Children:" }) }), fields.map((field, index) => field.startsWith('rel-child-') && isFieldVisible(index) ? (_jsx(Box, { marginLeft: 2, children: renderRelationshipField(field, index) }, field)) : null)] })), fields.some((f, i) => f.startsWith('rel-dependent-') && isFieldVisible(i)) && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginLeft: 2, children: _jsx(Text, { dimColor: true, children: "Depended on by:" }) }), fields.map((field, index) => field.startsWith('rel-dependent-') &&
|
|
513
|
-
isFieldVisible(index) ? (_jsx(Box, { marginLeft: 2, children: renderRelationshipField(field, index) }, field)) : null)] }))] })), _jsx(Box, { marginTop: 1, children: _jsx(Text, {
|
|
874
|
+
isFieldVisible(index) ? (_jsx(Box, { marginLeft: 2, children: renderRelationshipField(field, index) }, field)) : null)] }))] })), _jsx(Box, { marginTop: 1, children: showDirtyPrompt ? (_jsx(Text, { children: selectedWorkItemId !== null || title.trim() ? (_jsxs(Text, { children: ["Unsaved changes:", ' ', _jsx(Text, { color: "green", bold: true, children: "(s)" }), _jsx(Text, { children: "ave " }), _jsx(Text, { color: "red", bold: true, children: "(d)" }), _jsx(Text, { children: "iscard " }), _jsx(Text, { color: "yellow", bold: true, children: "(esc)" }), _jsx(Text, { children: " stay" })] })) : (_jsxs(Text, { children: ["Discard new item?", ' ', _jsx(Text, { color: "red", bold: true, children: "(d)" }), _jsx(Text, { children: "iscard " }), _jsx(Text, { color: "yellow", bold: true, children: "(esc)" }), _jsx(Text, { children: " stay" })] })) })) : (_jsx(Text, { dimColor: true, children: editing
|
|
875
|
+
? 'enter confirm esc revert ? help'
|
|
876
|
+
: isDirty
|
|
877
|
+
? '↑↓ navigate enter edit ctrl+s save & back esc back (unsaved changes) ? help'
|
|
878
|
+
: '↑↓ navigate enter edit ctrl+s save & back esc back ? help' })) })] }));
|
|
514
879
|
}
|
|
515
880
|
//# sourceMappingURL=WorkItemForm.js.map
|