Add hierarchical tasks with tree and flat mode
- parent_id on tasks; roots shown by default, others expandable - Expand/collapse per node with expand/collapse button - Sub button creates subtask directly from the row - Make subtask of button in edit dialog with searchable picker - Remove parent clears the parent link in the dialog - Flat mode toggle shows all tasks indented by depth - Tree filter shows nodes whose descendants match, not only direct matches - Deletion blocked if task has subtasks (client + server) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
14
server.mjs
14
server.mjs
@@ -3,7 +3,7 @@ process.on('uncaughtException', (err) => { console.error('[uncaughtException
|
||||
|
||||
import express from 'express';
|
||||
import { next_id } from './lib/ids.mjs';
|
||||
import { list_tasks, get_task, set_task, delete_task } from './lib/storage.mjs';
|
||||
import { list_tasks, get_task, set_task, delete_task, has_child_tasks } from './lib/storage.mjs';
|
||||
import { get_config } from './lib/config.mjs';
|
||||
|
||||
const app = express();
|
||||
@@ -50,8 +50,9 @@ app.get('/api/tasks', (req, res) => {
|
||||
});
|
||||
|
||||
app.post('/api/tasks', (req, res) => {
|
||||
const { title, body = '', status = 'open', priority = 'normal', tags = [] } = req.body;
|
||||
const { title, body = '', status = 'open', priority = 'normal', tags = [], parent_id = null } = req.body;
|
||||
if (!title?.trim()) { return fail(res, 'title is required'); }
|
||||
if (parent_id !== null && !get_task(Number(parent_id))) { return fail(res, 'parent task not found', 404); }
|
||||
const now = Date.now();
|
||||
const task = {
|
||||
id: next_id('task'),
|
||||
@@ -60,6 +61,7 @@ app.post('/api/tasks', (req, res) => {
|
||||
status,
|
||||
priority,
|
||||
tags: Array.isArray(tags) ? tags : [],
|
||||
parent_id: parent_id !== null ? Number(parent_id) : null,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
};
|
||||
@@ -76,19 +78,23 @@ app.get('/api/tasks/:id', (req, res) => {
|
||||
app.put('/api/tasks/:id', (req, res) => {
|
||||
const existing = get_task(Number(req.params.id));
|
||||
if (!existing) { return fail(res, 'not found', 404); }
|
||||
const { title, body, status, priority, tags } = req.body;
|
||||
const { title, body, status, priority, tags, parent_id } = req.body;
|
||||
const updated = { ...existing, updated_at: Date.now() };
|
||||
if (title !== undefined) { updated.title = title.trim(); }
|
||||
if (body !== undefined) { updated.body = body.trim(); }
|
||||
if (status !== undefined) { updated.status = status; }
|
||||
if (priority !== undefined) { updated.priority = priority; }
|
||||
if (tags !== undefined && Array.isArray(tags)) { updated.tags = tags; }
|
||||
if (parent_id !== undefined) { updated.parent_id = parent_id !== null ? Number(parent_id) : null; }
|
||||
set_task(updated);
|
||||
ok(res, { task: updated });
|
||||
});
|
||||
|
||||
app.delete('/api/tasks/:id', (req, res) => {
|
||||
if (!delete_task(Number(req.params.id))) { return fail(res, 'not found', 404); }
|
||||
const id = Number(req.params.id);
|
||||
if (!get_task(id)) { return fail(res, 'not found', 404); }
|
||||
if (has_child_tasks(id)) { return fail(res, 'task has subtasks — delete or reparent them first'); }
|
||||
delete_task(id);
|
||||
ok(res);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user