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:
@@ -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>
|
||||
|
||||
|
||||
@@ -70,6 +70,7 @@ async function createSubTask() {
|
||||
status: "todo",
|
||||
priority: "none",
|
||||
project_id: projectId.value,
|
||||
milestone_id: milestoneId.value,
|
||||
parent_id: taskId.value,
|
||||
});
|
||||
subTasks.value = [...subTasks.value, created];
|
||||
|
||||
Reference in New Issue
Block a user