Fix project/milestone association and redesign AppHeader navigation

Backend:
- routes/tasks.py: POST + PUT were silently dropping project_id,
  milestone_id, parent_id from request body — root cause of association
  not saving from the task editor
- routes/tasks.py: GET /api/tasks/:id now includes parent_title in
  response (secondary lookup when parent_id is set)
- routes/notes.py: add PATCH /api/notes/:id for partial updates (used
  by sub-task status toggle; PUT already existed but PATCH was missing)
- routes/projects.py: GET /api/projects/:id/notes now fetches milestone
  IDs and passes them via milestone_ids so tasks assigned to a milestone
  (but lacking project_id) are included in the project view
- services/notes.py: create_note() auto-sets project_id from milestone
  when milestone_id is provided and project_id is omitted; list_notes()
  gains milestone_ids param — when combined with project_id uses OR
  condition (project_id=X OR milestone_id IN (...))

Frontend:
- NoteEditorView: add MilestoneSelector; milestone resets when project
  changes; all save paths (save/create/auto-save) include milestone_id
- stores/notes.ts: add milestone_id to createNote + updateNote types
- TaskEditorView: sub-tasks now inherit milestone_id from parent task
- AppHeader: three-zone layout — brand left, Notes/Projects/Tasks/Chat
  centered (absolute positioning), right rail with status/theme/? and
  gear dropdown containing Settings/Users/Logs; mobile dropdown unchanged

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Bryan Van Deusen
2026-03-03 13:34:01 -05:00
parent 47c9cb39a2
commit 87d3f3ea16
10 changed files with 309 additions and 120 deletions
+17 -3
View File
@@ -11,6 +11,7 @@ import MarkdownToolbar from "@/components/MarkdownToolbar.vue";
import TiptapEditor from "@/components/TiptapEditor.vue";
import TagInput from "@/components/TagInput.vue";
import ProjectSelector from "@/components/ProjectSelector.vue";
import MilestoneSelector from "@/components/MilestoneSelector.vue";
const route = useRoute();
const router = useRouter();
@@ -21,6 +22,7 @@ const title = ref("");
const body = ref("");
const tags = ref<string[]>([]);
const projectId = ref<number | null>(null);
const milestoneId = ref<number | null>(null);
const dirty = ref(false);
const saving = ref(false);
const showPreview = ref(false);
@@ -139,13 +141,15 @@ let savedTitle = "";
let savedBody = "";
let savedTags: string[] = [];
let savedProjectId: number | null = null;
let savedMilestoneId: number | null = null;
function markDirty() {
dirty.value =
title.value !== savedTitle ||
body.value !== savedBody ||
JSON.stringify(tags.value) !== JSON.stringify(savedTags) ||
projectId.value !== savedProjectId;
projectId.value !== savedProjectId ||
milestoneId.value !== savedMilestoneId;
}
function onBodyUpdate(newVal: string) {
@@ -161,10 +165,12 @@ onMounted(async () => {
body.value = store.currentNote.body;
tags.value = [...(store.currentNote.tags || [])];
projectId.value = store.currentNote.project_id ?? null;
milestoneId.value = store.currentNote.milestone_id ?? null;
savedTitle = title.value;
savedBody = body.value;
savedTags = [...tags.value];
savedProjectId = projectId.value;
savedMilestoneId = milestoneId.value;
}
}
});
@@ -179,11 +185,13 @@ async function save() {
body: body.value,
tags: tags.value,
project_id: projectId.value,
milestone_id: milestoneId.value,
});
savedTitle = title.value;
savedBody = body.value;
savedTags = [...tags.value];
savedProjectId = projectId.value;
savedMilestoneId = milestoneId.value;
dirty.value = false;
toast.show("Note saved");
} else {
@@ -192,6 +200,7 @@ async function save() {
body: body.value,
tags: tags.value,
project_id: projectId.value,
milestone_id: milestoneId.value,
});
dirty.value = false;
toast.show("Note created");
@@ -232,11 +241,12 @@ async function autoSave() {
if (!isEditing.value || !dirty.value || saving.value) return;
saving.value = true;
try {
await store.updateNote(noteId.value!, { title: title.value, body: body.value, tags: tags.value, project_id: projectId.value } as Record<string, unknown>);
await store.updateNote(noteId.value!, { title: title.value, body: body.value, tags: tags.value, project_id: projectId.value, milestone_id: milestoneId.value } as Record<string, unknown>);
savedTitle = title.value;
savedBody = body.value;
savedTags = [...tags.value];
savedProjectId = projectId.value;
savedMilestoneId = milestoneId.value;
dirty.value = false;
toast.show("Auto-saved");
} catch {
@@ -303,7 +313,11 @@ onUnmounted(() => window.removeEventListener("beforeunload", onBeforeUnload));
<div class="field-row-meta">
<div class="meta-field">
<label class="meta-label">Project</label>
<ProjectSelector v-model="projectId" @update:modelValue="markDirty" />
<ProjectSelector v-model="projectId" @update:modelValue="milestoneId = null; markDirty()" />
</div>
<div class="meta-field">
<label class="meta-label">Milestone</label>
<MilestoneSelector :projectId="projectId" v-model="milestoneId" @update:modelValue="markDirty" />
</div>
</div>