mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 05:26:47 +00:00
fix(shared): align todo contracts with the wire format the client sends
The optional metadata fields (category, due_date, description, assigned_user_id — plus priority on update) now accept explicit null: the client clears fields by sending null, which the schemas rejected. checked accepts the legacy 0/1 numeric form alongside boolean (the integration net pins it). Drops the client-side casts that papered over the too-narrow types.
This commit is contained in:
@@ -350,7 +350,7 @@ function DetailPane({ item, tripId, categories, members, onClose }: {
|
||||
name: name.trim(), description: desc || null,
|
||||
due_date: dueDate || null, category: category || null,
|
||||
assigned_user_id: assignedUserId, priority,
|
||||
} as any)
|
||||
})
|
||||
} catch (err: unknown) { toast.error(err instanceof Error ? err.message : t('common.error')) }
|
||||
setSaving(false)
|
||||
}
|
||||
@@ -566,7 +566,7 @@ function NewTaskPane({ tripId, categories, members, defaultCategory, onCreated,
|
||||
name: name.trim(), description: desc || null, priority,
|
||||
due_date: dueDate || null, category: trimmedCategory || null,
|
||||
assigned_user_id: assignedUserId,
|
||||
} as any)
|
||||
})
|
||||
if (item?.id) onCreated(item.id)
|
||||
} catch (err: unknown) { toast.error(err instanceof Error ? err.message : t('common.error')) }
|
||||
setSaving(false)
|
||||
|
||||
@@ -20,19 +20,6 @@ export interface MTaskSheetProps {
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
/** Payload shape covering both create and update — the update schema allows
|
||||
* clearing a field with `null`, which `TodoUpdateItemRequest` itself (typed
|
||||
* against the create request) doesn't express; the store call below is cast
|
||||
* to it rather than widened to `any`. */
|
||||
interface TodoSavePayload {
|
||||
name: string
|
||||
description: string | null
|
||||
due_date: string | null
|
||||
category: string | null
|
||||
assigned_user_id: number | null
|
||||
priority: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Add/Edit task sheet (spec 03 §4.7 `openTask` / progress-card "New task"):
|
||||
* name, description, list (category), due date, assignee and priority.
|
||||
@@ -99,7 +86,7 @@ export default function MTaskSheet({ planner, open, itemId, categories, members,
|
||||
setSaving(true)
|
||||
try {
|
||||
if (item) {
|
||||
const payload: TodoSavePayload = {
|
||||
const payload: TodoUpdateItemRequest = {
|
||||
name: trimmedName,
|
||||
description: description || null,
|
||||
due_date: dueDate || null,
|
||||
@@ -107,7 +94,7 @@ export default function MTaskSheet({ planner, open, itemId, categories, members,
|
||||
assigned_user_id: assignedUserId,
|
||||
priority,
|
||||
}
|
||||
await tripActions.updateTodoItem(tripId, item.id, payload as unknown as TodoUpdateItemRequest)
|
||||
await tripActions.updateTodoItem(tripId, item.id, payload)
|
||||
} else {
|
||||
const payload: TodoCreateItemRequest = {
|
||||
name: trimmedName,
|
||||
|
||||
@@ -17,6 +17,18 @@ describe('todoCreateItemRequestSchema', () => {
|
||||
// priority is numeric (matches the service), not a string
|
||||
expect(todoCreateItemRequestSchema.safeParse({ name: 'X', priority: 'high' }).success).toBe(false);
|
||||
});
|
||||
|
||||
it('accepts explicit null on the optional metadata fields (client clears with null)', () => {
|
||||
expect(
|
||||
todoCreateItemRequestSchema.safeParse({
|
||||
name: 'X',
|
||||
category: null,
|
||||
due_date: null,
|
||||
description: null,
|
||||
assigned_user_id: null,
|
||||
}).success,
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('todoUpdateItemRequestSchema', () => {
|
||||
@@ -24,6 +36,25 @@ describe('todoUpdateItemRequestSchema', () => {
|
||||
expect(todoUpdateItemRequestSchema.safeParse({}).success).toBe(true);
|
||||
expect(todoUpdateItemRequestSchema.safeParse({ checked: true }).success).toBe(true);
|
||||
});
|
||||
|
||||
it('accepts checked as 0/1 (legacy numeric form) but rejects other numbers', () => {
|
||||
expect(todoUpdateItemRequestSchema.safeParse({ checked: 1 }).success).toBe(true);
|
||||
expect(todoUpdateItemRequestSchema.safeParse({ checked: 0 }).success).toBe(true);
|
||||
expect(todoUpdateItemRequestSchema.safeParse({ checked: 2 }).success).toBe(false);
|
||||
});
|
||||
|
||||
it('accepts explicit null on the nullable fields (bodyKeys clear protocol)', () => {
|
||||
expect(
|
||||
todoUpdateItemRequestSchema.safeParse({
|
||||
category: null,
|
||||
due_date: null,
|
||||
description: null,
|
||||
assigned_user_id: null,
|
||||
priority: null,
|
||||
}).success,
|
||||
).toBe(true);
|
||||
expect(todoUpdateItemRequestSchema.safeParse({ name: null }).success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('todoReorderRequestSchema', () => {
|
||||
|
||||
@@ -12,22 +12,27 @@ import { z } from 'zod';
|
||||
|
||||
export const todoCreateItemRequestSchema = z.object({
|
||||
name: z.string().min(1),
|
||||
category: z.string().optional(),
|
||||
due_date: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
assigned_user_id: z.number().optional(),
|
||||
// The client clears optional fields by sending explicit null (the service
|
||||
// coerces falsy to its defaults), so every optional metadata field is nullable.
|
||||
category: z.string().nullable().optional(),
|
||||
due_date: z.string().nullable().optional(),
|
||||
description: z.string().nullable().optional(),
|
||||
assigned_user_id: z.number().nullable().optional(),
|
||||
priority: z.number().optional(),
|
||||
});
|
||||
export type TodoCreateItemRequest = z.infer<typeof todoCreateItemRequestSchema>;
|
||||
|
||||
export const todoUpdateItemRequestSchema = z.object({
|
||||
name: z.string().optional(),
|
||||
checked: z.boolean().optional(),
|
||||
category: z.string().optional(),
|
||||
due_date: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
assigned_user_id: z.number().optional(),
|
||||
priority: z.number().optional(),
|
||||
// The legacy route accepted both boolean and 0/1 for checked — both stay valid.
|
||||
checked: z.union([z.boolean(), z.number().int().min(0).max(1)]).optional(),
|
||||
// Nullable fields follow the bodyKeys protocol: a key present with null
|
||||
// clears the field, an omitted key leaves it unchanged.
|
||||
category: z.string().nullable().optional(),
|
||||
due_date: z.string().nullable().optional(),
|
||||
description: z.string().nullable().optional(),
|
||||
assigned_user_id: z.number().nullable().optional(),
|
||||
priority: z.number().nullable().optional(),
|
||||
});
|
||||
export type TodoUpdateItemRequest = z.infer<typeof todoUpdateItemRequestSchema>;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user