M2 checklists: note kind + items backend + editor/card UI
- Migration 0006: notes.kind ('text'|'list') + note_items (text, checked,
position). Item API: add/update(toggle)/delete/reorder; PATCH note kind; note
responses include kind + items[] (merged in one query alongside labels).
- notes store: kind/items on Note, setKind/addItem/updateItem/deleteItem.
- NoteChecklist component (toggle/add/edit/delete items); rendered read-only-ish
on cards (checkboxes toggle) and editable in the editor.
- Editor: convert text<->checklist (body lines become items on convert to list).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { nextTick, onMounted, ref, watch } from "vue";
|
||||
import { computed, nextTick, onMounted, ref, watch } from "vue";
|
||||
import { useNotesStore } from "../stores/notes";
|
||||
import ColorPicker from "./ColorPicker.vue";
|
||||
import Icon from "./Icon.vue";
|
||||
import LabelPicker from "./LabelPicker.vue";
|
||||
import NoteChecklist from "./NoteChecklist.vue";
|
||||
import type { Note, NoteLabel } from "../stores/notes";
|
||||
import type { NoteColor } from "../notes/colors";
|
||||
|
||||
@@ -11,6 +12,10 @@ const props = defineProps<{ note: Note }>();
|
||||
const emit = defineEmits<{ (e: "close"): void }>();
|
||||
const notes = useNotesStore();
|
||||
|
||||
// Read the note reactively from the store so checklist item add/toggle/delete
|
||||
// (which reconcile a fresh note object) reflect live while the editor is open.
|
||||
const liveNote = computed(() => notes.items.find((n) => n.id === props.note.id) ?? props.note);
|
||||
|
||||
const title = ref(props.note.title ?? "");
|
||||
const body = ref(props.note.body);
|
||||
const color = ref<NoteColor>(props.note.color);
|
||||
@@ -44,6 +49,24 @@ async function removeLabel(id: string) {
|
||||
await onLabelsChange(labelList.value.filter((lb) => lb.id !== id));
|
||||
}
|
||||
|
||||
async function toggleKind() {
|
||||
if (liveNote.value.kind === "list") {
|
||||
await notes.setKind(props.note.id, "text");
|
||||
return;
|
||||
}
|
||||
// Convert existing body lines into checklist items, then switch to a list.
|
||||
const lines = body.value
|
||||
.split("\n")
|
||||
.map((s) => s.trim())
|
||||
.filter((s) => s.length > 0);
|
||||
for (const line of lines) await notes.addItem(props.note.id, line);
|
||||
if (lines.length > 0) {
|
||||
body.value = "";
|
||||
await notes.saveEdit(props.note.id, { title: title.value, body: "", color: color.value });
|
||||
}
|
||||
await notes.setKind(props.note.id, "list");
|
||||
}
|
||||
|
||||
async function close() {
|
||||
const changed =
|
||||
(title.value.trim() || null) !== (props.note.title ?? null) ||
|
||||
@@ -80,12 +103,15 @@ async function act(fn: () => Promise<void>) {
|
||||
class="w-full bg-transparent text-base font-semibold outline-none placeholder:text-neutral-400"
|
||||
/>
|
||||
<textarea
|
||||
v-if="liveNote.kind === 'text'"
|
||||
ref="bodyInput"
|
||||
v-model="body"
|
||||
rows="8"
|
||||
placeholder="Take a note…"
|
||||
class="w-full resize-none bg-transparent text-sm leading-relaxed outline-none placeholder:text-neutral-400"
|
||||
/>
|
||||
<NoteChecklist v-else class="py-1" :note-id="liveNote.id" :items="liveNote.items" editable />
|
||||
|
||||
<div v-if="labelList.length" class="flex flex-wrap gap-1.5 pt-1">
|
||||
<span
|
||||
v-for="lb in labelList"
|
||||
@@ -104,9 +130,20 @@ async function act(fn: () => Promise<void>) {
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between gap-2 border-t border-neutral-100 px-3 py-2 dark:border-neutral-800">
|
||||
<ColorPicker v-model="color" />
|
||||
<div class="flex items-center gap-0.5">
|
||||
<button
|
||||
v-if="!note.trashed"
|
||||
type="button"
|
||||
class="icon-btn"
|
||||
:class="liveNote.kind === 'list' ? 'text-brand-700 dark:text-brand' : ''"
|
||||
:title="liveNote.kind === 'list' ? 'Convert to text note' : 'Convert to checklist'"
|
||||
@click="toggleKind"
|
||||
>
|
||||
<Icon name="checkbox" />
|
||||
</button>
|
||||
<LabelPicker v-if="!note.trashed" :model-value="labelList" @update:model-value="onLabelsChange" />
|
||||
<template v-if="!note.trashed">
|
||||
<button
|
||||
|
||||
Reference in New Issue
Block a user