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

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

View File

@ -794,7 +794,7 @@ export function TaskBoardPanel() {
</div> </div>
{/* Column Body */} {/* 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 => ( {tasksByStatus[column.key]?.map(task => (
<div <div
key={task.id} key={task.id}
@ -1253,11 +1253,18 @@ function TaskDetailModal({
if (!confirm(t('deleteTaskConfirm', { title: task.title }))) return if (!confirm(t('deleteTaskConfirm', { title: task.title }))) return
try { try {
const res = await fetch(`/api/tasks/${task.id}`, { method: 'DELETE' }) const res = await fetch(`/api/tasks/${task.id}`, { method: 'DELETE' })
if (!res.ok) throw new Error('Failed to delete task') if (!res.ok) {
onDelete() 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() onClose()
} catch { } catch (error) {
// task.deleted SSE will sync state if needed // 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 dateSuffix = formatDateSuffix()
const childTitle = `${template.title} - ${dateSuffix}` 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(` const existing = db.prepare(`
SELECT id FROM tasks SELECT id FROM tasks
WHERE title = ? AND workspace_id = ? WHERE title = ? AND workspace_id = ? AND project_id = ?
LIMIT 1 LIMIT 1
`).get(childTitle, template.workspace_id) `).get(childTitle, template.workspace_id, template.project_id)
if (existing) continue if (existing) continue
// Spawn child task // Spawn child task

View File

@ -83,7 +83,7 @@ test.describe('Tasks CRUD', () => {
expect(res.status()).toBe(400) 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) const { id, body: first } = await createTestTask(request)
cleanup.push(id) cleanup.push(id)
@ -91,7 +91,11 @@ test.describe('Tasks CRUD', () => {
headers: API_KEY_HEADER, headers: API_KEY_HEADER,
data: { title: first.task.title }, 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 ─────────────────────────── // ── GET /api/tasks ───────────────────────────