Sort a notebook, import Markdown back, and stop the page shrinking in Draw

The paper measurably shrank on switching to Draw — 733px to 460px on the same
screen. The editor was a flex item at its default `flex: 0 1 auto`, so its width
was its widest row's: the toolbar. Swapping a dozen formatting buttons for six
drawing ones collapsed it. It now fills the pane, so the width is a property of
the window.

Each notebook remembers how its notes are ordered, on the server rather than per
device. The manual order has to survive being looked away from, so `reindex`
renumbers every note in the notebook rather than the filtered view, and dragging
is switched off while a search narrows the list. Reordering has its own route:
`PUT /notes/:id` would bump `updated_at` and snapshot a version, so a drag would
move a note to the top of "last changed" and spend a slot of its history.

Markdown and our own ZIP archives come back as notes, with their images
re-uploaded as attachments. A drawing returns as the PNG it was exported as —
vectors are not recoverable from pixels — and the import says so rather than
leaving it to be discovered. Writing the parser turned up a real bug in the
exporter: blocks were emitted with no blank line between them, so a list after a
list became one list and a quote after an item was read as part of it.

Creating or importing from "All Notes" now asks which notebook instead of
silently taking the first one. The web-link prompt is a real dialog: a bare
domain becomes https, javascript: is refused with a reason. The PDF viewer's
hide-drawing control is a labelled button rather than a filled-or-hollow dot,
and the export icon points down into a tray like every other download.

186 unit tests, 179 browser checks across eleven suites, including a new
`offline` suite that loads the app cold with the network off.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
marcandClaude Opus 5 committed 2026-08-09 01:38:03 +02:00
1 parent 3acb8b66a4
commit 0a0864c58b
27 files changed
+2962 -86

No files matched your search

+165
View File
@@ -0,0 +1,165 @@
import { useEffect, useMemo, useRef, useState } from "react";
import { importFiles } from "../import/importNotes";
/**
* Runs an import and says plainly what came of it.
*
* The report is the point. An import can succeed in the sense that no error was
* thrown and still not be what the user expected — a drawing arrives as a flat
* picture, an image the archive did not contain cannot be restored. Those are
* stated afterwards rather than left to be discovered, because the moment to
* learn that a drawing is no longer editable is now, while the original export
* is still on disk.
*/
export function ImportDialog({ files, folderId, onDone, onClose, dark }) {
const [progress, setProgress] = useState({ done: 0, total: 0, label: "" });
const [result, setResult] = useState(null);
const [error, setError] = useState("");
const started = useRef(false);
const T = useMemo(
() => ({
bg: dark ? "#161b22" : "#fff",
border: dark ? "#30363d" : "#d0d7de",
text: dark ? "#e6edf3" : "#1f2328",
muted: dark ? "#8b949e" : "#656d76",
accent: dark ? "#388bfd" : "#0969da",
warning: dark ? "#d29922" : "#9a6700",
danger: dark ? "#f85149" : "#cf222e",
track: dark ? "#21262d" : "#e5e7eb",
}),
[dark]
);
useEffect(() => {
// React runs effects twice in development; importing twice would create
// every note a second time.
if (started.current) return;
started.current = true;
importFiles(files, folderId, setProgress)
.then((r) => {
setResult(r);
onDone?.(r);
})
.catch((e) => setError(e?.message || "Der Import ist fehlgeschlagen."));
}, [files, folderId, onDone]);
const done = result || error;
const pct = progress.total ? Math.round((progress.done / progress.total) * 100) : 0;
return (
<div
style={{
position: "fixed",
inset: 0,
background: "rgba(0,0,0,0.5)",
display: "flex",
alignItems: "center",
justifyContent: "center",
zIndex: 2000,
backdropFilter: "blur(4px)",
}}
onClick={done ? onClose : undefined}
>
<div
onClick={(e) => e.stopPropagation()}
style={{
background: T.bg,
borderRadius: 16,
padding: 24,
width: 440,
maxWidth: "92vw",
border: `1px solid ${T.border}`,
boxShadow: "0 20px 40px rgba(0,0,0,0.3)",
fontFamily: "'DM Sans',sans-serif",
color: T.text,
}}
>
<div
style={{
fontSize: 17,
fontWeight: 600,
marginBottom: 14,
fontFamily: "'Newsreader',Georgia,serif",
}}
>
{done ? "Import abgeschlossen" : "Notizen werden importiert…"}
</div>
{!done && (
<>
<div style={{ height: 6, borderRadius: 3, background: T.track, overflow: "hidden" }}>
<div
style={{
width: `${pct}%`,
height: "100%",
background: T.accent,
transition: "width .2s",
}}
/>
</div>
<div style={{ fontSize: 12, color: T.muted, marginTop: 8 }}>
{progress.total ? `${progress.done} von ${progress.total}` : "Archiv wird gelesen…"}
{progress.label ? ` — ${progress.label}` : ""}
</div>
</>
)}
{error && <div style={{ fontSize: 13, color: T.danger }}>{error}</div>}
{result && (
<div style={{ fontSize: 13, lineHeight: 1.7 }}>
<div>
<strong>{result.notes.length}</strong>{" "}
{result.notes.length === 1 ? "Notiz importiert" : "Notizen importiert"}
</div>
{result.flattenedDrawings > 0 && (
<div style={{ color: T.warning, marginTop: 10, fontSize: 12.5 }}>
{result.flattenedDrawings === 1
? "Eine Zeichnung kam als Bild zurück"
: `${result.flattenedDrawings} Zeichnungen kamen als Bilder zurück`}{" "}
— im Archiv liegen sie als PNG, und aus Pixeln lassen sich die Striche nicht zurückrechnen.
Sie sind sichtbar und exportierbar, aber nicht mehr mit dem Stift zu bearbeiten.
</div>
)}
{result.missingImages > 0 && (
<div style={{ color: T.warning, marginTop: 10, fontSize: 12.5 }}>
{result.missingImages} {result.missingImages === 1 ? "Bild lag" : "Bilder lagen"} nicht im
Archiv. Die Stelle in der Notiz bleibt erhalten, das Bild fehlt.
</div>
)}
{result.failed.length > 0 && (
<div style={{ color: T.danger, marginTop: 10, fontSize: 12.5 }}>
Nicht gelesen: {result.failed.join(", ")}
</div>
)}
</div>
)}
<div style={{ display: "flex", justifyContent: "flex-end", marginTop: 20 }}>
<button
onClick={onClose}
disabled={!done}
style={{
padding: "8px 20px",
borderRadius: 10,
border: "none",
background: done ? T.accent : T.track,
color: done ? "#fff" : T.muted,
cursor: done ? "pointer" : "default",
fontSize: 13,
fontWeight: 600,
}}
>
{done ? "Fertig" : "Läuft…"}
</button>
</div>
</div>
</div>
);
}