Import: ThoughtSync-native round-trip + Google Keep Takeout
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 6s
CI & Build / Python tests (push) Successful in 8s
CI & Build / Build & push image (push) Successful in 33s

Complete the export/import pair (task 1907). POST /api/notes/import takes
an uploaded .zip and appends its notes — never overwriting existing ones.

Two formats, auto-detected:
- ThoughtSync export: recognized by its notes.json (app == thoughtsync);
  round-trips title/body/color/kind/pinned/archived/remind_at/timestamps/
  labels/items and re-attaches image media from the zip.
- Google Keep Takeout: each Keep <note>.json → a note. Maps title,
  textContent/listContent (+ checked), labels, Keep color enum (nearest
  palette match), isPinned/isArchived, isTrashed (→ trash), created/edited
  microsecond timestamps; folds annotation URLs into the body; resolves
  attachment filePaths relative to the note's folder.

Imported notes reuse create_note's derivation + reconciliation:
display-title derive, #tag reconcile, [[wiki-link]] rewrite. Explicit
labels attach as manual (via_tag=false); inline #tags reconcile as tags.
Image attachments copied into media storage; non-image types (e.g. Keep
audio) skipped until any-file attachments land.

Frontend: an Import control in the sidebar (next to Export) — hidden file
input + FormData POST + result toast ("Imported N notes (M skipped)"),
reloading the board + labels. New upload icon; notes-store importNotes().

Tests: import auth-guard + pure-helper coverage (_usec_to_dt, _keep_spec
list/text/color/annotation/attachment mapping, _native_spec round-trip).

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:
2026-07-22 21:24:02 -04:00
co-authored by Claude Opus 4.8
parent 1417479729
commit 333ab9ce74
6 changed files with 439 additions and 0 deletions
+18
View File
@@ -171,6 +171,23 @@ export const useNotesStore = defineStore("notes", () => {
reconcile(await api.del<Note>(`/api/notes/${id}/attachments/${attId}`));
}
async function importNotes(file: File): Promise<{ source: string; imported: number; skipped: number }> {
const form = new FormData();
form.append("file", file);
const resp = await fetch("/api/notes/import", { method: "POST", credentials: "include", body: form });
const data: unknown = await resp.json().catch(() => ({}));
if (!resp.ok) {
const message =
typeof data === "object" && data !== null && "error" in data
? String((data as { error: unknown }).error)
: "Import failed.";
throw { error: message, status: resp.status };
}
// Refresh the current lens so imported notes appear (labels reloaded by caller).
await load(view.value, activeLabel.value);
return data as { source: string; imported: number; skipped: number };
}
async function fetchOne(id: string): Promise<Note | null> {
try {
return await api.get<Note>(`/api/notes/${id}`);
@@ -242,6 +259,7 @@ export const useNotesStore = defineStore("notes", () => {
deleteItem,
uploadAttachment,
deleteAttachment,
importNotes,
fetchOne,
createTitled,
reorder,