|
24 | 24 | import BasePicker from '../pickers/BasePicker.svelte'; |
25 | 25 | import UserPicker from '../pickers/UserPicker.svelte'; |
26 | 26 | import Avatar from '../components/Avatar.svelte'; |
27 | | - import SafeMarkdown from '../components/SafeMarkdown.svelte'; |
| 27 | + import Input from '../components/Input.svelte'; |
| 28 | + import ItemDetailDescription from '../features/items/ItemDetailDescription.svelte'; |
28 | 29 |
|
29 | 30 | let { itemId } = $props(); |
30 | 31 |
|
|
35 | 36 | let transitioning = $state(false); |
36 | 37 | let isWatching = $state(false); |
37 | 38 | let watchBusy = $state(false); |
| 39 | + let saving = $state(false); |
| 40 | + let editingTitle = $state(false); |
| 41 | + let editTitle = $state(''); |
| 42 | + let editingDescription = $state(false); |
| 43 | + let editDescription = $state(''); |
38 | 44 |
|
39 | 45 | // Workflow-less personal tasks use the permitted transition endpoint to toggle |
40 | 46 | // globally stable Open/Done IDs, matching desktop and PWA personal views. |
|
91 | 97 | const summary = await loadMobileItemDetailSummary(id); |
92 | 98 | if (token !== loadToken) return; |
93 | 99 | item = summary?.item ?? null; |
| 100 | + editTitle = item?.title || ''; |
| 101 | + editDescription = item?.description || ''; |
94 | 102 | transitions = summary?.transitions?.available_transitions ?? []; |
95 | 103 | isWatching = summary?.watching || false; |
96 | 104 | personalTaskCount = summary?.personal_task_count ?? 0; |
|
167 | 175 | } |
168 | 176 | } |
169 | 177 |
|
| 178 | + async function saveTitle() { |
| 179 | + const title = editTitle.trim(); |
| 180 | + if (!title || title === item.title) { |
| 181 | + editTitle = item.title || ''; |
| 182 | + editingTitle = false; |
| 183 | + return; |
| 184 | + } |
| 185 | + saving = true; |
| 186 | + try { |
| 187 | + const updated = await api.items.update(itemId, { title }); |
| 188 | + item = { ...item, ...updated, title }; |
| 189 | + editingTitle = false; |
| 190 | + } catch (err) { |
| 191 | + console.error('Failed to save title:', err); |
| 192 | + } finally { |
| 193 | + saving = false; |
| 194 | + } |
| 195 | + } |
| 196 | +
|
| 197 | + function handleTitleKeydown(event) { |
| 198 | + if (event.key === 'Enter') { |
| 199 | + event.preventDefault(); |
| 200 | + saveTitle(); |
| 201 | + } else if (event.key === 'Escape') { |
| 202 | + editTitle = item.title || ''; |
| 203 | + editingTitle = false; |
| 204 | + } |
| 205 | + } |
| 206 | +
|
| 207 | + function handleSaveField({ field, value }) { |
| 208 | + if (field !== 'description') return; |
| 209 | + editDescription = value; |
| 210 | + saveDescription(); |
| 211 | + } |
| 212 | +
|
| 213 | + async function saveDescription() { |
| 214 | + saving = true; |
| 215 | + try { |
| 216 | + const updated = await api.items.update(itemId, { description: editDescription }); |
| 217 | + item = { ...item, ...updated, description: editDescription }; |
| 218 | + editingDescription = false; |
| 219 | + } catch (err) { |
| 220 | + console.error('Failed to save description:', err); |
| 221 | + } finally { |
| 222 | + saving = false; |
| 223 | + } |
| 224 | + } |
| 225 | +
|
| 226 | + function cancelDescription() { |
| 227 | + editDescription = item.description || ''; |
| 228 | + editingDescription = false; |
| 229 | + } |
| 230 | +
|
170 | 231 | async function updateAssignee(user) { |
171 | 232 | const assigneeId = user?.id ?? null; |
172 | 233 | if (assigneeId === (item.assignee_id ?? null)) return; |
|
411 | 472 | <div class="status-line"><span class="type">{item.item_type_name}</span></div> |
412 | 473 | {/if} |
413 | 474 |
|
414 | | - <h1 class="title" data-testid="detail-title">{item.title}</h1> |
| 475 | + {#if editingTitle} |
| 476 | + <Input |
| 477 | + bind:value={editTitle} |
| 478 | + class="title-input" |
| 479 | + aria-label="Edit title" |
| 480 | + dataTestid="mobile-title-editor" |
| 481 | + onblur={saveTitle} |
| 482 | + onkeydown={handleTitleKeydown} |
| 483 | + disabled={saving} |
| 484 | + /> |
| 485 | + {:else} |
| 486 | + <h1 class="title" data-testid="detail-title"> |
| 487 | + <button |
| 488 | + class="title-button" |
| 489 | + type="button" |
| 490 | + onclick={() => { editTitle = item.title || ''; editingTitle = true; }} |
| 491 | + aria-label="Edit title" |
| 492 | + >{item.title}</button> |
| 493 | + </h1> |
| 494 | + {/if} |
415 | 495 |
|
416 | 496 | <!-- Status + assignee pickers. Status options come from the workflow's |
417 | 497 | available-transitions endpoint (not hardcoded), so custom workflows |
|
492 | 572 | </UserPicker> |
493 | 573 | </div> |
494 | 574 |
|
495 | | - {#if item.description} |
496 | | - <div class="html-content desc" data-testid="detail-description"> |
497 | | - <SafeMarkdown html={item.description_html} source={item.description} /> |
498 | | - </div> |
499 | | - {/if} |
| 575 | + <div class="desc" data-testid="detail-description"> |
| 576 | + <ItemDetailDescription |
| 577 | + {item} |
| 578 | + bind:editingDescription |
| 579 | + bind:editDescription |
| 580 | + {saving} |
| 581 | + availableSubIssueTypes={[]} |
| 582 | + showLinkButton={false} |
| 583 | + showDiagramButton={false} |
| 584 | + showAIActions={false} |
| 585 | + onsavefield={handleSaveField} |
| 586 | + oncanceledit={cancelDescription} |
| 587 | + onstartEditingDescription={() => { |
| 588 | + editDescription = item.description || ''; |
| 589 | + editingDescription = true; |
| 590 | + }} |
| 591 | + /> |
| 592 | + </div> |
500 | 593 |
|
501 | 594 | <!-- Meta --> |
502 | 595 | {#if item.due_date || personalTaskCount > 0} |
|
676 | 769 | .type { font-size: 0.75rem; color: var(--ds-text-subtle); text-transform: uppercase; letter-spacing: 0.02em; } |
677 | 770 |
|
678 | 771 | .title { font-size: 1.25rem; font-weight: var(--font-semibold, 600); color: var(--ds-text); margin: 0 0 1rem; line-height: 1.3; } |
| 772 | + .title-button { display: block; width: 100%; padding: 0; border: none; background: transparent; color: inherit; font: inherit; text-align: left; cursor: text; } |
| 773 | + .title-button:active { opacity: 0.7; } |
| 774 | + .title-input { width: 100%; margin-bottom: 1rem; font-size: 1.25rem; font-weight: var(--font-semibold, 600); } |
679 | 775 |
|
680 | 776 | /* Status + assignee picker field rows */ |
681 | 777 | .fields { |
|
0 commit comments