From e2b9daba754c3e00a1fc5ce381b92b9a9dc86a22 Mon Sep 17 00:00:00 2001 From: Sandro Date: Sun, 1 Nov 2026 16:12:01 +0100 Subject: [PATCH] fix: shift range from a pinned note in its folder; kanban fits the window; list toolbar; quick dump targets only select; aligned bullets; steady search field; browser voice waits 5 s Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 17 ++- README.md | 2 +- frontend/package-lock.json | 4 +- frontend/package.json | 2 +- frontend/src/components/CommandPalette.tsx | 36 ++--- .../features/capture/InstantCaptureBar.tsx | 18 +-- .../__tests__/InstantCaptureBar.test.tsx | 7 +- frontend/src/features/triage/TasksView.tsx | 125 ++++++++++++------ .../features/workspace/BacklinksSection.tsx | 2 +- .../src/features/workspace/DocumentEditor.tsx | 2 +- frontend/src/features/workspace/LinkModal.tsx | 2 +- .../src/features/workspace/MergeDialog.tsx | 2 +- .../src/features/workspace/OutlinerBlock.tsx | 3 +- frontend/src/features/workspace/TrashView.tsx | 2 +- .../src/features/workspace/WorkspaceView.tsx | 58 ++++---- frontend/src/lib/useVoiceInput.ts | 16 ++- internal/api/docsync_test.go | 4 +- internal/api/realtime_test.go | 2 +- internal/storage/docsync_sim_test.go | 2 +- internal/storage/docsync_test.go | 26 ++-- internal/storage/documents.go | 2 +- internal/storage/import_markdown.go | 2 +- internal/storage/restructure.go | 2 +- internal/storage/sqlite_test.go | 4 +- internal/storage/thoughts.go | 2 +- internal/storage/visibility_test.go | 2 +- 26 files changed, 208 insertions(+), 138 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 58e2a50..c7f4e40 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,20 @@ Das Format folgt [Keep a Changelog](https://keepachangelog.com/de/1.1.0/), die V ## [Unreleased] +## [0.44.0] - 2026-11-01 + +### Changed +- Neue Notizen bekommen 🗒️ als Standard-Symbol (vorher 📝 bzw. 📄). +- Aufgaben-Liste: „Gruppe“ und „Sortierung“ stehen direkt über der Liste neben den Reitern „Offen / Erledigt / Papierkorb“; „Erledigte in den Papierkorb“ und „Papierkorb leeren“ sind ein Papierkorb-Symbol rechts daneben. +- Kanban am PC: Das Board ist so hoch wie das Fenster; nur noch die Spalten scrollen, die Seite nicht mehr zusätzlich (wenn alle Spalten in eine Reihe passen). +- Quick Dump: Im aufgeklappten Bereich wählen Ziel und Liste nur aus, gespeichert wird mit Enter bzw. dem Knopf. Der Hinweis „(Klick speichert sofort …)“ entfällt. +- Spracheingabe über den Browser: Ein Tippen auf das Mikrofon hört über kurze Pausen hinweg zu und endet erst nach 5 Sekunden Stille. + +### Fixed +- Workspace: Shift+Klick ab einer angehefteten Notiz, die in ihrem Ordner angeklickt wurde, markierte alles ab der angehefteten Zeile ganz oben statt ab dem Ordner. +- Notizen: Ein Punkt mit Unterpunkten rückte durch den Aufklapp-Pfeil ein Stück nach rechts; jetzt stehen alle Punkte genau untereinander. +- Globale Suche: Das Eingabefeld springt beim Tippen nicht mehr hin und her (Lade-Symbol und Leeren-Knopf teilen sich einen festen Platz). + ## [0.43.0] - 2026-11-01 ### Added @@ -704,7 +718,8 @@ Das Format folgt [Keep a Changelog](https://keepachangelog.com/de/1.1.0/), die V - Geteilte Listen (Einkaufs-, Pack- und Checklisten) mit `#Kategorie`-Gruppierung, Mengenangaben und Echtzeit-Abhaken. - Benutzer-, Gruppen- und Admin-Verwaltung (Ersteinrichtungs-Assistent, Einladungscodes, Profilbilder, persönliche API-Keys & Webhook-Ingest `/api/v1/webhooks/ingest`, JSON-Datenexport) sowie CSS-Theme-Engine mit 4 Themes (`Dark`, `Light`, `High Contrast`, `Soft`) und PWA-Unterstützung. -[Unreleased]: https://git.dsit.me/sandrodach/MindSift/compare/v0.43.0...dev +[Unreleased]: https://git.dsit.me/sandrodach/MindSift/compare/v0.44.0...dev +[0.44.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.43.0...v0.44.0 [0.43.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.42.0...v0.43.0 [0.42.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.41.0...v0.42.0 [0.41.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.40.1...v0.41.0 diff --git a/README.md b/README.md index 9495778..4d252c7 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![Self-Hosted](https://img.shields.io/badge/100%25-Self--Hosted-2563eb?style=flat-square)](#) [![Docker](https://img.shields.io/badge/Deployment-Single%20Container-2496ed?style=flat-square&logo=docker&logoColor=white)](#-schnellstart) [![SQLite](https://img.shields.io/badge/Daten-SQLite-003b57?style=flat-square&logo=sqlite&logoColor=white)](#-deine-daten-gehören-dir) -[![Version](https://img.shields.io/badge/Version-v0.43.0-16a34a?style=flat-square)](CHANGELOG.md) +[![Version](https://img.shields.io/badge/Version-v0.44.0-16a34a?style=flat-square)](CHANGELOG.md) **MindSift** ist eine selbst gehostete Schaltstelle für spontane Gedanken, Aufgaben mit Kanban-Board, hierarchische Outliner-Notizen, die mehrere Personen gleichzeitig bearbeiten können, und gemeinsame Listen. Alles läuft als ein einzelner Container mit eingebetteter SQLite-Datenbank, auf deinem eigenen Server. diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 9f83596..8277360 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "mindsift-frontend", - "version": "0.43.0", + "version": "0.44.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mindsift-frontend", - "version": "0.43.0", + "version": "0.44.0", "license": "Unlicense", "dependencies": { "@fontsource-variable/inter": "5.3.0", diff --git a/frontend/package.json b/frontend/package.json index fbc8bd2..4829917 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "mindsift-frontend", "private": true, - "version": "0.43.0", + "version": "0.44.0", "license": "Unlicense", "type": "module", "scripts": { diff --git a/frontend/src/components/CommandPalette.tsx b/frontend/src/components/CommandPalette.tsx index 1e3d92c..78d3078 100644 --- a/frontend/src/components/CommandPalette.tsx +++ b/frontend/src/components/CommandPalette.tsx @@ -204,21 +204,25 @@ export const CommandPalette: React.FC = ({ placeholder="Globale Suche in Notizen, Aufgaben und Listen…" className="flex-1 bg-transparent border-0 outline-none text-main placeholder-muted text-base sm:text-sm focus:ring-0 p-0" /> - {loading && } - {query && !loading && ( - - )} + {/* One fixed slot for the spinner or the clear button, so the field keeps its width while typing */} + + {loading ? ( + + ) : query ? ( + + ) : null} + - ) : ( - <> - {/* Group By */} -
- Gruppe: - -
- - {/* Sort By */} -
- Sortierung: - -
- )} @@ -2464,6 +2459,7 @@ export const TasksView: React.FC = ({
{kanbanTouch.ghost}
= ({ onDragLeave={(e) => handleKanbanColDragLeave(e, col.id)} onDrop={(e) => handleKanbanDrop(e, col.id, colTasks)} data-kanban-col={col.id} - className={`bg-canvas border border-border flex flex-col min-h-[420px] overflow-hidden transition-all ${ - dragOverKanbanCol === col.id ? 'ring-2 ring-accent/60 bg-accent/5' : '' - } ${blockedForDrag ? 'opacity-40' : ''}`} + style={kanbanColHeight ? { height: kanbanColHeight } : undefined} + className={`bg-canvas border border-border flex flex-col overflow-hidden transition-all ${ + kanbanColHeight ? '' : 'min-h-[420px]' + } ${dragOverKanbanCol === col.id ? 'ring-2 ring-accent/60 bg-accent/5' : ''} ${blockedForDrag ? 'opacity-40' : ''}`} title={blockedForDrag ? 'Diese Spalte gibt es in der Liste der Aufgabe nicht' : undefined} > {/* Column Header */} @@ -2501,7 +2498,7 @@ export const TasksView: React.FC = ({
{/* Tasks List in Column */} -
+
{colTasks.map((task) => { const isDragging = draggingTaskId === task.id; const readOnly = isTaskReadOnly(task); @@ -2748,16 +2745,56 @@ export const TasksView: React.FC = ({
+ {/* Grouping and sorting right above the list, then clearing as a plain trash icon (desktop; phones + use the menu) */} +
+
+ Gruppe: + +
+
+ Sortierung: + +
+
{filter === 'discarded' && taskTrash.some((t) => t.canEdit) && ( - )} {filter === 'done' && doneTasks.length > 0 && !isSelectedListReadOnly && ( - )}
diff --git a/frontend/src/features/workspace/BacklinksSection.tsx b/frontend/src/features/workspace/BacklinksSection.tsx index 611b03b..0bf0ee3 100644 --- a/frontend/src/features/workspace/BacklinksSection.tsx +++ b/frontend/src/features/workspace/BacklinksSection.tsx @@ -84,7 +84,7 @@ export const BacklinksSection: React.FC = ({ docId, title onClick={() => onOpen(l.docId)} className="flex items-center gap-2 min-w-0 max-w-full text-left cursor-pointer group" > - {l.icon || '📝'} + {l.icon || '🗒️'} {l.title || 'Unbenannte Notiz'} diff --git a/frontend/src/features/workspace/DocumentEditor.tsx b/frontend/src/features/workspace/DocumentEditor.tsx index 9ea03c4..12881b2 100644 --- a/frontend/src/features/workspace/DocumentEditor.tsx +++ b/frontend/src/features/workspace/DocumentEditor.tsx @@ -1290,7 +1290,7 @@ export const DocumentEditor: React.FC = ({ {inJournal ? ( ) : ( - doc?.icon || '📝' + doc?.icon || '🗒️' )} {showEmojiPicker && canEdit && ( diff --git a/frontend/src/features/workspace/LinkModal.tsx b/frontend/src/features/workspace/LinkModal.tsx index c0f6c5b..ba5d920 100644 --- a/frontend/src/features/workspace/LinkModal.tsx +++ b/frontend/src/features/workspace/LinkModal.tsx @@ -362,7 +362,7 @@ export const LinkModal: React.FC = ({ className="flex items-center justify-between p-2.5 bg-canvas border border-border rounded-card hover:border-accent/60 transition-colors group cursor-pointer" >
- {d.icon || '📝'} + {d.icon || '🗒️'}
{d.title || 'Unbenannte Notiz'} diff --git a/frontend/src/features/workspace/MergeDialog.tsx b/frontend/src/features/workspace/MergeDialog.tsx index 0ab9765..0f8475b 100644 --- a/frontend/src/features/workspace/MergeDialog.tsx +++ b/frontend/src/features/workspace/MergeDialog.tsx @@ -93,7 +93,7 @@ export const MergeDialog: React.FC = ({ docs, sourceId, target onClick={() => setTarget(d.id)} className="w-full min-h-[44px] sm:min-h-0 px-3 py-2 flex items-center gap-2 text-left text-xs text-main hover:bg-panel-elevated" > - {d.icon || '📝'} + {d.icon || '🗒️'} {d.title || 'Unbenannte Notiz'} {d.folderPath && {d.folderPath}} diff --git a/frontend/src/features/workspace/OutlinerBlock.tsx b/frontend/src/features/workspace/OutlinerBlock.tsx index 068afa5..35ffc6e 100644 --- a/frontend/src/features/workspace/OutlinerBlock.tsx +++ b/frontend/src/features/workspace/OutlinerBlock.tsx @@ -1115,7 +1115,8 @@ export const OutlinerBlock: React.FC = ({ onClick={() => flushDebounce(localContent, { collapsed: !block.collapsed }) } - className="p-0.5 text-muted hover:text-accent transition-[color,opacity] opacity-0 group-hover:opacity-100 focus-visible:opacity-100" + // Same width as the spacer of rows without children (the padding is taken back), so bullets stay aligned + className="p-0.5 -m-0.5 text-muted hover:text-accent transition-[color,opacity] opacity-0 group-hover:opacity-100 focus-visible:opacity-100" title={block.collapsed ? 'Unterpunkte ausklappen' : 'Unterpunkte einklappen'} > {block.collapsed ? ( diff --git a/frontend/src/features/workspace/TrashView.tsx b/frontend/src/features/workspace/TrashView.tsx index d3888d0..377441b 100644 --- a/frontend/src/features/workspace/TrashView.tsx +++ b/frontend/src/features/workspace/TrashView.tsx @@ -88,7 +88,7 @@ export const TrashView: React.FC = ({ items, onOpen, onRestore, className="flex items-center gap-2.5 flex-1 min-w-0 text-left cursor-pointer group" title="Schreibgeschützt ansehen" > - {t.icon || '📝'} + {t.icon || '🗒️'} {t.title || 'Unbenannte Notiz'} diff --git a/frontend/src/features/workspace/WorkspaceView.tsx b/frontend/src/features/workspace/WorkspaceView.tsx index c9f62a4..9c0aa12 100644 --- a/frontend/src/features/workspace/WorkspaceView.tsx +++ b/frontend/src/features/workspace/WorkspaceView.tsx @@ -282,7 +282,7 @@ const FolderTreeItem: React.FC = ({ : 'border-transparent text-main hover:bg-panel-elevated' }`} > - {d.icon || '📝'} + {d.icon || '🗒️'} {d.title || 'Unbenannte Notiz'}
))} @@ -455,6 +455,8 @@ export const WorkspaceView: React.FC = ({ const [draggingDocIds, setDraggingDocIds] = useState([]); const [trashDragOver, setTrashDragOver] = useState(false); const lastMarkedRef = useRef(null); + // The row last clicked: a pinned note has two rows (pinned and in its folder), the range starts at this one + const lastMarkedRowRef = useRef(null); const treeRef = useRef(null); // Merging notes: dialog (picker/confirmation) and a toast with undo afterwards @@ -580,14 +582,14 @@ export const WorkspaceView: React.FC = ({ }, [currentUser, reloadTrash]); const handleCreateDocument = useCallback( - async (folderPath = '', title = 'Neue Notiz', icon = '📝') => { + async (folderPath = '', title = 'Neue Notiz', icon = '🗒️') => { if (isCreatingRef.current) return; isCreatingRef.current = true; triggerHaptic(10); try { const created = await api.createDocument({ title: title.trim() || 'Neue Notiz', - icon: icon.trim() || '📝', + icon: icon.trim() || '🗒️', folderPath, blocks: [ { @@ -624,7 +626,7 @@ export const WorkspaceView: React.FC = ({ // The desktop keeps creating at once. const [newNoteSheet, setNewNoteSheet] = useState<{ folderPath: string } | null>(null); const [newNoteTitle, setNewNoteTitle] = useState(''); - const [newNoteIcon, setNewNoteIcon] = useState('📝'); + const [newNoteIcon, setNewNoteIcon] = useState('🗒️'); const openNewNote = useCallback( (folderPath = '') => { if (window.innerWidth >= 640) { @@ -632,7 +634,7 @@ export const WorkspaceView: React.FC = ({ return; } setNewNoteTitle(''); - setNewNoteIcon('📝'); + setNewNoteIcon('🗒️'); setNewNoteSheet({ folderPath }); }, [handleCreateDocument] @@ -794,6 +796,7 @@ export const WorkspaceView: React.FC = ({ // Ctrl/Cmd+click toggles a note, Shift+click marks the range in the order shown (from the last marked // or the open note); a plain click clears the marks and opens the note const handleDocRowClick = (e: React.MouseEvent, id: string, open: () => void) => { + const row = (e.currentTarget as HTMLElement).closest('[data-doc-row]'); if (e.metaKey || e.ctrlKey) { setMarkedDocIds((prev) => { const next = new Set(prev); @@ -803,23 +806,32 @@ export const WorkspaceView: React.FC = ({ return next; }); lastMarkedRef.current = id; + lastMarkedRowRef.current = row; return; } if (e.shiftKey) { - const order: string[] = []; - treeRef.current?.querySelectorAll('[data-doc-row]').forEach((el) => { - const rowId = el.dataset.docRow; - if (rowId && !order.includes(rowId)) order.push(rowId); - }); - const anchor = lastMarkedRef.current || selectedDocId || id; - const a = order.indexOf(anchor); - const b = order.indexOf(id); - const range = a < 0 || b < 0 ? [id] : order.slice(Math.min(a, b), Math.max(a, b) + 1); + // Rows in the order shown; a pinned note appears twice, so positions count, not IDs + const rows = Array.from(treeRef.current?.querySelectorAll('[data-doc-row]') || []); + const b = row ? rows.indexOf(row) : rows.findIndex((el) => el.dataset.docRow === id); + const anchorId = lastMarkedRef.current || selectedDocId || id; + let a = lastMarkedRowRef.current ? rows.indexOf(lastMarkedRowRef.current) : -1; + if (a < 0 || rows[a].dataset.docRow !== anchorId) { + // The open note without a click here: its row nearest to the clicked one + a = -1; + rows.forEach((el, i) => { + if (el.dataset.docRow === anchorId && (a < 0 || Math.abs(i - b) < Math.abs(a - b))) a = i; + }); + } + const range = + a < 0 || b < 0 + ? [id] + : rows.slice(Math.min(a, b), Math.max(a, b) + 1).map((el) => el.dataset.docRow || '').filter(Boolean); setMarkedDocIds((prev) => new Set([...prev, ...range])); return; } setMarkedDocIds(new Set()); lastMarkedRef.current = id; + lastMarkedRowRef.current = row; open(); }; @@ -1408,7 +1420,7 @@ export const WorkspaceView: React.FC = ({ map.set(d.id, { docId: d.id, title: d.title || 'Unbenannte Notiz', - icon: d.icon || '📝', + icon: d.icon || '🗒️', folderPath: d.folderPath || '', matchedBlocks: matched, }); @@ -1421,7 +1433,7 @@ export const WorkspaceView: React.FC = ({ map.set(dg.docId, { docId: dg.docId, title: dg.title || 'Unbenannte Notiz', - icon: dg.icon || '📝', + icon: dg.icon || '🗒️', folderPath: dg.folderPath || '', matchedBlocks: dg.matchedBlocks || [], }); @@ -1978,7 +1990,7 @@ export const WorkspaceView: React.FC = ({ : 'border-transparent text-main hover:bg-panel-elevated' }`} > - {d.icon || '📝'} + {d.icon || '🗒️'} {d.title || 'Unbenannte Notiz'} {d.sharedWith && d.sharedWith.length > 0 && ( @@ -2049,7 +2061,7 @@ export const WorkspaceView: React.FC = ({ : 'border-transparent text-main hover:bg-panel-elevated' }`} > - {d.icon || '📝'} + {d.icon || '🗒️'} {d.title || 'Unbenannte Notiz'} {d.sharedWith && d.sharedWith.length > 0 && ( @@ -2351,7 +2363,7 @@ export const WorkspaceView: React.FC = ({ className="inline-flex items-center gap-1.5 text-[11px] px-2 py-0.5 rounded-btn bg-panel-elevated text-main truncate max-w-full" title={meta.docs[0].title} > - {meta.docs[0].icon || '📝'} + {meta.docs[0].icon || '🗒️'} {meta.docs[0].title || 'Unbenannte Notiz'}
@@ -2452,7 +2464,7 @@ export const WorkspaceView: React.FC = ({ onClick={() => handleJumpToDocBlock(docGroup.docId)} className="flex items-center gap-2 text-xs font-semibold text-accent hover:underline cursor-pointer" > - {docGroup.icon || '📝'} + {docGroup.icon || '🗒️'} {docGroup.title} {docGroup.folderPath && ( @@ -2702,7 +2714,7 @@ export const WorkspaceView: React.FC = ({ className="inline-flex items-center gap-1 text-[11px] px-2 py-0.5 rounded-btn bg-main/[0.06] text-muted hover:text-main" title={item.doc.title} > - {item.doc.icon || '📝'} + {item.doc.icon || '🗒️'} {item.doc.title} {item.doc.folderPath && ( ({item.doc.folderPath}) @@ -2803,7 +2815,7 @@ export const WorkspaceView: React.FC = ({ ) : ( )} - {doc.icon || '📝'} + {doc.icon || '🗒️'} {doc.title} {docOpenCount} offen @@ -2955,7 +2967,7 @@ export const WorkspaceView: React.FC = ({ }} >
- + (null); const segmentTimerRef = useRef(null); const restartTimerRef = useRef(null); + const silenceTimerRef = useRef(null); const cancelledRef = useRef(false); const stoppedRef = useRef(false); const transcriptRef = useRef(''); @@ -59,6 +62,8 @@ export function useVoiceInput({ onText, onError, continuous = false, live = fals segmentTimerRef.current = null; if (restartTimerRef.current !== null) window.clearTimeout(restartTimerRef.current); restartTimerRef.current = null; + if (silenceTimerRef.current !== null) window.clearTimeout(silenceTimerRef.current); + silenceTimerRef.current = null; }; useEffect(() => { @@ -240,8 +245,17 @@ export function useVoiceInput({ onText, onError, continuous = false, live = fals const recognition = new Recognition(); recognition.lang = 'de-DE'; recognition.interimResults = true; - recognition.continuous = continuous || live; + // A tap (neither hold nor dictation) listens through short pauses and ends after 5 s of silence + const stopOnSilence = !continuous && !live; + recognition.continuous = continuous || live || stopOnSilence; + const armSilenceStop = () => { + if (!stopOnSilence) return; + if (silenceTimerRef.current !== null) window.clearTimeout(silenceTimerRef.current); + silenceTimerRef.current = window.setTimeout(() => void stop(), BROWSER_SILENCE_STOP_MS); + }; + armSilenceStop(); recognition.onresult = (event: any) => { + armSilenceStop(); const parts: string[] = []; for (let i = 0; i < event.results.length; i++) parts.push(event.results[i][0].transcript); const sessionText = joinSpeechResults(parts); diff --git a/internal/api/docsync_test.go b/internal/api/docsync_test.go index 17a5143..034d20d 100644 --- a/internal/api/docsync_test.go +++ b/internal/api/docsync_test.go @@ -38,7 +38,7 @@ func newCollabFixture(t *testing.T) collabFixture { editor, editorTok := newTestUser(t, store, "editor@example.com") viewer, viewerTok := newTestUser(t, store, "viewer@example.com") _, outsiderTok := newTestUser(t, store, "outsider2@example.com") - doc, err := store.CreateDocument(owner.ID, "", "Gemeinsam", "📄", "", []domain.DocumentBlock{{ID: "b1", Type: "bullet", Content: "Erster Punkt"}}, nil, false) + doc, err := store.CreateDocument(owner.ID, "", "Gemeinsam", "🗒️", "", []domain.DocumentBlock{{ID: "b1", Type: "bullet", Content: "Erster Punkt"}}, nil, false) if err != nil { t.Fatalf("create doc: %v", err) } @@ -199,7 +199,7 @@ func itoa(n int64) string { func TestDocSyncAfterCompaction(t *testing.T) { srv, store, ts, owner, ownerTok := startRealtimeServer(t) - doc, err := store.CreateDocument(owner.ID, "", "Alt", "📄", "", []domain.DocumentBlock{ + doc, err := store.CreateDocument(owner.ID, "", "Alt", "🗒️", "", []domain.DocumentBlock{ {ID: "b1", Type: "bullet", Content: "Bleibt"}, {ID: "b2", Type: "bullet", Content: "Weg"}, }, nil, false) if err != nil { diff --git a/internal/api/realtime_test.go b/internal/api/realtime_test.go index 9c89762..5c22817 100644 --- a/internal/api/realtime_test.go +++ b/internal/api/realtime_test.go @@ -310,7 +310,7 @@ func TestWebSocketHeartbeat(t *testing.T) { func TestDocChangesArePushedToTheNoteRoom(t *testing.T) { _, store, ts, owner, ownerToken := startRealtimeServer(t) editor, editorToken := newTestUser(t, store, "push-editor@example.com") - doc, err := store.CreateDocument(owner.ID, "", "Push", "📄", "", []domain.DocumentBlock{{ID: "push_b1", Type: "bullet", Content: "Start"}}, nil, false) + doc, err := store.CreateDocument(owner.ID, "", "Push", "🗒️", "", []domain.DocumentBlock{{ID: "push_b1", Type: "bullet", Content: "Start"}}, nil, false) if err != nil { t.Fatal(err) } diff --git a/internal/storage/docsync_sim_test.go b/internal/storage/docsync_sim_test.go index 6b3c9c9..66765f6 100644 --- a/internal/storage/docsync_sim_test.go +++ b/internal/storage/docsync_sim_test.go @@ -315,7 +315,7 @@ func (w *simWorld) sync(c *simClient, reliable bool) { func runSimulation(t *testing.T, seed int64, steps int) { store, u := newSyncTestStore(t) - doc, err := store.CreateDocument(u.ID, "", "Sim", "📄", "", flatBlocks([3]interface{}{"root", 0, "Start"}), nil, false) + doc, err := store.CreateDocument(u.ID, "", "Sim", "🗒️", "", flatBlocks([3]interface{}{"root", 0, "Start"}), nil, false) if err != nil { t.Fatal(err) } diff --git a/internal/storage/docsync_test.go b/internal/storage/docsync_test.go index 7445151..efc0a05 100644 --- a/internal/storage/docsync_test.go +++ b/internal/storage/docsync_test.go @@ -71,7 +71,7 @@ func TestLegacySaveRoundTripsThroughTheLog(t *testing.T) { [3]interface{}{"d", 1, "Aufgabe 2"}, [3]interface{}{"e", 0, "Anderes"}, ) - doc, err := s.CreateDocument(u.ID, "", "Notiz", "📄", "", initial, nil, false) + doc, err := s.CreateDocument(u.ID, "", "Notiz", "🗒️", "", initial, nil, false) if err != nil { t.Fatal(err) } @@ -111,7 +111,7 @@ func TestLegacySaveRoundTripsThroughTheLog(t *testing.T) { func TestApplyDocOpsIsIdempotent(t *testing.T) { s, u := newSyncTestStore(t) - doc, _ := s.CreateDocument(u.ID, "", "Notiz", "📄", "", flatBlocks([3]interface{}{"a", 0, "Start"}), nil, false) + doc, _ := s.CreateDocument(u.ID, "", "Notiz", "🗒️", "", flatBlocks([3]interface{}{"a", 0, "Start"}), nil, false) a := collab.Actor{UserID: u.ID, ClientID: "tab1", Name: "Owner"} text := "Geändert" op := collab.Op{OpID: "op-1", Kind: collab.OpSetContent, BlockID: "a", BaseVersion: 1, Content: &text} @@ -129,8 +129,8 @@ func TestApplyDocOpsIsIdempotent(t *testing.T) { func TestInsertCannotReuseBlockIDOfAnotherNote(t *testing.T) { s, u := newSyncTestStore(t) - other, _ := s.CreateDocument(u.ID, "", "Andere", "📄", "", flatBlocks([3]interface{}{"shared_id", 0, "Fremd"}), nil, false) - doc, _ := s.CreateDocument(u.ID, "", "Notiz", "📄", "", nil, nil, false) + other, _ := s.CreateDocument(u.ID, "", "Andere", "🗒️", "", flatBlocks([3]interface{}{"shared_id", 0, "Fremd"}), nil, false) + doc, _ := s.CreateDocument(u.ID, "", "Notiz", "🗒️", "", nil, nil, false) a := collab.Actor{UserID: u.ID, ClientID: "tab1"} content, root := "Übernahme", "" res, _, err := s.ApplyDocOps(doc.ID, a, []collab.Op{{OpID: "x", Kind: collab.OpInsert, BlockID: "shared_id", ParentID: &root, OrderKey: "a0", Content: &content}}, nil) @@ -148,7 +148,7 @@ func TestDocRole(t *testing.T) { reader, _ := s.CreateUser("reader@example.com", "hash", "Reader", "member") writer, _ := s.CreateUser("writer@example.com", "hash", "Writer", "member") stranger, _ := s.CreateUser("stranger@example.com", "hash", "Stranger", "member") - doc, _ := s.CreateDocument(owner.ID, "", "Notiz", "📄", "", nil, nil, false) + doc, _ := s.CreateDocument(owner.ID, "", "Notiz", "🗒️", "", nil, nil, false) now := domain.NowISO() if _, err := s.db.Exec(`INSERT INTO doc_shares (id, doc_id, user_id, permission, created_at, created_by) VALUES ('s1', ?, ?, 'read', ?, ?), ('s2', ?, ?, 'write', ?, ?)`, @@ -167,7 +167,7 @@ func TestDocRole(t *testing.T) { func TestBackfillConvertsFlatBlocksToTree(t *testing.T) { s, u := newSyncTestStore(t) - doc, _ := s.CreateDocument(u.ID, "", "Alt", "📄", "", nil, nil, false) + doc, _ := s.CreateDocument(u.ID, "", "Alt", "🗒️", "", nil, nil, false) if _, err := s.db.Exec(`DELETE FROM blocks WHERE document_id = ?`, doc.ID); err != nil { t.Fatal(err) } @@ -186,7 +186,7 @@ func TestBackfillConvertsFlatBlocksToTree(t *testing.T) { func TestTaskLinksFollowTheirBlock(t *testing.T) { s, u := newSyncTestStore(t) - doc, _ := s.CreateDocument(u.ID, "", "Notiz", "📄", "", []domain.DocumentBlock{{ID: "todo1", Type: "todo", Content: "Anrufen"}}, nil, false) + doc, _ := s.CreateDocument(u.ID, "", "Notiz", "🗒️", "", []domain.DocumentBlock{{ID: "todo1", Type: "todo", Content: "Anrufen"}}, nil, false) task, err := s.CreateTaskFromDocBlock(u.ID, doc.ID, "todo1", "Anrufen", false) if err != nil { t.Fatal(err) @@ -225,8 +225,8 @@ func TestFixedBlockIDsWorkInSeveralNotes(t *testing.T) { s, u := newSyncTestStore(t) other, _ := s.CreateUser("second@example.com", "hash", "Second", "member") blocks := flatBlocks([3]interface{}{"blk_1", 0, "Willkommen"}, [3]interface{}{"blk_2", 1, "Unterpunkt"}) - first, _ := s.CreateDocument(u.ID, "", "Start", "📄", "", blocks, nil, false) - second, _ := s.CreateDocument(other.ID, "", "Start", "📄", "", blocks, nil, false) + first, _ := s.CreateDocument(u.ID, "", "Start", "🗒️", "", blocks, nil, false) + second, _ := s.CreateDocument(other.ID, "", "Start", "🗒️", "", blocks, nil, false) a, _ := s.GetDocumentByID(u.ID, first.ID) b, _ := s.GetDocumentByID(other.ID, second.ID) @@ -254,7 +254,7 @@ func backdate(t *testing.T, s *Store, docID string, age time.Duration) { func TestCompactionKeepsStateAndForcesResync(t *testing.T) { s, u := newSyncTestStore(t) - doc, _ := s.CreateDocument(u.ID, "", "Notiz", "📄", "", flatBlocks( + doc, _ := s.CreateDocument(u.ID, "", "Notiz", "🗒️", "", flatBlocks( [3]interface{}{"a", 0, "Bleibt"}, [3]interface{}{"b", 0, "Gelöscht"}, [3]interface{}{"c", 1, "Kind von b"}, @@ -324,8 +324,8 @@ func TestCompactionKeepsStateAndForcesResync(t *testing.T) { func TestCompactionRetentionDependsOnSharing(t *testing.T) { s, u := newSyncTestStore(t) other, _ := s.CreateUser("ben@example.com", "hash", "Ben", "member") - private, _ := s.CreateDocument(u.ID, "", "Privat", "📄", "", flatBlocks([3]interface{}{"p", 0, "x"}), nil, false) - shared, _ := s.CreateDocument(u.ID, "", "Geteilt", "📄", "", flatBlocks([3]interface{}{"s", 0, "x"}), nil, false) + private, _ := s.CreateDocument(u.ID, "", "Privat", "🗒️", "", flatBlocks([3]interface{}{"p", 0, "x"}), nil, false) + shared, _ := s.CreateDocument(u.ID, "", "Geteilt", "🗒️", "", flatBlocks([3]interface{}{"s", 0, "x"}), nil, false) if _, err := s.CreateDocShare(u.ID, shared.ID, other.Email, "write"); err != nil { t.Fatal(err) } @@ -344,7 +344,7 @@ func TestCompactionRetentionDependsOnSharing(t *testing.T) { func TestDeletedAtFollowsDeleteAndRestore(t *testing.T) { s, u := newSyncTestStore(t) - doc, _ := s.CreateDocument(u.ID, "", "Notiz", "📄", "", flatBlocks([3]interface{}{"a", 0, "x"}, [3]interface{}{"b", 0, "y"}), nil, false) + doc, _ := s.CreateDocument(u.ID, "", "Notiz", "🗒️", "", flatBlocks([3]interface{}{"a", 0, "x"}, [3]interface{}{"b", 0, "y"}), nil, false) a := collab.Actor{UserID: u.ID, ClientID: "tab1"} deletedAt := func() string { var v string diff --git a/internal/storage/documents.go b/internal/storage/documents.go index ff8d1e9..9f71566 100644 --- a/internal/storage/documents.go +++ b/internal/storage/documents.go @@ -147,7 +147,7 @@ func (s *Store) CreateDocument(userID, spaceID, title, icon, folderPath string, title = "Unbenannte Notiz" } if icon == "" { - icon = "📄" + icon = "🗒️" } if blocks == nil { blocks = []domain.DocumentBlock{ diff --git a/internal/storage/import_markdown.go b/internal/storage/import_markdown.go index 03202c8..2ae2c2d 100644 --- a/internal/storage/import_markdown.go +++ b/internal/storage/import_markdown.go @@ -536,7 +536,7 @@ func (s *Store) ApplyImport(userID string, plan *ImportPlan) (ImportSummary, err } icon := d.icon if icon == "" { - icon = "📄" + icon = "🗒️" } blocks, files := linkImportMedia(d, plan.Media, nil) doc, err := s.CreateDocument(userID, "", title, icon, d.folder, blocks, nil, false) diff --git a/internal/storage/restructure.go b/internal/storage/restructure.go index 6699b45..6a60a24 100644 --- a/internal/storage/restructure.go +++ b/internal/storage/restructure.go @@ -134,7 +134,7 @@ func (s *Store) ExtractBranch(userID, docID, blockID string, a collab.Actor, lea base, tags := branchTitle(root.Content) title := s.uniqueTitle(userID, base) - created, err := s.CreateDocument(userID, "", title, "📄", source.FolderPath, children, nil, false) + created, err := s.CreateDocument(userID, "", title, "🗒️", source.FolderPath, children, nil, false) if err != nil { return nil, "", err } diff --git a/internal/storage/sqlite_test.go b/internal/storage/sqlite_test.go index 61e3e4d..c0cd790 100644 --- a/internal/storage/sqlite_test.go +++ b/internal/storage/sqlite_test.go @@ -189,7 +189,7 @@ func TestStoreLifecycle(t *testing.T) { if !store.CanEditTaskList(user2.ID, taskList.ID) { t.Fatalf("editor share should allow editing") } - newDoc, err := store.CreateDocument(user.ID, "", "Test Note", "📄", "", []domain.DocumentBlock{ + newDoc, err := store.CreateDocument(user.ID, "", "Test Note", "🗒️", "", []domain.DocumentBlock{ {ID: "blk_1", Type: "bullet", Content: "Geheim", Level: 0}, }, []string{"test"}, false) if err != nil { @@ -393,7 +393,7 @@ func TestFolderManagementAndDeletion(t *testing.T) { } // 2. Create document in subfolder - doc1, err := store.CreateDocument(user.ID, "", "Doc in MindSift", "📄", "Projekte/MindSift", nil, nil, false) + doc1, err := store.CreateDocument(user.ID, "", "Doc in MindSift", "🗒️", "Projekte/MindSift", nil, nil, false) if err != nil { t.Fatalf("CreateDocument failed: %v", err) } diff --git a/internal/storage/thoughts.go b/internal/storage/thoughts.go index b7ed6c5..dcf7645 100644 --- a/internal/storage/thoughts.go +++ b/internal/storage/thoughts.go @@ -383,7 +383,7 @@ func (s *Store) UpdateThoughtTriage(userID, thoughtID, action, convertTo, target Level: 0, }) } - doc, err := s.CreateDocument(userID, "", title, "📄", "", blocks, thought.Tags, false) + doc, err := s.CreateDocument(userID, "", title, "🗒️", "", blocks, thought.Tags, false) if err == nil && doc != nil { convertedRefID = doc.ID // A task stays in its list and becomes the note's first checkbox (same link as a task made diff --git a/internal/storage/visibility_test.go b/internal/storage/visibility_test.go index 5435153..c6471d7 100644 --- a/internal/storage/visibility_test.go +++ b/internal/storage/visibility_test.go @@ -58,7 +58,7 @@ func TestVisibilityFollowsOrganizations(t *testing.T) { } // Sharing: visible people only; an invisible e-mail looks like an unknown one - doc, _ := store.CreateDocument(anna.ID, "", "Einkauf", "📄", "", nil, nil, false) + doc, _ := store.CreateDocument(anna.ID, "", "Einkauf", "🗒️", "", nil, nil, false) if _, err := store.CreateDocShare(anna.ID, doc.ID, ben.ID, "write"); err != nil { t.Fatalf("share with Ben failed: %v", err) }