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
|
|
@ -184,12 +184,9 @@ export async function POST(request: NextRequest) {
|
|||
metadata = {}
|
||||
} = 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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 ───────────────────────────
|
||||
|
|
|
|||
Loading…
Reference in New Issue