diff --git a/frontend/src/components/TagInput.vue b/frontend/src/components/TagInput.vue new file mode 100644 index 0000000..4fa8f36 --- /dev/null +++ b/frontend/src/components/TagInput.vue @@ -0,0 +1,212 @@ + + + + + diff --git a/frontend/src/components/TiptapEditor.vue b/frontend/src/components/TiptapEditor.vue index a971193..5152bcf 100644 --- a/frontend/src/components/TiptapEditor.vue +++ b/frontend/src/components/TiptapEditor.vue @@ -9,18 +9,15 @@ import DOMPurify from "dompurify"; import { serializeToMarkdown } from "@/utils/markdownSerializer"; import { TagDecoration } from "@/extensions/TagDecoration"; import { WikilinkDecoration } from "@/extensions/WikilinkDecoration"; -import { TagSuggestion } from "@/extensions/TagSuggestion"; import { WikilinkSuggestion } from "@/extensions/WikilinkSuggestion"; const props = withDefaults( defineProps<{ modelValue: string; placeholder?: string; - fetchTags?: (query: string) => Promise; }>(), { placeholder: "", - fetchTags: undefined, } ); @@ -72,9 +69,6 @@ try { }), TagDecoration, WikilinkDecoration, - TagSuggestion.configure({ - fetchTags: props.fetchTags ?? (async () => []), - }), WikilinkSuggestion, ], onUpdate({ editor: ed }) { diff --git a/frontend/src/stores/notes.ts b/frontend/src/stores/notes.ts index f718f94..6494f3a 100644 --- a/frontend/src/stores/notes.ts +++ b/frontend/src/stores/notes.ts @@ -77,6 +77,7 @@ export const useNotesStore = defineStore("notes", () => { async function createNote(data: { title: string; body: string; + tags?: string[]; }): Promise { try { return await apiPost("/api/notes", data); @@ -88,7 +89,7 @@ export const useNotesStore = defineStore("notes", () => { async function updateNote( id: number, - data: Partial> + data: Partial> ): Promise { try { const note = await apiPut(`/api/notes/${id}`, data); diff --git a/frontend/src/stores/tasks.ts b/frontend/src/stores/tasks.ts index d46c5fb..7641adc 100644 --- a/frontend/src/stores/tasks.ts +++ b/frontend/src/stores/tasks.ts @@ -65,6 +65,7 @@ export const useTasksStore = defineStore("tasks", () => { async function createTask(data: { title: string; body: string; + tags?: string[]; status?: TaskStatus; priority?: TaskPriority; due_date?: string | null; @@ -80,7 +81,7 @@ export const useTasksStore = defineStore("tasks", () => { async function updateTask( id: number, data: Partial< - Pick + Pick > ): Promise { try { diff --git a/frontend/src/views/NoteEditorView.vue b/frontend/src/views/NoteEditorView.vue index b5e08c9..9312c6d 100644 --- a/frontend/src/views/NoteEditorView.vue +++ b/frontend/src/views/NoteEditorView.vue @@ -9,6 +9,7 @@ import { apiPost } from "@/api/client"; import type { Editor } from "@tiptap/vue-3"; import MarkdownToolbar from "@/components/MarkdownToolbar.vue"; import TiptapEditor from "@/components/TiptapEditor.vue"; +import TagInput from "@/components/TagInput.vue"; const route = useRoute(); const router = useRouter(); @@ -17,6 +18,7 @@ const toast = useToastStore(); const title = ref(""); const body = ref(""); +const tags = ref([]); const dirty = ref(false); const saving = ref(false); const showPreview = ref(false); @@ -106,6 +108,7 @@ async function fetchTagSuggestions() { const res = await apiPost<{ suggested_tags: string[] }>("/api/notes/suggest-tags", { title: title.value, body: body.value, + current_tags: tags.value, }); suggestedTags.value = res.suggested_tags; } catch { @@ -117,7 +120,9 @@ async function fetchTagSuggestions() { function applyTagSuggestion(tag: string) { if (appliedTags.value.has(tag)) return; - body.value = body.value.trimEnd() + `\n#${tag}`; + if (!tags.value.includes(tag)) { + tags.value = [...tags.value, tag]; + } appliedTags.value.add(tag); markDirty(); } @@ -130,9 +135,13 @@ function dismissTagSuggestions() { // Track saved state for dirty detection let savedTitle = ""; let savedBody = ""; +let savedTags: string[] = []; function markDirty() { - dirty.value = title.value !== savedTitle || body.value !== savedBody; + dirty.value = + title.value !== savedTitle || + body.value !== savedBody || + JSON.stringify(tags.value) !== JSON.stringify(savedTags); } function onBodyUpdate(newVal: string) { @@ -146,8 +155,10 @@ onMounted(async () => { if (store.currentNote) { title.value = store.currentNote.title; body.value = store.currentNote.body; + tags.value = [...(store.currentNote.tags || [])]; savedTitle = title.value; savedBody = body.value; + savedTags = [...tags.value]; } } }); @@ -160,15 +171,18 @@ async function save() { await store.updateNote(noteId.value!, { title: title.value, body: body.value, + tags: tags.value, }); savedTitle = title.value; savedBody = body.value; + savedTags = [...tags.value]; dirty.value = false; toast.show("Note saved"); } else { const note = await store.createNote({ title: title.value, body: body.value, + tags: tags.value, }); dirty.value = false; toast.show("Note created"); @@ -209,9 +223,10 @@ 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 }); + await store.updateNote(noteId.value!, { title: title.value, body: body.value, tags: tags.value }); savedTitle = title.value; savedBody = body.value; + savedTags = [...tags.value]; dirty.value = false; toast.show("Auto-saved"); } catch { @@ -275,6 +290,12 @@ onUnmounted(() => window.removeEventListener("beforeunload", onBeforeUnload)); @input="markDirty" /> + +
+ +