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

- 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
This commit is contained in:
HonzysClawdbot 2026-03-15 12:31:15 +01:00 committed by GitHub
parent d980fc1a4e
commit b6717b8bf8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 24 additions and 17 deletions

View File

@ -185,11 +185,8 @@ export async function POST(request: NextRequest) {
} = body;
const normalizedStatus = normalizeTaskCreateStatus(status, assigned_to)
// Check for duplicate title
const existingTask = db.prepare('SELECT id FROM tasks WHERE title = ? AND workspace_id = ?').get(title, workspaceId);
if (existingTask) {
return NextResponse.json({ error: 'Task with this title already exists' }, { status: 409 });
}
// Resolve project_id for the task
const resolvedProjectId = resolveProjectId(db, workspaceId, project_id)
const now = Math.floor(Date.now() / 1000);
const mentionResolution = resolveMentionRecipients(description || '', db, workspaceId);
@ -203,7 +200,6 @@ export async function POST(request: NextRequest) {
const resolvedCompletedAt = completed_at ?? (normalizedStatus === 'done' ? now : null)
const createTaskTx = db.transaction(() => {
const resolvedProjectId = resolveProjectId(db, workspaceId, project_id)
db.prepare(`
UPDATE projects
SET ticket_counter = ticket_counter + 1, updated_at = unixepoch()

View File

@ -794,7 +794,7 @@ export function TaskBoardPanel() {
</div>
{/* Column Body */}
<div className="flex-1 p-2.5 space-y-2.5 min-h-32 overflow-y-auto">
<div className="flex-1 p-2.5 space-y-2.5 min-h-32 h-full overflow-y-auto">
{tasksByStatus[column.key]?.map(task => (
<div
key={task.id}
@ -1253,11 +1253,18 @@ function TaskDetailModal({
if (!confirm(t('deleteTaskConfirm', { title: task.title }))) return
try {
const res = await fetch(`/api/tasks/${task.id}`, { method: 'DELETE' })
if (!res.ok) throw new Error('Failed to delete task')
onDelete()
if (!res.ok) {
const errorData = await res.json().catch(() => ({ error: 'Failed to delete task' }))
throw new Error(errorData.error || 'Failed to delete task')
}
// Close modal immediately on successful deletion
// SSE will handle the task.deleted event and remove the task from the UI
onClose()
} catch {
// task.deleted SSE will sync state if needed
} catch (error) {
// Show error to user
const errorMessage = error instanceof Error ? error.message : 'Failed to delete task'
alert(errorMessage)
// Don't close modal on error
}
}}
>

View File

@ -71,12 +71,12 @@ export async function spawnRecurringTasks(): Promise<{ ok: boolean; message: str
const dateSuffix = formatDateSuffix()
const childTitle = `${template.title} - ${dateSuffix}`
// Duplicate prevention: check if a child with this exact title already exists
// Duplicate prevention: check if a child with this exact title already exists in the same project
const existing = db.prepare(`
SELECT id FROM tasks
WHERE title = ? AND workspace_id = ?
WHERE title = ? AND workspace_id = ? AND project_id = ?
LIMIT 1
`).get(childTitle, template.workspace_id)
`).get(childTitle, template.workspace_id, template.project_id)
if (existing) continue
// Spawn child task

View File

@ -83,7 +83,7 @@ test.describe('Tasks CRUD', () => {
expect(res.status()).toBe(400)
})
test('POST rejects duplicate title', async ({ request }) => {
test('POST allows duplicate title', async ({ request }) => {
const { id, body: first } = await createTestTask(request)
cleanup.push(id)
@ -91,7 +91,11 @@ test.describe('Tasks CRUD', () => {
headers: API_KEY_HEADER,
data: { title: first.task.title },
})
expect(res.status()).toBe(409)
expect(res.status()).toBe(201)
const body = await res.json()
cleanup.push(body.task.id)
expect(body.task.title).toBe(first.task.title)
expect(body.task.id).not.toBe(first.task.id)
})
// ── GET /api/tasks ───────────────────────────