Merge tasks into notes: a task is just a note with task attributes
A task is now a note with status/priority/due_date columns set (status IS NOT NULL). This eliminates the separate tasks table, companion note system, cascade deletes, bidirectional title sync, and _skip_cascade flags. Migration (0004): - Add status, priority, due_date columns to notes table - Migrate task data from companion notes and orphan tasks - Drop tasks table and old enum types Backend: - models/note.py: Add TaskStatus/TaskPriority enums, task columns, is_task property - models/task.py: Deleted (merged into note.py) - models/__init__.py: Re-export enums from note.py, remove Task import - services/notes.py: Remove companion/cascade logic, add is_task filter, convert_note_to_task, convert_task_to_note, simplified backlinks - services/tasks.py: Rewritten as thin wrappers around notes service - routes/notes.py: Add is_task filter (default false), task fields in CRUD, convert-to-note endpoint - routes/tasks.py: description→body (with fallback), remove note_id filter Frontend: - types/note.ts: Add TaskStatus, TaskPriority, task fields to Note interface - types/task.ts: Task is now a re-export alias for Note - stores/notes.ts: Simplify convertToTask, add convertToNote - stores/tasks.ts: description→body in createTask/updateTask - TaskEditorView: description→body, remove companion note UI - TaskViewerView: description→body, remove companion note link, add Convert to Note - NoteViewerView: Remove companion task UI, simplify convert-to-task - TaskCard: description→body, non-null assertions for status/priority Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -6,9 +6,7 @@ import { useNotesStore } from "@/stores/notes";
|
||||
import { useToastStore } from "@/stores/toast";
|
||||
import { renderMarkdown } from "@/utils/markdown";
|
||||
import { useAutocomplete } from "@/composables/useAutocomplete";
|
||||
import { apiGet } from "@/api/client";
|
||||
import type { TaskStatus, TaskPriority } from "@/types/task";
|
||||
import type { Note } from "@/types/note";
|
||||
import MarkdownToolbar from "@/components/MarkdownToolbar.vue";
|
||||
|
||||
const route = useRoute();
|
||||
@@ -18,12 +16,10 @@ const notesStore = useNotesStore();
|
||||
const toast = useToastStore();
|
||||
|
||||
const title = ref("");
|
||||
const description = ref("");
|
||||
const body = ref("");
|
||||
const status = ref<TaskStatus>("todo");
|
||||
const priority = ref<TaskPriority>("none");
|
||||
const dueDate = ref("");
|
||||
const noteId = ref<number | null>(null);
|
||||
const companionNote = ref<Note | null>(null);
|
||||
const dirty = ref(false);
|
||||
const saving = ref(false);
|
||||
const showPreview = ref(false);
|
||||
@@ -34,7 +30,7 @@ const taskId = computed(() =>
|
||||
);
|
||||
const isEditing = computed(() => taskId.value !== null);
|
||||
|
||||
const renderedPreview = computed(() => renderMarkdown(description.value));
|
||||
const renderedPreview = computed(() => renderMarkdown(body.value));
|
||||
|
||||
// Autocomplete
|
||||
const {
|
||||
@@ -47,10 +43,10 @@ const {
|
||||
accept: acAccept,
|
||||
dismiss: acDismiss,
|
||||
onKeydown: acOnKeydown,
|
||||
} = useAutocomplete(textareaRef, description, (q) => notesStore.fetchAllTags(q));
|
||||
} = useAutocomplete(textareaRef, body, (q) => notesStore.fetchAllTags(q));
|
||||
|
||||
let savedTitle = "";
|
||||
let savedDescription = "";
|
||||
let savedBody = "";
|
||||
let savedStatus: TaskStatus = "todo";
|
||||
let savedPriority: TaskPriority = "none";
|
||||
let savedDueDate = "";
|
||||
@@ -58,7 +54,7 @@ let savedDueDate = "";
|
||||
function markDirty() {
|
||||
dirty.value =
|
||||
title.value !== savedTitle ||
|
||||
description.value !== savedDescription ||
|
||||
body.value !== savedBody ||
|
||||
status.value !== savedStatus ||
|
||||
priority.value !== savedPriority ||
|
||||
dueDate.value !== savedDueDate;
|
||||
@@ -71,7 +67,7 @@ function autoGrow() {
|
||||
el.style.height = Math.max(el.scrollHeight, 200) + "px";
|
||||
}
|
||||
|
||||
function onDescriptionInput() {
|
||||
function onBodyInput() {
|
||||
markDirty();
|
||||
autoGrow();
|
||||
detectTrigger();
|
||||
@@ -93,10 +89,10 @@ function insertAtCursor(before: string, after: string, placeholder: string) {
|
||||
el.focus();
|
||||
const start = el.selectionStart;
|
||||
const end = el.selectionEnd;
|
||||
const selected = description.value.slice(start, end);
|
||||
const selected = body.value.slice(start, end);
|
||||
const insert = selected || placeholder;
|
||||
description.value =
|
||||
description.value.slice(0, start) + before + insert + after + description.value.slice(end);
|
||||
body.value =
|
||||
body.value.slice(0, start) + before + insert + after + body.value.slice(end);
|
||||
markDirty();
|
||||
nextTick(() => {
|
||||
const newStart = start + before.length;
|
||||
@@ -113,24 +109,15 @@ onMounted(async () => {
|
||||
await store.fetchTask(taskId.value);
|
||||
if (store.currentTask) {
|
||||
title.value = store.currentTask.title;
|
||||
description.value = store.currentTask.description;
|
||||
status.value = store.currentTask.status;
|
||||
priority.value = store.currentTask.priority;
|
||||
body.value = store.currentTask.body;
|
||||
status.value = store.currentTask.status as TaskStatus;
|
||||
priority.value = store.currentTask.priority as TaskPriority;
|
||||
dueDate.value = store.currentTask.due_date || "";
|
||||
noteId.value = store.currentTask.note_id;
|
||||
savedTitle = title.value;
|
||||
savedDescription = description.value;
|
||||
savedBody = body.value;
|
||||
savedStatus = status.value;
|
||||
savedPriority = priority.value;
|
||||
savedDueDate = dueDate.value;
|
||||
|
||||
if (noteId.value) {
|
||||
try {
|
||||
companionNote.value = await apiGet<Note>(`/api/notes/${noteId.value}`);
|
||||
} catch {
|
||||
companionNote.value = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
nextTick(autoGrow);
|
||||
@@ -142,7 +129,7 @@ async function save() {
|
||||
try {
|
||||
const data = {
|
||||
title: title.value,
|
||||
description: description.value,
|
||||
body: body.value,
|
||||
status: status.value,
|
||||
priority: priority.value,
|
||||
due_date: dueDate.value || null,
|
||||
@@ -150,7 +137,7 @@ async function save() {
|
||||
if (isEditing.value) {
|
||||
await store.updateTask(taskId.value!, data);
|
||||
savedTitle = title.value;
|
||||
savedDescription = description.value;
|
||||
savedBody = body.value;
|
||||
savedStatus = status.value;
|
||||
savedPriority = priority.value;
|
||||
savedDueDate = dueDate.value;
|
||||
@@ -254,10 +241,10 @@ onUnmounted(() => window.removeEventListener("beforeunload", onBeforeUnload));
|
||||
<div class="textarea-wrapper" v-show="!showPreview">
|
||||
<textarea
|
||||
ref="textareaRef"
|
||||
v-model="description"
|
||||
v-model="body"
|
||||
placeholder="Describe this task... Use #tags inline"
|
||||
class="body-input"
|
||||
@input="onDescriptionInput"
|
||||
@input="onBodyInput"
|
||||
@keydown="onTextareaKeydown"
|
||||
@blur="onTextareaBlur"
|
||||
></textarea>
|
||||
@@ -314,13 +301,6 @@ onUnmounted(() => window.removeEventListener("beforeunload", onBeforeUnload));
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="companionNote" class="field companion-note">
|
||||
<label>Companion Note</label>
|
||||
<router-link :to="`/notes/${companionNote.id}`" class="note-link">
|
||||
{{ companionNote.title || "Untitled" }}
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<!-- Delete confirmation -->
|
||||
<teleport to="body">
|
||||
<div v-if="showDeleteConfirm" class="modal-overlay" @click="showDeleteConfirm = false">
|
||||
@@ -476,19 +456,6 @@ onUnmounted(() => window.removeEventListener("beforeunload", onBeforeUnload));
|
||||
color: var(--color-text);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.companion-note {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.note-link {
|
||||
color: var(--color-primary);
|
||||
text-decoration: none;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.note-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
|
||||
Reference in New Issue
Block a user