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:
parent
d980fc1a4e
commit
b6717b8bf8
|
|
@ -185,11 +185,8 @@ export async function POST(request: NextRequest) {
|
||||||
} = 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()
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 ───────────────────────────
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue