Merge pull request #201 from builderz-labs/fix/184-assignment-status-normalization
fix(tasks): auto-normalize assignment status transitions
This commit is contained in:
commit
0e14683a12
|
|
@ -6,6 +6,7 @@ import { mutationLimiter } from '@/lib/rate-limit';
|
|||
import { logger } from '@/lib/logger';
|
||||
import { validateBody, updateTaskSchema } from '@/lib/validation';
|
||||
import { resolveMentionRecipients } from '@/lib/mentions';
|
||||
import { normalizeTaskUpdateStatus } from '@/lib/task-status';
|
||||
|
||||
function formatTicketRef(prefix?: string | null, num?: number | null): string | undefined {
|
||||
if (!prefix || typeof num !== 'number' || !Number.isFinite(num) || num <= 0) return undefined
|
||||
|
|
@ -115,7 +116,7 @@ export async function PUT(
|
|||
const {
|
||||
title,
|
||||
description,
|
||||
status,
|
||||
status: requestedStatus,
|
||||
priority,
|
||||
project_id,
|
||||
assigned_to,
|
||||
|
|
@ -125,6 +126,12 @@ export async function PUT(
|
|||
tags,
|
||||
metadata
|
||||
} = body;
|
||||
const normalizedStatus = normalizeTaskUpdateStatus({
|
||||
currentStatus: currentTask.status,
|
||||
requestedStatus,
|
||||
assignedTo: assigned_to,
|
||||
assignedToProvided: assigned_to !== undefined,
|
||||
})
|
||||
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const descriptionMentionResolution = description !== undefined
|
||||
|
|
@ -152,15 +159,15 @@ export async function PUT(
|
|||
fieldsToUpdate.push('description = ?');
|
||||
updateParams.push(description);
|
||||
}
|
||||
if (status !== undefined) {
|
||||
if (status === 'done' && !hasAegisApproval(db, taskId, workspaceId)) {
|
||||
if (normalizedStatus !== undefined) {
|
||||
if (normalizedStatus === 'done' && !hasAegisApproval(db, taskId, workspaceId)) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Aegis approval is required to move task to done.' },
|
||||
{ status: 403 }
|
||||
)
|
||||
}
|
||||
fieldsToUpdate.push('status = ?');
|
||||
updateParams.push(status);
|
||||
updateParams.push(normalizedStatus);
|
||||
}
|
||||
if (priority !== undefined) {
|
||||
fieldsToUpdate.push('priority = ?');
|
||||
|
|
@ -240,8 +247,8 @@ export async function PUT(
|
|||
// Track changes and log activities
|
||||
const changes: string[] = [];
|
||||
|
||||
if (status && status !== currentTask.status) {
|
||||
changes.push(`status: ${currentTask.status} → ${status}`);
|
||||
if (normalizedStatus !== undefined && normalizedStatus !== currentTask.status) {
|
||||
changes.push(`status: ${currentTask.status} → ${normalizedStatus}`);
|
||||
|
||||
// Create notification for status change if assigned
|
||||
if (currentTask.assigned_to) {
|
||||
|
|
@ -249,7 +256,7 @@ export async function PUT(
|
|||
currentTask.assigned_to,
|
||||
'status_change',
|
||||
'Task Status Updated',
|
||||
`Task "${currentTask.title}" status changed to ${status}`,
|
||||
`Task "${currentTask.title}" status changed to ${normalizedStatus}`,
|
||||
'task',
|
||||
taskId,
|
||||
workspaceId
|
||||
|
|
@ -322,7 +329,7 @@ export async function PUT(
|
|||
priority: currentTask.priority,
|
||||
assigned_to: currentTask.assigned_to
|
||||
},
|
||||
newValues: { title, status, priority, assigned_to }
|
||||
newValues: { title, status: normalizedStatus ?? currentTask.status, priority, assigned_to }
|
||||
},
|
||||
workspaceId
|
||||
);
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { mutationLimiter } from '@/lib/rate-limit';
|
|||
import { logger } from '@/lib/logger';
|
||||
import { validateBody, createTaskSchema, bulkUpdateTaskStatusSchema } from '@/lib/validation';
|
||||
import { resolveMentionRecipients } from '@/lib/mentions';
|
||||
import { normalizeTaskCreateStatus } from '@/lib/task-status';
|
||||
|
||||
function formatTicketRef(prefix?: string | null, num?: number | null): string | undefined {
|
||||
if (!prefix || typeof num !== 'number' || !Number.isFinite(num) || num <= 0) return undefined
|
||||
|
|
@ -163,7 +164,7 @@ export async function POST(request: NextRequest) {
|
|||
const {
|
||||
title,
|
||||
description,
|
||||
status = 'inbox',
|
||||
status,
|
||||
priority = 'medium',
|
||||
project_id,
|
||||
assigned_to,
|
||||
|
|
@ -173,6 +174,7 @@ export async function POST(request: NextRequest) {
|
|||
tags = [],
|
||||
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);
|
||||
|
|
@ -212,7 +214,7 @@ export async function POST(request: NextRequest) {
|
|||
const dbResult = insertStmt.run(
|
||||
title,
|
||||
description,
|
||||
status,
|
||||
normalizedStatus,
|
||||
priority,
|
||||
resolvedProjectId,
|
||||
row.ticket_counter,
|
||||
|
|
@ -234,7 +236,7 @@ export async function POST(request: NextRequest) {
|
|||
// Log activity
|
||||
db_helpers.logActivity('task_created', 'task', taskId, created_by, `Created task: ${title}`, {
|
||||
title,
|
||||
status,
|
||||
status: normalizedStatus,
|
||||
priority,
|
||||
assigned_to
|
||||
}, workspaceId);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,47 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import { normalizeTaskCreateStatus, normalizeTaskUpdateStatus } from '../task-status'
|
||||
|
||||
describe('task status normalization', () => {
|
||||
it('sets assigned status on create when assignee is present', () => {
|
||||
expect(normalizeTaskCreateStatus(undefined, 'main')).toBe('assigned')
|
||||
expect(normalizeTaskCreateStatus('inbox', 'main')).toBe('assigned')
|
||||
})
|
||||
|
||||
it('keeps explicit non-inbox status on create', () => {
|
||||
expect(normalizeTaskCreateStatus('in_progress', 'main')).toBe('in_progress')
|
||||
})
|
||||
|
||||
it('auto-promotes inbox to assigned when assignment is added via update', () => {
|
||||
expect(
|
||||
normalizeTaskUpdateStatus({
|
||||
currentStatus: 'inbox',
|
||||
requestedStatus: undefined,
|
||||
assignedTo: 'main',
|
||||
assignedToProvided: true,
|
||||
})
|
||||
).toBe('assigned')
|
||||
})
|
||||
|
||||
it('auto-demotes assigned to inbox when assignment is removed via update', () => {
|
||||
expect(
|
||||
normalizeTaskUpdateStatus({
|
||||
currentStatus: 'assigned',
|
||||
requestedStatus: undefined,
|
||||
assignedTo: '',
|
||||
assignedToProvided: true,
|
||||
})
|
||||
).toBe('inbox')
|
||||
})
|
||||
|
||||
it('does not override explicit status changes on update', () => {
|
||||
expect(
|
||||
normalizeTaskUpdateStatus({
|
||||
currentStatus: 'inbox',
|
||||
requestedStatus: 'in_progress',
|
||||
assignedTo: 'main',
|
||||
assignedToProvided: true,
|
||||
})
|
||||
).toBe('in_progress')
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
import type { Task } from './db'
|
||||
|
||||
export type TaskStatus = Task['status']
|
||||
|
||||
function hasAssignee(assignedTo: string | null | undefined): boolean {
|
||||
return Boolean(assignedTo && assignedTo.trim())
|
||||
}
|
||||
|
||||
/**
|
||||
* Keep task state coherent when a task is created with an assignee.
|
||||
* If caller asks for `inbox` but also sets `assigned_to`, normalize to `assigned`.
|
||||
*/
|
||||
export function normalizeTaskCreateStatus(
|
||||
requestedStatus: TaskStatus | undefined,
|
||||
assignedTo: string | undefined
|
||||
): TaskStatus {
|
||||
const status = requestedStatus ?? 'inbox'
|
||||
if (status === 'inbox' && hasAssignee(assignedTo)) return 'assigned'
|
||||
return status
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-adjust status for assignment-only updates when caller does not
|
||||
* explicitly request a status transition.
|
||||
*/
|
||||
export function normalizeTaskUpdateStatus(args: {
|
||||
currentStatus: TaskStatus
|
||||
requestedStatus: TaskStatus | undefined
|
||||
assignedTo: string | null | undefined
|
||||
assignedToProvided: boolean
|
||||
}): TaskStatus | undefined {
|
||||
const { currentStatus, requestedStatus, assignedTo, assignedToProvided } = args
|
||||
if (requestedStatus !== undefined) return requestedStatus
|
||||
if (!assignedToProvided) return undefined
|
||||
|
||||
if (hasAssignee(assignedTo) && currentStatus === 'inbox') return 'assigned'
|
||||
if (!hasAssignee(assignedTo) && currentStatus === 'assigned') return 'inbox'
|
||||
return undefined
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue