@sascha384/tic 1.17.0 → 1.18.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/dist/app.d.ts +2 -34
- package/dist/app.js +8 -68
- package/dist/app.js.map +1 -1
- package/dist/components/Breadcrumbs.d.ts +5 -0
- package/dist/components/Breadcrumbs.js +16 -0
- package/dist/components/Breadcrumbs.js.map +1 -0
- package/dist/components/HelpScreen.d.ts +1 -1
- package/dist/components/HelpScreen.js +31 -3
- package/dist/components/HelpScreen.js.map +1 -1
- package/dist/components/IterationPicker.js +6 -2
- package/dist/components/IterationPicker.js.map +1 -1
- package/dist/components/Settings.js +38 -4
- package/dist/components/Settings.js.map +1 -1
- package/dist/components/StatusScreen.js +32 -4
- package/dist/components/StatusScreen.js.map +1 -1
- package/dist/components/WorkItemForm.js +230 -118
- package/dist/components/WorkItemForm.js.map +1 -1
- package/dist/components/WorkItemList.js +53 -50
- package/dist/components/WorkItemList.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/stores/backendDataStore.d.ts +2 -0
- package/dist/stores/backendDataStore.js +5 -1
- package/dist/stores/backendDataStore.js.map +1 -1
- package/dist/stores/formStackStore.d.ts +35 -0
- package/dist/stores/formStackStore.js +65 -0
- package/dist/stores/formStackStore.js.map +1 -0
- package/dist/stores/listViewStore.d.ts +17 -0
- package/dist/stores/listViewStore.js +54 -0
- package/dist/stores/listViewStore.js.map +1 -0
- package/dist/stores/navigationStore.d.ts +29 -0
- package/dist/stores/navigationStore.js +85 -0
- package/dist/stores/navigationStore.js.map +1 -0
- package/package.json +2 -1
|
@@ -5,17 +5,31 @@ import TextInput from 'ink-text-input';
|
|
|
5
5
|
import SelectInput from 'ink-select-input';
|
|
6
6
|
import { AutocompleteInput } from './AutocompleteInput.js';
|
|
7
7
|
import { MultiAutocompleteInput } from './MultiAutocompleteInput.js';
|
|
8
|
-
import {
|
|
8
|
+
import { useNavigationStore } from '../stores/navigationStore.js';
|
|
9
|
+
import { formStackStore, useFormStackStore, } from '../stores/formStackStore.js';
|
|
9
10
|
import { SyncQueueStore } from '../sync/queue.js';
|
|
10
11
|
import { useScrollViewport } from '../hooks/useScrollViewport.js';
|
|
11
12
|
import { useBackendDataStore } from '../stores/backendDataStore.js';
|
|
12
13
|
import { openInEditor } from '../editor.js';
|
|
13
14
|
import { slugifyTemplateName } from '../backends/local/templates.js';
|
|
14
|
-
import {
|
|
15
|
+
import { Breadcrumbs } from './Breadcrumbs.js';
|
|
15
16
|
const SELECT_FIELDS = ['type', 'status', 'iteration', 'priority'];
|
|
16
17
|
const PRIORITIES = ['low', 'medium', 'high', 'critical'];
|
|
17
18
|
export function WorkItemForm() {
|
|
18
|
-
const
|
|
19
|
+
const backend = useBackendDataStore((s) => s.backend);
|
|
20
|
+
const syncManager = useBackendDataStore((s) => s.syncManager);
|
|
21
|
+
const navigate = useNavigationStore((s) => s.navigate);
|
|
22
|
+
const navigateToHelp = useNavigationStore((s) => s.navigateToHelp);
|
|
23
|
+
const selectedWorkItemId = useNavigationStore((s) => s.selectedWorkItemId);
|
|
24
|
+
const activeType = useNavigationStore((s) => s.activeType);
|
|
25
|
+
const activeTemplate = useNavigationStore((s) => s.activeTemplate);
|
|
26
|
+
const setActiveTemplate = useNavigationStore((s) => s.setActiveTemplate);
|
|
27
|
+
const formMode = useNavigationStore((s) => s.formMode);
|
|
28
|
+
const setFormMode = useNavigationStore((s) => s.setFormMode);
|
|
29
|
+
const editingTemplateSlug = useNavigationStore((s) => s.editingTemplateSlug);
|
|
30
|
+
const setEditingTemplateSlug = useNavigationStore((s) => s.setEditingTemplateSlug);
|
|
31
|
+
const pushWorkItem = useNavigationStore((s) => s.pushWorkItem);
|
|
32
|
+
const popWorkItem = useNavigationStore((s) => s.popWorkItem);
|
|
19
33
|
const queueStore = useMemo(() => {
|
|
20
34
|
if (!syncManager)
|
|
21
35
|
return null;
|
|
@@ -42,6 +56,11 @@ export function WorkItemForm() {
|
|
|
42
56
|
const currentIteration = useBackendDataStore((s) => s.currentIteration);
|
|
43
57
|
const allItems = useBackendDataStore((s) => s.items);
|
|
44
58
|
const configLoading = useBackendDataStore((s) => s.loading);
|
|
59
|
+
// Form stack store for draft persistence
|
|
60
|
+
const currentDraft = useFormStackStore((s) => s.currentDraft());
|
|
61
|
+
const showDirtyPrompt = useFormStackStore((s) => s.showDiscardPrompt);
|
|
62
|
+
const storeIsDirty = useFormStackStore((s) => s.isDirty());
|
|
63
|
+
const { push: pushDraft, updateFields, setFocusedField: setStoreFocusedField, setShowDiscardPrompt: setStoreShowDiscardPrompt, } = formStackStore.getState();
|
|
45
64
|
const [existingItem, setExistingItem] = useState(null);
|
|
46
65
|
const [children, setChildren] = useState([]);
|
|
47
66
|
const [dependents, setDependents] = useState([]);
|
|
@@ -56,6 +75,10 @@ export function WorkItemForm() {
|
|
|
56
75
|
setItemLoading(false);
|
|
57
76
|
return;
|
|
58
77
|
}
|
|
78
|
+
if (!backend) {
|
|
79
|
+
setItemLoading(false);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
59
82
|
let cancelled = false;
|
|
60
83
|
setItemLoading(true);
|
|
61
84
|
void (async () => {
|
|
@@ -155,62 +178,91 @@ export function WorkItemForm() {
|
|
|
155
178
|
children,
|
|
156
179
|
dependents,
|
|
157
180
|
]);
|
|
158
|
-
const [title, setTitle] = useState('');
|
|
159
|
-
const [type, setType] = useState(activeType ?? types[0] ?? '');
|
|
160
|
-
const [status, setStatus] = useState(statuses[0] ?? '');
|
|
161
|
-
const [iteration, setIteration] = useState(currentIteration);
|
|
162
|
-
const [priority, setPriority] = useState('medium');
|
|
163
|
-
const [assignee, setAssignee] = useState('');
|
|
164
|
-
const [labels, setLabels] = useState('');
|
|
165
|
-
const [description, setDescription] = useState('');
|
|
166
|
-
const [parentId, setParentId] = useState('');
|
|
167
|
-
const [dependsOn, setDependsOn] = useState('');
|
|
168
|
-
const [newComment, setNewComment] = useState('');
|
|
169
181
|
const [comments, setComments] = useState([]);
|
|
170
|
-
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
const isDirty =
|
|
185
|
-
|
|
182
|
+
// Derive field values from current draft
|
|
183
|
+
const title = currentDraft?.fields.title ?? '';
|
|
184
|
+
const type = currentDraft?.fields.type ?? activeType ?? types[0] ?? '';
|
|
185
|
+
const status = currentDraft?.fields.status ?? statuses[0] ?? '';
|
|
186
|
+
const iteration = currentDraft?.fields.iteration ?? currentIteration;
|
|
187
|
+
const priority = (currentDraft?.fields.priority ?? 'medium');
|
|
188
|
+
const assignee = currentDraft?.fields.assignee ?? '';
|
|
189
|
+
const labels = currentDraft?.fields.labels ?? '';
|
|
190
|
+
const description = currentDraft?.fields.description ?? '';
|
|
191
|
+
const parentId = currentDraft?.fields.parentId ?? '';
|
|
192
|
+
const dependsOn = currentDraft?.fields.dependsOn ?? '';
|
|
193
|
+
const newComment = currentDraft?.fields.newComment ?? '';
|
|
194
|
+
const focusedField = currentDraft?.focusedField ?? 0;
|
|
195
|
+
// Use store's dirty detection
|
|
196
|
+
const isDirty = storeIsDirty;
|
|
197
|
+
// Field setter wrappers that update the store
|
|
198
|
+
const setTitle = (v) => updateFields({ title: v });
|
|
199
|
+
const setType = (v) => updateFields({ type: v });
|
|
200
|
+
const setStatus = (v) => updateFields({ status: v });
|
|
201
|
+
const setIteration = (v) => updateFields({ iteration: v });
|
|
202
|
+
const setPriority = (v) => updateFields({ priority: v });
|
|
203
|
+
const setAssignee = (v) => updateFields({ assignee: v });
|
|
204
|
+
const setLabels = (v) => updateFields({ labels: v });
|
|
205
|
+
const setDescription = (v) => updateFields({ description: v });
|
|
206
|
+
const setParentId = (v) => updateFields({ parentId: v });
|
|
207
|
+
const setDependsOn = (v) => updateFields({ dependsOn: v });
|
|
208
|
+
const setNewComment = (v) => updateFields({ newComment: v });
|
|
209
|
+
const setFocusedField = (v) => {
|
|
210
|
+
if (typeof v === 'function') {
|
|
211
|
+
const newVal = v(focusedField);
|
|
212
|
+
setStoreFocusedField(newVal);
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
setStoreFocusedField(v);
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
const setShowDirtyPrompt = setStoreShowDiscardPrompt;
|
|
219
|
+
// Initialize form draft when entering form (only if stack is empty)
|
|
220
|
+
useEffect(() => {
|
|
221
|
+
if (formStackStore.getState().stack.length > 0)
|
|
222
|
+
return; // Already has a draft
|
|
223
|
+
const initialFields = {
|
|
224
|
+
title: '',
|
|
225
|
+
type: activeType ?? types[0] ?? '',
|
|
226
|
+
status: statuses[0] ?? '',
|
|
227
|
+
iteration: currentIteration,
|
|
228
|
+
priority: 'medium',
|
|
229
|
+
assignee: '',
|
|
230
|
+
labels: '',
|
|
231
|
+
description: '',
|
|
232
|
+
parentId: '',
|
|
233
|
+
dependsOn: '',
|
|
234
|
+
newComment: '',
|
|
235
|
+
};
|
|
236
|
+
const draft = {
|
|
237
|
+
itemId: selectedWorkItemId,
|
|
238
|
+
itemTitle: selectedWorkItemId ? `#${selectedWorkItemId}` : '(new)',
|
|
239
|
+
fields: initialFields,
|
|
240
|
+
initialSnapshot: { ...initialFields },
|
|
241
|
+
focusedField: 0,
|
|
242
|
+
};
|
|
243
|
+
pushDraft(draft);
|
|
244
|
+
}, []); // Only on mount
|
|
186
245
|
// Sync form fields when the existing item finishes loading
|
|
187
246
|
useEffect(() => {
|
|
188
247
|
if (!existingItem)
|
|
189
248
|
return;
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
setIteration(existingItem.iteration);
|
|
194
|
-
setPriority(existingItem.priority ?? 'medium');
|
|
195
|
-
setAssignee(existingItem.assignee ?? '');
|
|
196
|
-
setLabels(existingItem.labels.join(', '));
|
|
197
|
-
setDescription(existingItem.description ?? '');
|
|
198
|
-
setParentId(existingItem.parent !== null && existingItem.parent !== undefined
|
|
249
|
+
setComments(existingItem.comments ?? []);
|
|
250
|
+
// Build field values
|
|
251
|
+
const parentIdValue = existingItem.parent !== null && existingItem.parent !== undefined
|
|
199
252
|
? (() => {
|
|
200
253
|
const pi = allItems.find((i) => i.id === existingItem.parent);
|
|
201
254
|
return pi
|
|
202
255
|
? `#${existingItem.parent} - ${pi.title}`
|
|
203
256
|
: String(existingItem.parent);
|
|
204
257
|
})()
|
|
205
|
-
: ''
|
|
206
|
-
|
|
258
|
+
: '';
|
|
259
|
+
const dependsOnValue = existingItem.dependsOn
|
|
207
260
|
?.map((depId) => {
|
|
208
261
|
const depItem = allItems.find((i) => i.id === depId);
|
|
209
262
|
return depItem ? `#${depId} - ${depItem.title}` : depId;
|
|
210
263
|
})
|
|
211
|
-
.join(', ') ?? ''
|
|
212
|
-
|
|
213
|
-
setInitialSnapshot(createSnapshot({
|
|
264
|
+
.join(', ') ?? '';
|
|
265
|
+
const newFields = {
|
|
214
266
|
title: existingItem.title,
|
|
215
267
|
type: existingItem.type,
|
|
216
268
|
status: existingItem.status,
|
|
@@ -219,23 +271,25 @@ export function WorkItemForm() {
|
|
|
219
271
|
assignee: existingItem.assignee ?? '',
|
|
220
272
|
labels: existingItem.labels.join(', '),
|
|
221
273
|
description: existingItem.description ?? '',
|
|
222
|
-
parentId:
|
|
223
|
-
|
|
224
|
-
const pi = allItems.find((i) => i.id === existingItem.parent);
|
|
225
|
-
return pi
|
|
226
|
-
? `#${existingItem.parent} - ${pi.title}`
|
|
227
|
-
: String(existingItem.parent);
|
|
228
|
-
})()
|
|
229
|
-
: '',
|
|
230
|
-
dependsOn: existingItem.dependsOn
|
|
231
|
-
?.map((depId) => {
|
|
232
|
-
const depItem = allItems.find((i) => i.id === depId);
|
|
233
|
-
return depItem ? `#${depId} - ${depItem.title}` : depId;
|
|
234
|
-
})
|
|
235
|
-
.join(', ') ?? '',
|
|
274
|
+
parentId: parentIdValue,
|
|
275
|
+
dependsOn: dependsOnValue,
|
|
236
276
|
newComment: '',
|
|
237
|
-
}
|
|
238
|
-
|
|
277
|
+
};
|
|
278
|
+
// Update both fields and initialSnapshot in the store
|
|
279
|
+
formStackStore.setState((state) => {
|
|
280
|
+
if (state.stack.length === 0)
|
|
281
|
+
return state;
|
|
282
|
+
const updated = [...state.stack];
|
|
283
|
+
const current = updated[updated.length - 1];
|
|
284
|
+
updated[updated.length - 1] = {
|
|
285
|
+
...current,
|
|
286
|
+
itemTitle: existingItem.title,
|
|
287
|
+
fields: newFields,
|
|
288
|
+
initialSnapshot: { ...newFields },
|
|
289
|
+
};
|
|
290
|
+
return { stack: updated };
|
|
291
|
+
});
|
|
292
|
+
}, [existingItem, allItems]);
|
|
239
293
|
// Prefill from template (create mode only)
|
|
240
294
|
useEffect(() => {
|
|
241
295
|
if (selectedWorkItemId !== null || !activeTemplate)
|
|
@@ -259,54 +313,15 @@ export function WorkItemForm() {
|
|
|
259
313
|
if (activeTemplate.dependsOn != null)
|
|
260
314
|
setDependsOn(activeTemplate.dependsOn.join(', '));
|
|
261
315
|
}, [activeTemplate, selectedWorkItemId]);
|
|
262
|
-
// Capture initial snapshot for new items once config finishes loading
|
|
263
|
-
useEffect(() => {
|
|
264
|
-
if (selectedWorkItemId !== null ||
|
|
265
|
-
configLoading ||
|
|
266
|
-
initialSnapshot !== null)
|
|
267
|
-
return;
|
|
268
|
-
setInitialSnapshot(createSnapshot({
|
|
269
|
-
title,
|
|
270
|
-
type,
|
|
271
|
-
status,
|
|
272
|
-
iteration,
|
|
273
|
-
priority,
|
|
274
|
-
assignee,
|
|
275
|
-
labels,
|
|
276
|
-
description,
|
|
277
|
-
parentId,
|
|
278
|
-
dependsOn,
|
|
279
|
-
newComment,
|
|
280
|
-
}));
|
|
281
|
-
}, [selectedWorkItemId, configLoading]);
|
|
282
316
|
// Load existing template for editing
|
|
283
317
|
useEffect(() => {
|
|
284
|
-
if (formMode !== 'template' || !editingTemplateSlug)
|
|
318
|
+
if (formMode !== 'template' || !editingTemplateSlug || !backend)
|
|
285
319
|
return;
|
|
286
320
|
let cancelled = false;
|
|
287
321
|
void backend.getTemplate(editingTemplateSlug).then((t) => {
|
|
288
322
|
if (cancelled)
|
|
289
323
|
return;
|
|
290
|
-
|
|
291
|
-
if (t.type != null)
|
|
292
|
-
setType(t.type);
|
|
293
|
-
if (t.status != null)
|
|
294
|
-
setStatus(t.status);
|
|
295
|
-
if (t.priority != null)
|
|
296
|
-
setPriority(t.priority);
|
|
297
|
-
if (t.assignee != null)
|
|
298
|
-
setAssignee(t.assignee);
|
|
299
|
-
if (t.labels != null)
|
|
300
|
-
setLabels(t.labels.join(', '));
|
|
301
|
-
if (t.iteration != null)
|
|
302
|
-
setIteration(t.iteration);
|
|
303
|
-
if (t.description != null)
|
|
304
|
-
setDescription(t.description);
|
|
305
|
-
if (t.parent != null)
|
|
306
|
-
setParentId(String(t.parent));
|
|
307
|
-
if (t.dependsOn != null)
|
|
308
|
-
setDependsOn(t.dependsOn.join(', '));
|
|
309
|
-
setInitialSnapshot(createSnapshot({
|
|
324
|
+
const newFields = {
|
|
310
325
|
title: t.name,
|
|
311
326
|
type: t.type ?? type,
|
|
312
327
|
status: t.status ?? status,
|
|
@@ -318,7 +333,21 @@ export function WorkItemForm() {
|
|
|
318
333
|
parentId: t.parent != null ? String(t.parent) : parentId,
|
|
319
334
|
dependsOn: t.dependsOn != null ? t.dependsOn.join(', ') : dependsOn,
|
|
320
335
|
newComment: '',
|
|
321
|
-
}
|
|
336
|
+
};
|
|
337
|
+
// Update both fields and initialSnapshot in the store
|
|
338
|
+
formStackStore.setState((state) => {
|
|
339
|
+
if (state.stack.length === 0)
|
|
340
|
+
return state;
|
|
341
|
+
const updated = [...state.stack];
|
|
342
|
+
const current = updated[updated.length - 1];
|
|
343
|
+
updated[updated.length - 1] = {
|
|
344
|
+
...current,
|
|
345
|
+
itemTitle: t.name,
|
|
346
|
+
fields: newFields,
|
|
347
|
+
initialSnapshot: { ...newFields },
|
|
348
|
+
};
|
|
349
|
+
return { stack: updated };
|
|
350
|
+
});
|
|
322
351
|
});
|
|
323
352
|
return () => {
|
|
324
353
|
cancelled = true;
|
|
@@ -329,10 +358,8 @@ export function WorkItemForm() {
|
|
|
329
358
|
.filter((item) => item.id !== selectedWorkItemId)
|
|
330
359
|
.map((item) => `#${item.id} - ${item.title}`);
|
|
331
360
|
}, [allItems, selectedWorkItemId]);
|
|
332
|
-
const [focusedField, setFocusedField] = useState(0);
|
|
333
361
|
const [editing, setEditing] = useState(false);
|
|
334
362
|
const [preEditValue, setPreEditValue] = useState('');
|
|
335
|
-
const [showDirtyPrompt, setShowDirtyPrompt] = useState(false);
|
|
336
363
|
const [pendingRelNav, setPendingRelNav] = useState(null);
|
|
337
364
|
const [saving, setSaving] = useState(false);
|
|
338
365
|
useEffect(() => {
|
|
@@ -345,6 +372,8 @@ export function WorkItemForm() {
|
|
|
345
372
|
currentField?.startsWith('rel-child-') ||
|
|
346
373
|
currentField?.startsWith('rel-dependent-');
|
|
347
374
|
async function save() {
|
|
375
|
+
if (!backend)
|
|
376
|
+
return;
|
|
348
377
|
const parsedLabels = labels
|
|
349
378
|
.split(',')
|
|
350
379
|
.map((l) => l.trim())
|
|
@@ -456,19 +485,25 @@ export function WorkItemForm() {
|
|
|
456
485
|
}
|
|
457
486
|
setActiveTemplate(null);
|
|
458
487
|
}
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
488
|
+
// Update the initialSnapshot after saving so isDirty becomes false
|
|
489
|
+
formStackStore.setState((state) => {
|
|
490
|
+
if (state.stack.length === 0)
|
|
491
|
+
return state;
|
|
492
|
+
const updated = [...state.stack];
|
|
493
|
+
const current = updated[updated.length - 1];
|
|
494
|
+
updated[updated.length - 1] = {
|
|
495
|
+
...current,
|
|
496
|
+
initialSnapshot: {
|
|
497
|
+
...current.fields,
|
|
498
|
+
newComment: '', // Comment was saved, reset
|
|
499
|
+
},
|
|
500
|
+
fields: {
|
|
501
|
+
...current.fields,
|
|
502
|
+
newComment: '', // Clear after save
|
|
503
|
+
},
|
|
504
|
+
};
|
|
505
|
+
return { stack: updated };
|
|
506
|
+
});
|
|
472
507
|
}
|
|
473
508
|
useInput((_input, key) => {
|
|
474
509
|
// Dirty prompt overlay — capture s/d/esc only
|
|
@@ -477,15 +512,40 @@ export function WorkItemForm() {
|
|
|
477
512
|
void (async () => {
|
|
478
513
|
await save();
|
|
479
514
|
if (pendingRelNav) {
|
|
515
|
+
// Push a new draft for the target item
|
|
516
|
+
const targetItem = allItems.find((i) => i.id === pendingRelNav);
|
|
517
|
+
const defaultFields = {
|
|
518
|
+
title: '',
|
|
519
|
+
type: activeType ?? types[0] ?? '',
|
|
520
|
+
status: statuses[0] ?? '',
|
|
521
|
+
iteration: currentIteration,
|
|
522
|
+
priority: 'medium',
|
|
523
|
+
assignee: '',
|
|
524
|
+
labels: '',
|
|
525
|
+
description: '',
|
|
526
|
+
parentId: '',
|
|
527
|
+
dependsOn: '',
|
|
528
|
+
newComment: '',
|
|
529
|
+
};
|
|
530
|
+
const newDraft = {
|
|
531
|
+
itemId: pendingRelNav,
|
|
532
|
+
itemTitle: targetItem?.title ?? `#${pendingRelNav}`,
|
|
533
|
+
fields: defaultFields,
|
|
534
|
+
initialSnapshot: { ...defaultFields },
|
|
535
|
+
focusedField: 0,
|
|
536
|
+
};
|
|
537
|
+
pushDraft(newDraft);
|
|
480
538
|
pushWorkItem(pendingRelNav);
|
|
481
539
|
setPendingRelNav(null);
|
|
482
540
|
}
|
|
483
541
|
else if (formMode === 'template') {
|
|
542
|
+
formStackStore.getState().pop();
|
|
484
543
|
setFormMode('item');
|
|
485
544
|
setEditingTemplateSlug(null);
|
|
486
545
|
navigate('settings');
|
|
487
546
|
}
|
|
488
547
|
else {
|
|
548
|
+
formStackStore.getState().pop();
|
|
489
549
|
const prev = popWorkItem();
|
|
490
550
|
if (prev === null)
|
|
491
551
|
navigate('list');
|
|
@@ -497,15 +557,41 @@ export function WorkItemForm() {
|
|
|
497
557
|
if (_input === 'd') {
|
|
498
558
|
// Discard: navigate back without saving
|
|
499
559
|
if (pendingRelNav) {
|
|
560
|
+
// Push a new draft for the target item (discarding current)
|
|
561
|
+
formStackStore.getState().pop();
|
|
562
|
+
const targetItem = allItems.find((i) => i.id === pendingRelNav);
|
|
563
|
+
const defaultFields = {
|
|
564
|
+
title: '',
|
|
565
|
+
type: activeType ?? types[0] ?? '',
|
|
566
|
+
status: statuses[0] ?? '',
|
|
567
|
+
iteration: currentIteration,
|
|
568
|
+
priority: 'medium',
|
|
569
|
+
assignee: '',
|
|
570
|
+
labels: '',
|
|
571
|
+
description: '',
|
|
572
|
+
parentId: '',
|
|
573
|
+
dependsOn: '',
|
|
574
|
+
newComment: '',
|
|
575
|
+
};
|
|
576
|
+
const newDraft = {
|
|
577
|
+
itemId: pendingRelNav,
|
|
578
|
+
itemTitle: targetItem?.title ?? `#${pendingRelNav}`,
|
|
579
|
+
fields: defaultFields,
|
|
580
|
+
initialSnapshot: { ...defaultFields },
|
|
581
|
+
focusedField: 0,
|
|
582
|
+
};
|
|
583
|
+
pushDraft(newDraft);
|
|
500
584
|
pushWorkItem(pendingRelNav);
|
|
501
585
|
setPendingRelNav(null);
|
|
502
586
|
}
|
|
503
587
|
else if (formMode === 'template') {
|
|
588
|
+
formStackStore.getState().pop();
|
|
504
589
|
setFormMode('item');
|
|
505
590
|
setEditingTemplateSlug(null);
|
|
506
591
|
navigate('settings');
|
|
507
592
|
}
|
|
508
593
|
else {
|
|
594
|
+
formStackStore.getState().pop();
|
|
509
595
|
const prev = popWorkItem();
|
|
510
596
|
if (prev === null)
|
|
511
597
|
navigate('list');
|
|
@@ -525,6 +611,7 @@ export function WorkItemForm() {
|
|
|
525
611
|
if (key.escape && !editing) {
|
|
526
612
|
if (configLoading || itemLoading || saving) {
|
|
527
613
|
// Allow escape even while loading (no save)
|
|
614
|
+
formStackStore.getState().pop();
|
|
528
615
|
if (formMode === 'template') {
|
|
529
616
|
setFormMode('item');
|
|
530
617
|
setEditingTemplateSlug(null);
|
|
@@ -542,6 +629,7 @@ export function WorkItemForm() {
|
|
|
542
629
|
return;
|
|
543
630
|
}
|
|
544
631
|
// Clean — just go back
|
|
632
|
+
formStackStore.getState().pop();
|
|
545
633
|
if (formMode === 'template') {
|
|
546
634
|
setFormMode('item');
|
|
547
635
|
setEditingTemplateSlug(null);
|
|
@@ -566,6 +654,7 @@ export function WorkItemForm() {
|
|
|
566
654
|
setSaving(true);
|
|
567
655
|
void (async () => {
|
|
568
656
|
await save();
|
|
657
|
+
formStackStore.getState().pop();
|
|
569
658
|
if (formMode === 'template') {
|
|
570
659
|
setFormMode('item');
|
|
571
660
|
setEditingTemplateSlug(null);
|
|
@@ -603,6 +692,29 @@ export function WorkItemForm() {
|
|
|
603
692
|
setShowDirtyPrompt(true);
|
|
604
693
|
}
|
|
605
694
|
else {
|
|
695
|
+
// Create new draft for target item before navigating
|
|
696
|
+
const targetItem = allItems.find((i) => i.id === targetId);
|
|
697
|
+
const defaultFields = {
|
|
698
|
+
title: '',
|
|
699
|
+
type: activeType ?? types[0] ?? '',
|
|
700
|
+
status: statuses[0] ?? '',
|
|
701
|
+
iteration: currentIteration,
|
|
702
|
+
priority: 'medium',
|
|
703
|
+
assignee: '',
|
|
704
|
+
labels: '',
|
|
705
|
+
description: '',
|
|
706
|
+
parentId: '',
|
|
707
|
+
dependsOn: '',
|
|
708
|
+
newComment: '',
|
|
709
|
+
};
|
|
710
|
+
const newDraft = {
|
|
711
|
+
itemId: targetId,
|
|
712
|
+
itemTitle: targetItem?.title ?? `#${targetId}`,
|
|
713
|
+
fields: defaultFields,
|
|
714
|
+
initialSnapshot: { ...defaultFields },
|
|
715
|
+
focusedField: 0,
|
|
716
|
+
};
|
|
717
|
+
pushDraft(newDraft);
|
|
606
718
|
pushWorkItem(targetId);
|
|
607
719
|
}
|
|
608
720
|
}
|
|
@@ -866,7 +978,7 @@ export function WorkItemForm() {
|
|
|
866
978
|
const isFieldVisible = (index) => index >= viewport.start && index < viewport.end;
|
|
867
979
|
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
|
|
868
980
|
? ` #${selectedWorkItemId}`
|
|
869
|
-
: ''] }) }), fields.map((field, index) => {
|
|
981
|
+
: ''] }) }), _jsx(Breadcrumbs, {}), fields.map((field, index) => {
|
|
870
982
|
if (field === 'rel-parent' ||
|
|
871
983
|
field.startsWith('rel-child-') ||
|
|
872
984
|
field.startsWith('rel-dependent-')) {
|