import { InputError, parseInput } from './lib/validate.ts' import { byField, tzField, dueField, idField, kindField, notesField, priorityField, tagsField, titleField } from './lib/fields.ts' import { addDays, dueInstant, line, localDate, mutateTodo, normalizeId, normalizeTags, parseDue, resolveTimeZone, type Due, type Kind, type Priority, } from './lib/store.ts' /** * Edit a todo/task in the shared todo list: change any field, add/remove tags, clear the due date, or snooze it. * Pass only the fields to change. To clear: `clearDue: true`, `notes: ""`, `project: ""`, `client: ""`. * * @param input - `{ id: string, by: string, title?, notes?, due?: date|datetime|"today"|"tomorrow", clearDue?: boolean, snooze?: { days?: int, hours?: int, until?: date|datetime }, priority?, kind?, tags?: string[] (replace), addTags?: string[], removeTags?: string[], project?: string, client?: string, timeZone?: string }` * @returns `{ todo, line, changes }` * * @example * import updateTodo from 'kody:@charlesmatte/todos/update' * * await updateTodo({ id: 'T12', by: 'assistant', priority: 'high', snooze: { days: 2 } }) */ export default async function updateTodo(input: unknown) { const p = parseInput<{ id: string by: string title?: string notes?: string due?: string clearDue?: boolean snooze?: Record priority?: Priority kind?: Kind tags?: Array addTags?: Array removeTags?: Array project?: string client?: string timeZone?: string }>( input, { id: idField, by: byField, title: titleField, notes: notesField, due: dueField, clearDue: { type: 'boolean' }, snooze: { type: 'object' }, priority: priorityField, kind: kindField, tags: tagsField, addTags: tagsField, removeTags: tagsField, project: { type: 'string', maxLength: 120, allowEmpty: true }, client: { type: 'string', maxLength: 120, allowEmpty: true }, timeZone: tzField, }, 'todos/update', ) const dueModes = [p.due !== undefined, p.clearDue === true, p.snooze !== undefined].filter(Boolean).length if (dueModes > 1) throw new InputError('todos/update: pass only one of "due", "clearDue", "snooze".') if (p.tags && (p.addTags || p.removeTags)) throw new InputError('todos/update: use "tags" (replace) or "addTags"/"removeTags", not both.') const tz = await resolveTimeZone(p.timeZone) let snoozeFn: ((cur: Due | null) => Due) | null = null if (p.snooze) { const s = parseInput<{ days?: number; hours?: number; until?: string }>( p.snooze, { days: { type: 'integer', min: 1, max: 365 }, hours: { type: 'integer', min: 1, max: 24 * 30 }, until: dueField }, 'todos/update snooze', ) const n = [s.days, s.hours, s.until].filter((v) => v !== undefined).length if (n !== 1) throw new InputError('todos/update: "snooze" needs exactly one of { days: int } | { hours: int } | { until: date|datetime }.') if (s.until) { const d = parseDue(s.until, tz, 'snooze.until') snoozeFn = () => d } else if (s.days) { const days = s.days // Shift from the current due (or from today if undated/overdue) by N days, keeping the time of day. snoozeFn = (cur) => { const today = localDate(tz) const base = cur && cur.date >= today ? cur.date : today const date = addDays(base, days) return cur?.time ? parseDue(`${date}T${cur.time}`, cur.timeZone ?? tz) : { date, at: null, time: null, timeZone: tz } } } else { const hours = s.hours! snoozeFn = (cur) => { const base = Math.max(Date.now(), cur ? dueInstant(cur, tz) : 0) return parseDue(new Date(base + hours * 3600_000).toISOString(), tz) } } } const newDue = p.due !== undefined ? parseDue(p.due, tz) : undefined const action = p.snooze ? 'snoozed' : 'updated' const todo = await mutateTodo(normalizeId(p.id), p.by, action, (t) => { const changes: Record = {} const set = (k: K, v: (typeof t)[K]) => { if (JSON.stringify(t[k]) !== JSON.stringify(v)) { changes[k as string] = { from: t[k], to: v } t[k] = v } } if (p.title !== undefined) set('title', p.title) if (p.notes !== undefined) set('notes', p.notes.trim() ? p.notes.trim() : null) if (newDue !== undefined) set('due', newDue) if (p.clearDue) set('due', null) if (snoozeFn) set('due', snoozeFn(t.due)) if (p.priority) set('priority', p.priority) if (p.kind) set('kind', p.kind) if (p.tags) set('tags', normalizeTags(p.tags)) if (p.addTags || p.removeTags) { const rm = new Set(normalizeTags(p.removeTags ?? [])) set('tags', normalizeTags([...t.tags, ...(p.addTags ?? [])]).filter((x) => !rm.has(x))) } if (p.project !== undefined) set('project', p.project.trim() || null) if (p.client !== undefined) set('client', p.client.trim() || null) if (Object.keys(changes).length === 0) throw new InputError('todos/update: nothing to change (all values already match).') return changes }) return { todo, line: line(todo, tz), changes: todo.history[todo.history.length - 1]?.changes ?? {} } }