|
1127 | 1127 | let statusSubmenuOpen = $state(false); |
1128 | 1128 | let prioritySubmenuOpen = $state(false); |
1129 | 1129 | let epicSubmenuOpen = $state(false); |
| 1130 | + let projectSubmenuOpen = $state(false); |
1130 | 1131 | let epics = $state<Epic[]>([]); |
1131 | 1132 | let epicsLoading = $state(false); |
1132 | 1133 | let showCreateEpic = $state(false); |
|
1175 | 1176 | statusSubmenuOpen = false; |
1176 | 1177 | prioritySubmenuOpen = false; |
1177 | 1178 | epicSubmenuOpen = false; |
| 1179 | + projectSubmenuOpen = false; |
1178 | 1180 | } |
1179 | 1181 |
|
1180 | 1182 | function closeContextMenu() { |
1181 | 1183 | ctxVisible = false; |
1182 | 1184 | statusSubmenuOpen = false; |
1183 | 1185 | prioritySubmenuOpen = false; |
1184 | 1186 | epicSubmenuOpen = false; |
| 1187 | + projectSubmenuOpen = false; |
1185 | 1188 | showCreateEpic = false; |
1186 | 1189 | newEpicTitle = ''; |
1187 | 1190 | // Note: ctxTask is intentionally NOT cleared so the DOM persists |
|
1387 | 1390 | } |
1388 | 1391 | } |
1389 | 1392 |
|
| 1393 | + // All unique project names for "Change Project" submenu |
| 1394 | + const allProjectNames = $derived.by(() => { |
| 1395 | + const set = new Set<string>(); |
| 1396 | + for (const t of tasks) set.add(getProjectFromTaskId(t.id)); |
| 1397 | + return [...set].sort(); |
| 1398 | + }); |
| 1399 | +
|
| 1400 | + async function handleChangeProject(taskId: string, newProject: string) { |
| 1401 | + closeContextMenu(); |
| 1402 | + try { |
| 1403 | + const response = await fetch(`/api/tasks/${taskId}`, { |
| 1404 | + method: 'PUT', |
| 1405 | + headers: { 'Content-Type': 'application/json' }, |
| 1406 | + body: JSON.stringify({ project: newProject }), |
| 1407 | + }); |
| 1408 | + if (response.ok) { |
| 1409 | + onRetry(); |
| 1410 | + } |
| 1411 | + } catch (err) { |
| 1412 | + console.error('Failed to change project:', err); |
| 1413 | + } |
| 1414 | + } |
| 1415 | +
|
1390 | 1416 | async function handleDuplicateTask(task: Task) { |
1391 | 1417 | closeContextMenu(); |
1392 | 1418 | const projectName = getProjectFromTaskId(task.id); |
|
1997 | 2023 | onkeydown={(e) => e.stopPropagation()} |
1998 | 2024 | > |
1999 | 2025 | <!-- Launch --> |
2000 | | - <button class="task-context-menu-item" onmouseenter={() => { statusSubmenuOpen = false; prioritySubmenuOpen = false; epicSubmenuOpen = false; }} onclick={() => { const t = ctxTask!; closeContextMenu(); onSpawnTask(t); ctxTask = null; }}> |
| 2026 | + <button class="task-context-menu-item" onmouseenter={() => { statusSubmenuOpen = false; prioritySubmenuOpen = false; epicSubmenuOpen = false; projectSubmenuOpen = false; }} onclick={() => { const t = ctxTask!; closeContextMenu(); onSpawnTask(t); ctxTask = null; }}> |
2001 | 2027 | <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
2002 | 2028 | <path d="M12 2C12 2 8 6 8 12C8 15 9 17 10 18L10 21C10 21.5 10.5 22 11 22H13C13.5 22 14 21.5 14 21L14 18C15 17 16 15 16 12C16 6 12 2 12 2Z" /> |
2003 | 2029 | <circle cx="12" cy="10" r="2" /> |
|
2007 | 2033 |
|
2008 | 2034 | <!-- Resume (only for tasks with a resumable session) --> |
2009 | 2035 | {#if ctxTask.assignee || resumableTasks.has(ctxTask.id)} |
2010 | | - <button class="task-context-menu-item" onmouseenter={() => { statusSubmenuOpen = false; prioritySubmenuOpen = false; epicSubmenuOpen = false; }} onclick={() => { const t = ctxTask!; handleResumeTask(t); ctxTask = null; }} disabled={resumingTaskId === ctxTask.id}> |
| 2036 | + <button class="task-context-menu-item" onmouseenter={() => { statusSubmenuOpen = false; prioritySubmenuOpen = false; epicSubmenuOpen = false; projectSubmenuOpen = false; }} onclick={() => { const t = ctxTask!; handleResumeTask(t); ctxTask = null; }} disabled={resumingTaskId === ctxTask.id}> |
2011 | 2037 | <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
2012 | 2038 | <path d="M5.25 5.653c0-.856.917-1.398 1.667-.986l11.54 6.347a1.125 1.125 0 0 1 0 1.972l-11.54 6.347a1.125 1.125 0 0 1-1.667-.986V5.653Z" /> |
2013 | 2039 | </svg> |
|
2016 | 2042 | {/if} |
2017 | 2043 |
|
2018 | 2044 | <!-- View Details --> |
2019 | | - <button class="task-context-menu-item" onmouseenter={() => { statusSubmenuOpen = false; prioritySubmenuOpen = false; epicSubmenuOpen = false; }} onclick={() => { const id = ctxTask!.id; closeContextMenu(); onTaskClick(id); ctxTask = null; }}> |
| 2045 | + <button class="task-context-menu-item" onmouseenter={() => { statusSubmenuOpen = false; prioritySubmenuOpen = false; epicSubmenuOpen = false; projectSubmenuOpen = false; }} onclick={() => { const id = ctxTask!.id; closeContextMenu(); onTaskClick(id); ctxTask = null; }}> |
2020 | 2046 | <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
2021 | 2047 | <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" /> |
2022 | 2048 | <circle cx="12" cy="12" r="3" /> |
|
2026 | 2052 |
|
2027 | 2053 | <!-- Reply (integrated tasks from ingest — feedback widget, Supabase, Telegram, Slack, etc.) --> |
2028 | 2054 | {#if isIntegratedTask(ctxTask)} |
2029 | | - <button class="task-context-menu-item" onmouseenter={() => { statusSubmenuOpen = false; prioritySubmenuOpen = false; epicSubmenuOpen = false; }} onclick={() => { const t = ctxTask!; closeContextMenu(); openReplyModal(t); }}> |
| 2055 | + <button class="task-context-menu-item" onmouseenter={() => { statusSubmenuOpen = false; prioritySubmenuOpen = false; epicSubmenuOpen = false; projectSubmenuOpen = false; }} onclick={() => { const t = ctxTask!; closeContextMenu(); openReplyModal(t); }}> |
2030 | 2056 | <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
2031 | 2057 | <path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z" /> |
2032 | 2058 | </svg> |
|
2040 | 2066 | <!-- svelte-ignore a11y_no_static_element_interactions --> |
2041 | 2067 | <div |
2042 | 2068 | class="task-context-menu-submenu-container" |
2043 | | - onmouseenter={() => { statusSubmenuOpen = true; prioritySubmenuOpen = false; epicSubmenuOpen = false; }} |
| 2069 | + onmouseenter={() => { statusSubmenuOpen = true; prioritySubmenuOpen = false; epicSubmenuOpen = false; projectSubmenuOpen = false; }} |
2044 | 2070 | onmouseleave={() => { statusSubmenuOpen = false; }} |
2045 | 2071 | > |
2046 | 2072 | <button class="task-context-menu-item task-context-menu-item-has-submenu"> |
|
2082 | 2108 | <!-- svelte-ignore a11y_no_static_element_interactions --> |
2083 | 2109 | <div |
2084 | 2110 | class="task-context-menu-submenu-container" |
2085 | | - onmouseenter={() => { prioritySubmenuOpen = true; statusSubmenuOpen = false; epicSubmenuOpen = false; }} |
| 2111 | + onmouseenter={() => { prioritySubmenuOpen = true; statusSubmenuOpen = false; epicSubmenuOpen = false; projectSubmenuOpen = false; }} |
2086 | 2112 | onmouseleave={() => { prioritySubmenuOpen = false; }} |
2087 | 2113 | > |
2088 | 2114 | <button class="task-context-menu-item task-context-menu-item-has-submenu"> |
|
2124 | 2150 | <!-- svelte-ignore a11y_no_static_element_interactions --> |
2125 | 2151 | <div |
2126 | 2152 | class="task-context-menu-submenu-container" |
2127 | | - onmouseenter={() => { epicSubmenuOpen = true; statusSubmenuOpen = false; prioritySubmenuOpen = false; if (ctxTask) { const p = getProjectFromTaskId(ctxTask.id); fetchEpics(p); } }} |
| 2153 | + onmouseenter={() => { epicSubmenuOpen = true; statusSubmenuOpen = false; prioritySubmenuOpen = false; projectSubmenuOpen = false; if (ctxTask) { const p = getProjectFromTaskId(ctxTask.id); fetchEpics(p); } }} |
2128 | 2154 | onmouseleave={() => { epicSubmenuOpen = false; }} |
2129 | 2155 | > |
2130 | 2156 | <button class="task-context-menu-item task-context-menu-item-has-submenu"> |
|
2193 | 2219 | {/if} |
2194 | 2220 | </div> |
2195 | 2221 |
|
| 2222 | + <!-- Change Project (submenu) --> |
| 2223 | + <!-- svelte-ignore a11y_no_static_element_interactions --> |
| 2224 | + <div |
| 2225 | + class="task-context-menu-submenu-container" |
| 2226 | + onmouseenter={() => { projectSubmenuOpen = true; statusSubmenuOpen = false; prioritySubmenuOpen = false; epicSubmenuOpen = false; }} |
| 2227 | + onmouseleave={() => { projectSubmenuOpen = false; }} |
| 2228 | + > |
| 2229 | + <button class="task-context-menu-item task-context-menu-item-has-submenu"> |
| 2230 | + <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
| 2231 | + <path d="M2 3h6a4 4 0 014 4v14a3 3 0 00-3-3H2z" /> |
| 2232 | + <path d="M22 3h-6a4 4 0 00-4 4v14a3 3 0 013-3h7z" /> |
| 2233 | + </svg> |
| 2234 | + <span>Change Project</span> |
| 2235 | + <svg class="task-context-menu-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
| 2236 | + <polyline points="9 18 15 12 9 6" /> |
| 2237 | + </svg> |
| 2238 | + </button> |
| 2239 | + {#if projectSubmenuOpen} |
| 2240 | + <div class="task-context-submenu task-context-submenu-project"> |
| 2241 | + {#each allProjectNames as proj} |
| 2242 | + {@const currentProject = getProjectFromTaskId(ctxTask!.id)} |
| 2243 | + <button |
| 2244 | + class="task-context-menu-item {currentProject === proj ? 'task-context-menu-item-active' : ''}" |
| 2245 | + onclick={() => handleChangeProject(ctxTask!.id, proj)} |
| 2246 | + > |
| 2247 | + <span class="task-status-dot" style="background: {projectColors[proj] || getProjectColor(proj + '-x')};"></span> |
| 2248 | + <span>{proj}</span> |
| 2249 | + {#if currentProject === proj} |
| 2250 | + <svg class="task-context-menu-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"> |
| 2251 | + <polyline points="20 6 9 17 4 12" /> |
| 2252 | + </svg> |
| 2253 | + {/if} |
| 2254 | + </button> |
| 2255 | + {/each} |
| 2256 | + </div> |
| 2257 | + {/if} |
| 2258 | + </div> |
| 2259 | + |
2196 | 2260 | <div class="task-context-menu-divider"></div> |
2197 | 2261 |
|
2198 | 2262 | <!-- Duplicate --> |
2199 | | - <button class="task-context-menu-item" onmouseenter={() => { statusSubmenuOpen = false; prioritySubmenuOpen = false; epicSubmenuOpen = false; }} onclick={() => handleDuplicateTask(ctxTask!)}> |
| 2263 | + <button class="task-context-menu-item" onmouseenter={() => { statusSubmenuOpen = false; prioritySubmenuOpen = false; epicSubmenuOpen = false; projectSubmenuOpen = false; }} onclick={() => handleDuplicateTask(ctxTask!)}> |
2200 | 2264 | <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
2201 | 2265 | <rect x="9" y="9" width="13" height="13" rx="2" ry="2" /> |
2202 | 2266 | <path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" /> |
|
2205 | 2269 | </button> |
2206 | 2270 |
|
2207 | 2271 | <!-- Close Task --> |
2208 | | - <button class="task-context-menu-item task-context-menu-item-danger" onmouseenter={() => { statusSubmenuOpen = false; prioritySubmenuOpen = false; epicSubmenuOpen = false; }} onclick={() => handleChangeStatus(ctxTask!.id, 'closed')}> |
| 2272 | + <button class="task-context-menu-item task-context-menu-item-danger" onmouseenter={() => { statusSubmenuOpen = false; prioritySubmenuOpen = false; epicSubmenuOpen = false; projectSubmenuOpen = false; }} onclick={() => handleChangeStatus(ctxTask!.id, 'closed')}> |
2209 | 2273 | <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
2210 | 2274 | <circle cx="12" cy="12" r="10" /> |
2211 | 2275 | <line x1="15" y1="9" x2="9" y2="15" /> |
|
2215 | 2279 | </button> |
2216 | 2280 |
|
2217 | 2281 | <!-- Delete Task --> |
2218 | | - <button class="task-context-menu-item task-context-menu-item-danger" onmouseenter={() => { statusSubmenuOpen = false; prioritySubmenuOpen = false; epicSubmenuOpen = false; }} onclick={() => handleDeleteTask(ctxTask!.id)}> |
| 2282 | + <button class="task-context-menu-item task-context-menu-item-danger" onmouseenter={() => { statusSubmenuOpen = false; prioritySubmenuOpen = false; epicSubmenuOpen = false; projectSubmenuOpen = false; }} onclick={() => handleDeleteTask(ctxTask!.id)}> |
2219 | 2283 | <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
2220 | 2284 | <polyline points="3 6 5 6 21 6" /> |
2221 | 2285 | <path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2" /> |
|
3013 | 3077 | overflow-y: auto; |
3014 | 3078 | } |
3015 | 3079 |
|
| 3080 | + .task-context-submenu-project { |
| 3081 | + max-height: 300px; |
| 3082 | + overflow-y: auto; |
| 3083 | + } |
| 3084 | +
|
3016 | 3085 | /* Chevron for submenu indicators */ |
3017 | 3086 | .task-context-menu-chevron { |
3018 | 3087 | width: 12px !important; |
|
0 commit comments