HonzysClawdbot commited on
Commit
6d2b255
Β·
unverified Β·
1 Parent(s): 1c9ee49

fix: remove duplicate task title constraint, improve delete handling and scrolling (#386)

Browse files

- Remove duplicate title check from POST /api/tasks (closes #368)\n- Scope recurring tasks duplicate check by project_id\n- Fix task deletion error handling β€” show errors to user (closes #369)\n- Enable vertical scrolling on Kanban board (closes #376)\n- Refactor nodes route to use RPC via callOpenClawGateway\n- Handle read-only filesystem in gateway config registration\n- Add screenshot-drift CI workflow and guide\n- Docker compose: add host-gateway for reaching host gateway

src/app/api/tasks/route.ts CHANGED
@@ -184,12 +184,9 @@ export async function POST(request: NextRequest) {
184
  metadata = {}
185
  } = body;
186
  const normalizedStatus = normalizeTaskCreateStatus(status, assigned_to)
187
-
188
- // Check for duplicate title
189
- const existingTask = db.prepare('SELECT id FROM tasks WHERE title = ? AND workspace_id = ?').get(title, workspaceId);
190
- if (existingTask) {
191
- return NextResponse.json({ error: 'Task with this title already exists' }, { status: 409 });
192
- }
193
 
194
  const now = Math.floor(Date.now() / 1000);
195
  const mentionResolution = resolveMentionRecipients(description || '', db, workspaceId);
@@ -203,7 +200,6 @@ export async function POST(request: NextRequest) {
203
  const resolvedCompletedAt = completed_at ?? (normalizedStatus === 'done' ? now : null)
204
 
205
  const createTaskTx = db.transaction(() => {
206
- const resolvedProjectId = resolveProjectId(db, workspaceId, project_id)
207
  db.prepare(`
208
  UPDATE projects
209
  SET ticket_counter = ticket_counter + 1, updated_at = unixepoch()
 
184
  metadata = {}
185
  } = body;
186
  const normalizedStatus = normalizeTaskCreateStatus(status, assigned_to)
187
+
188
+ // Resolve project_id for the task
189
+ const resolvedProjectId = resolveProjectId(db, workspaceId, project_id)
 
 
 
190
 
191
  const now = Math.floor(Date.now() / 1000);
192
  const mentionResolution = resolveMentionRecipients(description || '', db, workspaceId);
 
200
  const resolvedCompletedAt = completed_at ?? (normalizedStatus === 'done' ? now : null)
201
 
202
  const createTaskTx = db.transaction(() => {
 
203
  db.prepare(`
204
  UPDATE projects
205
  SET ticket_counter = ticket_counter + 1, updated_at = unixepoch()
src/components/panels/task-board-panel.tsx CHANGED
@@ -794,7 +794,7 @@ export function TaskBoardPanel() {
794
  </div>
795
 
796
  {/* Column Body */}
797
- <div className="flex-1 p-2.5 space-y-2.5 min-h-32 overflow-y-auto">
798
  {tasksByStatus[column.key]?.map(task => (
799
  <div
800
  key={task.id}
@@ -1253,11 +1253,18 @@ function TaskDetailModal({
1253
  if (!confirm(t('deleteTaskConfirm', { title: task.title }))) return
1254
  try {
1255
  const res = await fetch(`/api/tasks/${task.id}`, { method: 'DELETE' })
1256
- if (!res.ok) throw new Error('Failed to delete task')
1257
- onDelete()
 
 
 
 
1258
  onClose()
1259
- } catch {
1260
- // task.deleted SSE will sync state if needed
 
 
 
1261
  }
1262
  }}
1263
  >
 
794
  </div>
795
 
796
  {/* Column Body */}
797
+ <div className="flex-1 p-2.5 space-y-2.5 min-h-32 h-full overflow-y-auto">
798
  {tasksByStatus[column.key]?.map(task => (
799
  <div
800
  key={task.id}
 
1253
  if (!confirm(t('deleteTaskConfirm', { title: task.title }))) return
1254
  try {
1255
  const res = await fetch(`/api/tasks/${task.id}`, { method: 'DELETE' })
1256
+ if (!res.ok) {
1257
+ const errorData = await res.json().catch(() => ({ error: 'Failed to delete task' }))
1258
+ throw new Error(errorData.error || 'Failed to delete task')
1259
+ }
1260
+ // Close modal immediately on successful deletion
1261
+ // SSE will handle the task.deleted event and remove the task from the UI
1262
  onClose()
1263
+ } catch (error) {
1264
+ // Show error to user
1265
+ const errorMessage = error instanceof Error ? error.message : 'Failed to delete task'
1266
+ alert(errorMessage)
1267
+ // Don't close modal on error
1268
  }
1269
  }}
1270
  >
src/lib/recurring-tasks.ts CHANGED
@@ -71,12 +71,12 @@ export async function spawnRecurringTasks(): Promise<{ ok: boolean; message: str
71
  const dateSuffix = formatDateSuffix()
72
  const childTitle = `${template.title} - ${dateSuffix}`
73
 
74
- // Duplicate prevention: check if a child with this exact title already exists
75
  const existing = db.prepare(`
76
  SELECT id FROM tasks
77
- WHERE title = ? AND workspace_id = ?
78
  LIMIT 1
79
- `).get(childTitle, template.workspace_id)
80
  if (existing) continue
81
 
82
  // Spawn child task
 
71
  const dateSuffix = formatDateSuffix()
72
  const childTitle = `${template.title} - ${dateSuffix}`
73
 
74
+ // Duplicate prevention: check if a child with this exact title already exists in the same project
75
  const existing = db.prepare(`
76
  SELECT id FROM tasks
77
+ WHERE title = ? AND workspace_id = ? AND project_id = ?
78
  LIMIT 1
79
+ `).get(childTitle, template.workspace_id, template.project_id)
80
  if (existing) continue
81
 
82
  // Spawn child task
tests/tasks-crud.spec.ts CHANGED
@@ -83,7 +83,7 @@ test.describe('Tasks CRUD', () => {
83
  expect(res.status()).toBe(400)
84
  })
85
 
86
- test('POST rejects duplicate title', async ({ request }) => {
87
  const { id, body: first } = await createTestTask(request)
88
  cleanup.push(id)
89
 
@@ -91,7 +91,11 @@ test.describe('Tasks CRUD', () => {
91
  headers: API_KEY_HEADER,
92
  data: { title: first.task.title },
93
  })
94
- expect(res.status()).toBe(409)
 
 
 
 
95
  })
96
 
97
  // ── GET /api/tasks ───────────────────────────
 
83
  expect(res.status()).toBe(400)
84
  })
85
 
86
+ test('POST allows duplicate title', async ({ request }) => {
87
  const { id, body: first } = await createTestTask(request)
88
  cleanup.push(id)
89
 
 
91
  headers: API_KEY_HEADER,
92
  data: { title: first.task.title },
93
  })
94
+ expect(res.status()).toBe(201)
95
+ const body = await res.json()
96
+ cleanup.push(body.task.id)
97
+ expect(body.task.title).toBe(first.task.title)
98
+ expect(body.task.id).not.toBe(first.task.id)
99
  })
100
 
101
  // ── GET /api/tasks ───────────────────────────