@ikenga/pkg-tasks 0.2.0 → 0.4.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.
@@ -5,17 +5,21 @@ import {
5
5
  cn,
6
6
  Icon,
7
7
  Button,
8
+ useState,
8
9
  useQuery,
9
10
  useMutation,
10
11
  useQueryClient,
11
12
  } from '../../lib/ui.js';
12
- import { getSupabase } from '../../lib/supabase.js';
13
13
  import { queryKeys } from '../../lib/query-keys.js';
14
14
  import {
15
15
  blockingTaskQuery,
16
+ reassignTask,
16
17
  subtasksQuery,
17
18
  taskDetailQuery,
19
+ updateTaskStatus,
18
20
  } from '../../lib/queries.js';
21
+ import { assigneeOptions } from '../../lib/assignees.js';
22
+ import { getContext } from '../../lib/bridge.js';
19
23
  import {
20
24
  assigneeIsAgent,
21
25
  autoCloseSignal,
@@ -49,20 +53,29 @@ export function TaskDetailPane({ taskId, density = 'full', onNavigateTask }) {
49
53
  const updateStatus = useMutation({
50
54
  /** @param {TaskStatus} status */
51
55
  mutationFn: async (status) => {
52
- /** @type {{ status: TaskStatus, completed_at?: string | null }} */
53
- const patch = { status };
54
- if (status === 'completed') patch.completed_at = new Date().toISOString();
55
- else if (task?.completed_at) patch.completed_at = null;
56
- const { error: e } = await getSupabase()
57
- .from('tasks')
58
- .update(patch)
59
- .eq('id', taskId);
60
- if (e) throw e;
56
+ await updateTaskStatus(taskId, status);
61
57
  },
62
58
  onSuccess: () =>
63
59
  void queryClient.invalidateQueries({ queryKey: queryKeys.tasks.all }),
64
60
  });
65
61
 
62
+ // Reassign — toggled open by the head's Reassign button (was dead until now).
63
+ const [reassignOpen, setReassignOpen] = useState(false);
64
+ const reassign = useMutation({
65
+ /** @param {string} value picked assigned_to ('' = unassign) */
66
+ mutationFn: async (value) => {
67
+ const picked = assigneeOptions(getContext()).find((o) => o.value === value);
68
+ await reassignTask(taskId, value || null, picked ? picked.type : null);
69
+ },
70
+ onSuccess: () => {
71
+ // tasks.all covers every list view (assigned_to is shown across them);
72
+ // detail covers this pane's own query.
73
+ void queryClient.invalidateQueries({ queryKey: queryKeys.tasks.all });
74
+ void queryClient.invalidateQueries({ queryKey: queryKeys.tasks.detail(taskId) });
75
+ setReassignOpen(false);
76
+ },
77
+ });
78
+
66
79
  if (isLoading) {
67
80
  return html`
68
81
  <div class=${cn('tk-detail-pane', `is-${density}`)}>
@@ -106,7 +119,12 @@ export function TaskDetailPane({ taskId, density = 'full', onNavigateTask }) {
106
119
  ${density === 'full' && html`
107
120
  <div class="tk-det-actions">
108
121
  <${Button} variant="outline" size="sm" type="button">Reschedule</${Button}>
109
- <${Button} variant="outline" size="sm" type="button">Reassign</${Button}>
122
+ <${Button}
123
+ variant=${reassignOpen ? 'default' : 'outline'}
124
+ size="sm"
125
+ type="button"
126
+ onClick=${() => setReassignOpen((v) => !v)}
127
+ >Reassign</${Button}>
110
128
  <${Button}
111
129
  size="sm"
112
130
  type="button"
@@ -119,6 +137,59 @@ export function TaskDetailPane({ taskId, density = 'full', onNavigateTask }) {
119
137
  `}
120
138
  </div>
121
139
 
140
+ ${density === 'full' && reassignOpen && html`
141
+ <div
142
+ style=${{
143
+ display: 'flex',
144
+ alignItems: 'center',
145
+ gap: 8,
146
+ marginTop: 8,
147
+ padding: '8px 10px',
148
+ background: 'var(--bg-sunken)',
149
+ border: '1px solid var(--border-soft)',
150
+ borderRadius: 'var(--radius-sm)',
151
+ }}
152
+ >
153
+ <span
154
+ style=${{
155
+ fontFamily: 'var(--font-mono)',
156
+ fontSize: 10.5,
157
+ color: 'var(--fg-faint)',
158
+ letterSpacing: '0.06em',
159
+ textTransform: 'uppercase',
160
+ }}
161
+ >Assign to</span>
162
+ <select
163
+ value=${task.assigned_to ?? ''}
164
+ disabled=${reassign.isPending}
165
+ onChange=${(e) => reassign.mutate(e.target.value)}
166
+ style=${{
167
+ height: 26,
168
+ fontSize: 11.5,
169
+ padding: '0 6px',
170
+ background: 'var(--bg-base)',
171
+ border: '1px solid var(--border)',
172
+ borderRadius: 'var(--radius-xs)',
173
+ color: 'var(--fg)',
174
+ fontFamily: 'inherit',
175
+ }}
176
+ >
177
+ <option value="">Unassigned</option>
178
+ ${task.assigned_to && !assigneeOptions(getContext()).some((o) => o.value === task.assigned_to) &&
179
+ html`<option value=${task.assigned_to}>${task.assigned_to} (current)</option>`}
180
+ ${assigneeOptions(getContext()).map(
181
+ (o) => html`<option key=${o.value} value=${o.value}>${o.label}</option>`,
182
+ )}
183
+ </select>
184
+ ${reassign.isPending && html`<${Icon} name="loader" size=${12} className="tk-spin" />`}
185
+ ${reassign.isError && html`
186
+ <span style=${{ color: 'var(--danger)', fontSize: 11 }}>
187
+ ${(/** @type {Error} */ (reassign.error)).message}
188
+ </span>
189
+ `}
190
+ </div>
191
+ `}
192
+
122
193
  <h2 class="tk-det-title">${task.title}</h2>
123
194
 
124
195
  <div class="tk-det-meta-row">