|
1 | 1 | <script> |
| 2 | + import { onDestroy } from 'svelte'; |
| 3 | + import { api } from '../api.js'; |
2 | 4 | import { BasePicker } from '../pickers'; |
3 | 5 | import Input from '../components/Input.svelte'; |
4 | 6 | import Label from '../components/Label.svelte'; |
|
97 | 99 | }; |
98 | 100 | })); |
99 | 101 |
|
100 | | - // Prepare work items for combobox with workspace info |
101 | | - const workItemOptions = $derived(workItems.map(item => ({ |
| 102 | + let workItemResults = $state(null); |
| 103 | + let searchingWorkItems = $state(false); |
| 104 | + let selectedWorkItem = $state(null); |
| 105 | + let workItemSearchVersion = 0; |
| 106 | +
|
| 107 | + function resetWorkItemSearch() { |
| 108 | + workItemSearchVersion += 1; |
| 109 | + workItemResults = null; |
| 110 | + searchingWorkItems = false; |
| 111 | + } |
| 112 | +
|
| 113 | + onDestroy(() => { workItemSearchVersion += 1; }); |
| 114 | +
|
| 115 | + async function searchWorkItems(query) { |
| 116 | + const trimmedQuery = query.trim(); |
| 117 | + if (!trimmedQuery) { |
| 118 | + resetWorkItemSearch(); |
| 119 | + return; |
| 120 | + } |
| 121 | + const version = ++workItemSearchVersion; |
| 122 | + searchingWorkItems = true; |
| 123 | + workItemResults = []; |
| 124 | + try { |
| 125 | + const results = await api.links.search(trimmedQuery, 'item', 20); |
| 126 | + if (version !== workItemSearchVersion) return; |
| 127 | + workItemResults = Array.isArray(results) ? results : []; |
| 128 | + } catch (error) { |
| 129 | + if (version !== workItemSearchVersion) return; |
| 130 | + console.error('Work item search failed:', error); |
| 131 | + workItemResults = []; |
| 132 | + } finally { |
| 133 | + if (version === workItemSearchVersion) searchingWorkItems = false; |
| 134 | + } |
| 135 | + } |
| 136 | +
|
| 137 | + // Keep a remotely selected item available after the search closes. |
| 138 | + const initialWorkItems = $derived(selectedWorkItem && !workItems.some(item => item.id === selectedWorkItem.id) |
| 139 | + ? [selectedWorkItem, ...workItems] |
| 140 | + : workItems); |
| 141 | + const workItemOptions = $derived((workItemResults ?? initialWorkItems).map(item => ({ |
102 | 142 | id: item.id, |
103 | 143 | title: item.title, |
104 | 144 | subtitle: item.workspace_name || 'Unknown Workspace', |
|
232 | 272 | <div> |
233 | 273 | <Label color="default" class="mb-2">{t('time.workItemOptional')}</Label> |
234 | 274 | <BasePicker |
| 275 | + id="time-log-work-item" |
235 | 276 | bind:value={formData.item_id} |
236 | 277 | items={workItemOptions} |
237 | 278 | placeholder={t('placeholders.searchWorkItems')} |
238 | 279 | allowClear={true} |
239 | | - searchFields={['title', 'subtitle']} |
| 280 | + serverSearch={true} |
| 281 | + loading={searchingWorkItems} |
| 282 | + onSearchChange={searchWorkItems} |
| 283 | + onClose={resetWorkItemSearch} |
| 284 | + optionTestid={(option) => `time-log-work-item-option-${option.value}`} |
240 | 285 | getValue={(item) => item?.id} |
241 | 286 | getLabel={(item) => item?.title ?? ''} |
242 | 287 | onSelect={(item) => { |
| 288 | + selectedWorkItem = item?.item ?? null; |
243 | 289 | if (item && !formData.description.trim()) { |
244 | 290 | formData.description = item.title; |
245 | 291 | } |
|
0 commit comments