feat(workspace): add logseq outliner engine, global todo rollup, task list sharing avatars and admin toggle
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m22s

This commit is contained in:
sandrodach committed 2026-10-25 00:15:43 +02:00
1 parent 2504b48171
commit fe1d574787
9 files changed
+1316 -444

No files matched your search

+29
View File
@@ -49,6 +49,29 @@
- Word-/Notion-artige Formatierungsleiste oben (Fett, Kursiv, Durchgestrichen, Code, Links, Überschriften H1-H3, Checklisten, Aufzählungen, Callouts, Markdown-Tabellen, Fortschrittsbalken). - Word-/Notion-artige Formatierungsleiste oben (Fett, Kursiv, Durchgestrichen, Code, Links, Überschriften H1-H3, Checklisten, Aufzählungen, Callouts, Markdown-Tabellen, Fortschrittsbalken).
- Interne Wiki-Links (`[[Dokumenttitel]]`) mit Klick-Navigation und Auto-Erkennung. - Interne Wiki-Links (`[[Dokumenttitel]]`) mit Klick-Navigation und Auto-Erkennung.
- Physische Speicherung auf Festplatte unter `/data/workspace/<user_id>/...` als Standard-`.md`-Dateien zur einfachen Daten-Extraktion, synchronisiert mit SQLite. - Physische Speicherung auf Festplatte unter `/data/workspace/<user_id>/...` als Standard-`.md`-Dateien zur einfachen Daten-Extraktion, synchronisiert mit SQLite.
- [x] **Workspace Logseq Outliner-Engine, Zoom-Hoisting & Drag & Drop (`WorkspaceView.tsx`, `models.go`, `sqlite.go`):**
- Bullet-Point-basiertes hierarchisches Notizensystem inspiriert von Logseq.
- Jeder Block ist ein Bullet-Point (`•`) mit flexibler Einrückungstiefe (`Level: 0, 1, 2...`).
- Schnelle Tastaturnavigation:
- `Enter`: Neuer Bullet-Point auf gleicher Ebene.
- `Tab`: Block dem vorherigen unterordnen (einrücken).
- `Shift+Tab`: Block ausrücken.
- `Shift+Enter`: Weicher Zeilenumbruch innerhalb desselben Blocks.
- `Backspace`: Auf leerem Block rückt aus oder entfernt den Bullet-Point.
- **Zoom-in Hoisting:** Klick auf den Bullet-Dot zoomt tief in diesen Ast hinein (Fokusmodus) mit Breadcrumbs-Navigation und „Zoom aufheben"-Button.
- **Drag & Drop:** Bullet-Dots dienen als Anfasser zum Verschieben und Umsortieren von Blöcken und ganzen Unterbäumen.
- **Slash-Commands (`/`):** Schnelle Umwandlung in `/todo`, `/h1`, `/h2`, `/h3`, `/code`, `/table`, `/callout`, `/progress`, `/divider`.
- Physische Speicherung auf Festplatte unter `/data/workspace/<user_id>/...` als Standard-`.md`-Dateien mit portabler 2-Leerzeichen-Einrückung (` - [ ] ` bzw. ` - `), vollständig kompatibel mit externen Outlinern und Markdown-Viewern.
- [x] **Globaler Workspace To-Do Rollup Dashboard (`WorkspaceView.tsx`):**
- Aggregierte Übersicht aller To-Dos (`- [ ] ` / `- [x] `) aus sämtlichen Notiz-Dateien über alle Ordner hinweg.
- Übersichtliche Gruppierung nach Ordner ➔ Dokument ➔ Aufgaben mit einklappbaren Akkordeons und Zähler für offene Aufgaben.
- **Ordner-Drilldown:** Klick auf einen Ordner isoliert nur diesen Zweig für fokussiertes Abarbeiten, inklusive Zurücksetzen-Filter.
- **Live 2-Wege-Sync:** Abhaken oder Aktivieren eines To-Dos im Rollup synchronisiert direkt in Echtzeit zurück in die SQLite-Datenbank und die physische `.md`-Datei auf Festplatte.
- [x] **Aufgabenlisten-Teilen & Avatar-Stack (`TasksView.tsx`, `models.go`, `sqlite.go`):**
- Aufgabenlisten können direkt mit Teams/Spaces geteilt werden.
- Avatar-Stack (farbige Initialen-Bubbles mit Hover-Tooltips) im Header der Liste und in den Tabs visualisiert, wem eine Liste gehört und mit welchen Teammitgliedern sie geteilt ist.
- [x] **Admin-Schalter für öffentliche Selbstregistrierung (`AdminSettingsView.tsx`):**
- Unintuitiver Button in den Admin-Einstellungen unter Server & Isolation durch einen flüssig animierten Schiebeschalter (`rounded-pill`) mit eindeutigen Status-Beschriftungen ersetzt.
- [x] **Cross-Browser & Performance Fixes (Chrome & Firefox):** - [x] **Cross-Browser & Performance Fixes (Chrome & Firefox):**
- SQLite Connection-Pool Deadlock durch striktes Schließen von `sql.Rows` vor Unterabfragen behoben. - SQLite Connection-Pool Deadlock durch striktes Schließen von `sql.Rows` vor Unterabfragen behoben.
- `Cache-Control: no-store` auf allen API-Routen und `index.html`. - `Cache-Control: no-store` auf allen API-Routen und `index.html`.
@@ -268,6 +291,12 @@ Verbindungs-URL: `ws(s)://<host>/api/v1/ws/spaces/:spaceId?token=<jwt>`
| `T` | Triage-Deck | Datums-Auswahl für Termin-Aufgabe öffnen | | `T` | Triage-Deck | Datums-Auswahl für Termin-Aufgabe öffnen |
| `D` | Triage-Deck | Aktiven Quick Dump sofort in Workspace-Notiz umwandeln | | `D` | Triage-Deck | Aktiven Quick Dump sofort in Workspace-Notiz umwandeln |
| `Leertaste` | Triage-Deck | Karte überspringen („Später" – ans Ende des Stapels schieben) | | `Leertaste` | Triage-Deck | Karte überspringen („Später" – ans Ende des Stapels schieben) |
| `Enter` | Workspace Outliner | Neuen Bullet-Point auf gleicher Ebene erstellen |
| `Tab` | Workspace Outliner | Bullet-Point einrücken (dem vorherigen unterordnen) |
| `Shift + Tab` | Workspace Outliner | Bullet-Point ausrücken (Ebene verringern) |
| `Shift + Enter` | Workspace Outliner | Weicher Zeilenumbruch innerhalb des aktuellen Blocks |
| `Backspace` | Workspace Outliner | Bei leerem Block: Ebene ausrücken bzw. Bullet löschen |
| `/` | Workspace Outliner | Slash-Command-Menü öffnen (/todo, /h1, /code, etc.) |
--- ---
@@ -1126,33 +1126,43 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
</div> </div>
</div> </div>
{/* Public Registration Toggle */} {/* Public Registration Toggle Switch */}
<div className="p-4 bg-canvas border border-border rounded-card flex items-center justify-between gap-4"> <div className="p-4 bg-canvas border border-border rounded-card flex items-center justify-between gap-4">
<div> <div className="pr-4">
<p className="font-bold text-main">Öffentliche Selbstregistrierung erlauben</p> <p className="font-bold text-main">Öffentliche Selbstregistrierung erlauben</p>
<p className="text-[11px] text-muted mt-0.5"> <p className="text-[11px] text-muted mt-0.5">
Wenn deaktiviert, können sich Fremde auf dem Login-Bildschirm nicht registrieren. Wenn deaktiviert, können sich fremde Personen auf dem Login-Bildschirm nicht selbst registrieren.
Neue Benutzer benötigen zwingend einen Einladungscode oder müssen von einem Admin Neue Konten erfordern dann zwingend einen Einladungscode oder müssen von einem Admin angelegt werden.
angelegt werden.
</p> </p>
</div> </div>
<div className="flex items-center gap-3 flex-shrink-0">
<span className={`text-xs font-medium hidden sm:inline ${settings.allowRegistration ? 'text-accent' : 'text-muted'}`}>
{settings.allowRegistration ? 'Aktiviert (Offen)' : 'Gesperrt (Nur Einladung)'}
</span>
<button <button
type="button" type="button"
role="switch"
aria-checked={settings.allowRegistration}
onClick={() => onClick={() =>
handleSaveSystemSettings({ allowRegistration: !settings.allowRegistration }) handleSaveSystemSettings({ allowRegistration: !settings.allowRegistration })
} }
className={`h-8 px-4 text-xs font-semibold border rounded-btn flex-shrink-0 transition-colors ${ className={`relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-pill border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none ${
settings.allowRegistration settings.allowRegistration ? 'bg-accent' : 'bg-panel-elevated border border-border'
? 'bg-accent/20 border-accent text-accent'
: 'bg-panel border-border text-muted hover:text-main'
}`} }`}
title={settings.allowRegistration ? 'Selbstregistrierung deaktivieren' : 'Selbstregistrierung aktivieren'}
> >
{settings.allowRegistration ? 'Erlaubt (Offen)' : 'Gesperrt (Nur mit Einladung)'} <span
aria-hidden="true"
className={`pointer-events-none inline-block h-5 w-5 transform rounded-pill bg-white shadow-md ring-0 transition duration-200 ease-in-out ${
settings.allowRegistration ? 'translate-x-5' : 'translate-x-0'
}`}
/>
</button> </button>
</div> </div>
</div> </div>
</div> </div>
</div>
)} )}
{/* ================================================================= */} {/* ================================================================= */}
+212 -3
View File
@@ -14,10 +14,20 @@ import {
X, X,
Layers, Layers,
Share2, Share2,
Users,
} from 'lucide-react'; } from 'lucide-react';
import { TaskList, Thought, Space } from '../../lib/types'; import { TaskList, Thought, Space } from '../../lib/types';
import { api, triggerHaptic } from '../../lib/api'; import { api, triggerHaptic } from '../../lib/api';
const getInitials = (name?: string): string => {
if (!name) return '?';
const parts = name.trim().split(/\s+/);
if (parts.length >= 2) {
return (parts[0][0] + parts[1][0]).toUpperCase();
}
return name.slice(0, 2).toUpperCase();
};
interface TasksViewProps { interface TasksViewProps {
onRefreshStats: () => void; onRefreshStats: () => void;
onOpenDocument: (docId: string) => void; onOpenDocument: (docId: string) => void;
@@ -48,6 +58,11 @@ export const TasksView: React.FC<TasksViewProps> = ({
const [newListIcon, setNewListIcon] = useState('📋'); const [newListIcon, setNewListIcon] = useState('📋');
const [newListSpaceId, setNewListSpaceId] = useState(''); const [newListSpaceId, setNewListSpaceId] = useState('');
// Share TaskList modal state
const [showShareListModal, setShowShareListModal] = useState(false);
const [sharingTaskList, setSharingTaskList] = useState<TaskList | null>(null);
const [shareTargetSpaceId, setShareTargetSpaceId] = useState('');
// Active notes inline edit ID // Active notes inline edit ID
const [activeNotesTaskId, setActiveNotesTaskId] = useState<string | null>(null); const [activeNotesTaskId, setActiveNotesTaskId] = useState<string | null>(null);
const [notesDraft, setNotesDraft] = useState(''); const [notesDraft, setNotesDraft] = useState('');
@@ -195,6 +210,30 @@ export const TasksView: React.FC<TasksViewProps> = ({
} }
}; };
const handleOpenShareModal = (tl: TaskList) => {
setSharingTaskList(tl);
setShareTargetSpaceId(tl.spaceId || '');
setShowShareListModal(true);
};
const handleSaveShareList = async () => {
if (!sharingTaskList) return;
triggerHaptic(15);
try {
const updated = await api.updateTaskList(sharingTaskList.id, {
spaceId: shareTargetSpaceId,
});
setTaskLists((prev) =>
prev.map((l) => (l.id === updated.id ? { ...l, ...updated } : l))
);
setShowShareListModal(false);
setSharingTaskList(null);
await loadData();
} catch (err: any) {
alert(err.message || 'Fehler beim Speichern der Freigabe');
}
};
const safeTasks = Array.isArray(tasks) ? tasks : []; const safeTasks = Array.isArray(tasks) ? tasks : [];
const safeTaskLists = Array.isArray(taskLists) ? taskLists : []; const safeTaskLists = Array.isArray(taskLists) ? taskLists : [];
const safeSpaces = Array.isArray(spaces) ? spaces : []; const safeSpaces = Array.isArray(spaces) ? spaces : [];
@@ -245,11 +284,19 @@ export const TasksView: React.FC<TasksViewProps> = ({
{tl.tasksCount !== undefined && tl.tasksCount > 0 && ( {tl.tasksCount !== undefined && tl.tasksCount > 0 && (
<span className="ml-1 badge-neutral">{tl.tasksCount}</span> <span className="ml-1 badge-neutral">{tl.tasksCount}</span>
)} )}
{tl.spaceId && ( {tl.members && tl.members.length > 0 ? (
<div className="flex items-center -space-x-1 ml-0.5" title={`Geteilt (${tl.members.length} Mitglieder)`}>
{tl.members.slice(0, 2).map((m) => (
<span key={m.userId} className="w-3.5 h-3.5 rounded-pill bg-accent/20 border border-panel text-[8px] font-bold text-accent flex items-center justify-center">
{getInitials(m.displayName || m.email)[0]}
</span>
))}
</div>
) : tl.spaceId ? (
<span title="Geteilt mit Team/Space"> <span title="Geteilt mit Team/Space">
<Share2 className="w-2.5 h-2.5 text-accent ml-0.5" /> <Share2 className="w-2.5 h-2.5 text-accent ml-0.5" />
</span> </span>
)} ) : null}
</button> </button>
{selectedListId === tl.id && ( {selectedListId === tl.id && (
<button <button
@@ -273,8 +320,57 @@ export const TasksView: React.FC<TasksViewProps> = ({
</button> </button>
</div> </div>
{/* Right Header Section: Sharing, Avatar Stack & View Switcher */}
<div className="flex items-center gap-2">
{currentSelectedList && (
<div className="flex items-center gap-1.5">
<button
type="button"
onClick={() => handleOpenShareModal(currentSelectedList)}
className="btn-action h-8 px-2.5 flex items-center gap-1.5 text-xs"
title={`Liste '${currentSelectedList.title}' mit Team oder Nutzern teilen`}
>
<Users className="w-3.5 h-3.5 text-accent" />
<span>Teilen</span>
</button>
{/* Avatar Stack in the corner */}
{currentSelectedList.members && currentSelectedList.members.length > 0 ? (
<div
onClick={() => handleOpenShareModal(currentSelectedList)}
className="flex items-center -space-x-1.5 cursor-pointer hover:opacity-90 transition-opacity pl-0.5"
title={`Geteilt mit ${currentSelectedList.members.length} Mitgliedern (${currentSelectedList.spaceName || 'Team'}). Klick zum Verwalten.`}
>
{currentSelectedList.members.slice(0, 3).map((m) => (
<div
key={m.userId}
className="w-7 h-7 rounded-pill border-2 border-panel bg-accent/20 text-accent flex items-center justify-center text-[10px] font-bold shadow-sm"
title={`${m.displayName || m.email} (${m.role})`}
>
{getInitials(m.displayName || m.email)}
</div>
))}
{currentSelectedList.members.length > 3 && (
<div className="w-7 h-7 rounded-pill border-2 border-panel bg-accent text-on-accent flex items-center justify-center text-[10px] font-bold shadow-sm">
+{currentSelectedList.members.length - 3}
</div>
)}
</div>
) : (
<div
onClick={() => handleOpenShareModal(currentSelectedList)}
className="flex items-center cursor-pointer pl-0.5"
title="Privat (nur du). Klick zum Teilen."
>
<div className="w-7 h-7 rounded-pill border border-dashed border-border bg-canvas text-muted hover:text-accent hover:border-accent flex items-center justify-center text-[11px] font-bold transition-colors">
🔒
</div>
</div>
)}
</div>
)}
{/* View Switcher: List vs Kanban */} {/* View Switcher: List vs Kanban */}
<div className="flex items-center gap-1">
<div className="flex border border-border rounded-btn bg-canvas overflow-hidden"> <div className="flex border border-border rounded-btn bg-canvas overflow-hidden">
<button <button
onClick={() => setViewMode('list')} onClick={() => setViewMode('list')}
@@ -415,6 +511,119 @@ export const TasksView: React.FC<TasksViewProps> = ({
</div> </div>
)} )}
{/* SHARE LIST MODAL */}
{showShareListModal && sharingTaskList && (
<div className="fixed inset-0 modal-overlay z-50 flex items-center justify-center p-4">
<div className="w-full max-w-md panel-surface shadow-panel p-4 space-y-4">
<div className="flex items-center justify-between border-b border-border pb-2">
<h3 className="text-sm font-semibold text-main flex items-center gap-1.5">
<Users className="w-4 h-4 text-accent" />
<span>Liste teilen: {sharingTaskList.title}</span>
</h3>
<button
type="button"
onClick={() => {
setShowShareListModal(false);
setSharingTaskList(null);
}}
className="text-muted hover:text-main"
>
<X className="w-4 h-4" />
</button>
</div>
<div className="space-y-3">
<div>
<label className="block text-xs text-muted mb-1 font-medium">
Freigeben für Bereich / Team:
</label>
<select
value={shareTargetSpaceId}
onChange={(e) => setShareTargetSpaceId(e.target.value)}
className="w-full h-8 px-2.5 input-field text-xs bg-panel"
>
<option value="">🔒 Privat (Nicht geteilt - nur für mich)</option>
{safeSpaces.map((s) => (
<option key={s.id} value={s.id}>
{s.icon} {s.name} ({s.members?.length || 1} Mitglieder)
</option>
))}
</select>
</div>
{/* Members preview */}
{(() => {
const selectedSpace = safeSpaces.find((s) => s.id === shareTargetSpaceId);
const members = selectedSpace?.members || [];
if (members.length > 0) {
return (
<div className="space-y-2 pt-2 border-t border-border">
<p className="text-xs font-semibold text-main">
Personen mit Zugriff ({members.length}):
</p>
<div className="max-h-48 overflow-y-auto space-y-1.5 pr-1">
{members.map((m) => (
<div
key={m.userId}
className="p-2 bg-canvas border border-border rounded-card flex items-center justify-between text-xs"
>
<div className="flex items-center gap-2">
<div className="w-6 h-6 rounded-pill bg-accent/20 text-accent font-bold text-[10px] flex items-center justify-center">
{getInitials(m.displayName || m.email)}
</div>
<div>
<p className="font-semibold text-main">{m.displayName || m.email}</p>
<p className="text-[10px] text-muted">{m.email}</p>
</div>
</div>
<span className="badge-neutral text-[10px]">
{m.role === 'owner' ? 'Eigentümer' : m.role === 'editor' ? 'Bearbeiter' : 'Mitglied'}
</span>
</div>
))}
</div>
</div>
);
} else if (shareTargetSpaceId) {
return (
<p className="text-xs text-muted italic">Keine Mitglieder in diesem Bereich.</p>
);
} else {
return (
<div className="p-3 bg-canvas border border-border rounded-card text-xs text-muted space-y-1">
<p className="font-medium text-main">Diese Aufgabenliste ist privat.</p>
<p className="text-[11px] text-faint">
Wähle oben einen Familien- oder Team-Space aus, um die Liste und alle enthaltenen Aufgaben automatisch mit den Mitgliedern zu teilen.
</p>
</div>
);
}
})()}
</div>
<div className="flex justify-end gap-2 pt-2 border-t border-border">
<button
type="button"
onClick={() => {
setShowShareListModal(false);
setSharingTaskList(null);
}}
className="btn-action text-xs"
>
Abbrechen
</button>
<button
type="button"
onClick={handleSaveShareList}
className="btn-primary text-xs"
>
Freigabe speichern
</button>
</div>
</div>
</div>
)}
{/* VIEW MODES: LIST OR KANBAN */} {/* VIEW MODES: LIST OR KANBAN */}
{viewMode === 'kanban' ? ( {viewMode === 'kanban' ? (
/* KANBAN BOARD (3 COLUMNS) */ /* KANBAN BOARD (3 COLUMNS) */
File diff suppressed because it is too large. Load diff
+5
View File
@@ -58,6 +58,9 @@ export interface TaskList {
id: string; id: string;
userId: string; userId: string;
spaceId?: string; spaceId?: string;
spaceName?: string;
ownerName?: string;
members?: SpaceMember[];
title: string; title: string;
icon: string; icon: string;
color: string; color: string;
@@ -113,6 +116,8 @@ export interface DocumentBlock {
content: string; content: string;
checked?: boolean; checked?: boolean;
lang?: string; lang?: string;
level?: number;
collapsed?: boolean;
} }
export interface Document { export interface Document {
+16
View File
@@ -10,3 +10,19 @@ require (
golang.org/x/crypto v0.31.0 golang.org/x/crypto v0.31.0
modernc.org/sqlite v1.34.4 modernc.org/sqlite v1.34.4
) )
require (
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
golang.org/x/sys v0.28.0 // indirect
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect
modernc.org/libc v1.55.3 // indirect
modernc.org/mathutil v1.6.0 // indirect
modernc.org/memory v1.8.0 // indirect
modernc.org/strutil v1.2.0 // indirect
modernc.org/token v1.1.0 // indirect
)
+59
View File
@@ -0,0 +1,59 @@
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/go-chi/chi/v5 v5.2.0 h1:Aj1EtB0qR2Rdo2dG4O94RIU35w2lvQSj6BRA4+qwFL0=
github.com/go-chi/chi/v5 v5.2.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo=
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic=
golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw=
golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc=
modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ=
modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
modernc.org/ccgo/v4 v4.19.2 h1:lwQZgvboKD0jBwdaeVCTouxhxAyN6iawF3STraAal8Y=
modernc.org/ccgo/v4 v4.19.2/go.mod h1:ysS3mxiMV38XGRTTcgo0DQTeTmAO4oCmJl1nX9VFI3s=
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw=
modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI=
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4=
modernc.org/libc v1.55.3 h1:AzcW1mhlPNrRtjS5sS+eW2ISCgSOLLNyFzRh/V3Qj/U=
modernc.org/libc v1.55.3/go.mod h1:qFXepLhz+JjFThQ4kzwzOjA/y/artDeg+pcYnY+Q83w=
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=
modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU=
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss=
modernc.org/sqlite v1.34.4 h1:sjdARozcL5KJBvYQvLlZEmctRgW9xqIZc2ncN7PU0P8=
modernc.org/sqlite v1.34.4/go.mod h1:3QQFCG2SEMtc2nv+Wq4cQCH7Hjcg+p/RMlS1XK+zwbk=
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
+5
View File
@@ -101,6 +101,9 @@ type TaskList struct {
ID string `json:"id"` ID string `json:"id"`
UserID string `json:"userId"` UserID string `json:"userId"`
SpaceID string `json:"spaceId,omitempty"` SpaceID string `json:"spaceId,omitempty"`
SpaceName string `json:"spaceName,omitempty"`
OwnerName string `json:"ownerName,omitempty"`
Members []SpaceMember `json:"members,omitempty"`
Title string `json:"title"` Title string `json:"title"`
Icon string `json:"icon"` Icon string `json:"icon"`
Color string `json:"color"` Color string `json:"color"`
@@ -154,6 +157,8 @@ type DocumentBlock struct {
Content string `json:"content"` Content string `json:"content"`
Checked bool `json:"checked,omitempty"` Checked bool `json:"checked,omitempty"`
Lang string `json:"lang,omitempty"` Lang string `json:"lang,omitempty"`
Level int `json:"level,omitempty"`
Collapsed bool `json:"collapsed,omitempty"`
} }
type Document struct { type Document struct {
+30 -13
View File
@@ -923,33 +923,34 @@ func (s *Store) saveDocumentToDisk(doc *domain.Document) {
md.WriteString(fmt.Sprintf("# %s %s\n\n", doc.Icon, doc.Title)) md.WriteString(fmt.Sprintf("# %s %s\n\n", doc.Icon, doc.Title))
if len(doc.Blocks) > 0 { if len(doc.Blocks) > 0 {
for _, b := range doc.Blocks { for _, b := range doc.Blocks {
indent := strings.Repeat(" ", b.Level)
switch b.Type { switch b.Type {
case "h1": case "h1":
md.WriteString(fmt.Sprintf("# %s\n\n", b.Content)) md.WriteString(fmt.Sprintf("%s- # %s\n", indent, b.Content))
case "h2": case "h2":
md.WriteString(fmt.Sprintf("## %s\n\n", b.Content)) md.WriteString(fmt.Sprintf("%s- ## %s\n", indent, b.Content))
case "h3": case "h3":
md.WriteString(fmt.Sprintf("### %s\n\n", b.Content)) md.WriteString(fmt.Sprintf("%s- ### %s\n", indent, b.Content))
case "todo": case "todo":
chk := " " chk := " "
if b.Checked { if b.Checked {
chk = "x" chk = "x"
} }
md.WriteString(fmt.Sprintf("- [%s] %s\n", chk, b.Content)) md.WriteString(fmt.Sprintf("%s- [%s] %s\n", indent, chk, b.Content))
case "bullet": case "bullet", "paragraph":
md.WriteString(fmt.Sprintf("- %s\n", b.Content)) md.WriteString(fmt.Sprintf("%s- %s\n", indent, b.Content))
case "code": case "code":
md.WriteString(fmt.Sprintf("```%s\n%s\n```\n\n", b.Lang, b.Content)) md.WriteString(fmt.Sprintf("%s- ```%s\n%s %s\n%s ```\n", indent, b.Lang, indent, b.Content, indent))
case "callout": case "callout":
md.WriteString(fmt.Sprintf("> %s\n\n", b.Content)) md.WriteString(fmt.Sprintf("%s- > %s\n", indent, b.Content))
case "divider": case "divider":
md.WriteString("---\n\n") md.WriteString(fmt.Sprintf("%s- ---\n", indent))
case "table": case "table":
md.WriteString(b.Content + "\n\n") md.WriteString(fmt.Sprintf("%s- %s\n", indent, b.Content))
case "progress": case "progress":
md.WriteString(fmt.Sprintf("<!-- progress: %s -->\n\n", b.Content)) md.WriteString(fmt.Sprintf("%s- <!-- progress: %s -->\n", indent, b.Content))
default: default:
md.WriteString(b.Content + "\n\n") md.WriteString(fmt.Sprintf("%s- %s\n", indent, b.Content))
} }
} }
} else if doc.Content != "" { } else if doc.Content != "" {
@@ -1683,10 +1684,18 @@ func (s *Store) GetTaskLists(userID string) ([]domain.TaskList, error) {
_ = rows.Close() _ = rows.Close()
for i := range list { for i := range list {
list[i].Members = make([]domain.SpaceMember, 0)
_ = s.db.QueryRow( _ = s.db.QueryRow(
`SELECT COUNT(*) FROM thoughts WHERE task_list_id = ? AND is_completed = 0 AND (status = 'kept' OR (status = 'converted' AND converted_type = 'task'))`, `SELECT COUNT(*) FROM thoughts WHERE task_list_id = ? AND is_completed = 0 AND (status = 'kept' OR (status = 'converted' AND converted_type = 'task'))`,
list[i].ID, list[i].ID,
).Scan(&list[i].TasksCount) ).Scan(&list[i].TasksCount)
_ = s.db.QueryRow(`SELECT display_name FROM users WHERE id = ?`, list[i].UserID).Scan(&list[i].OwnerName)
if list[i].SpaceID != "" {
_ = s.db.QueryRow(`SELECT name FROM spaces WHERE id = ?`, list[i].SpaceID).Scan(&list[i].SpaceName)
if members, err := s.GetSpaceMembers(list[i].SpaceID); err == nil && members != nil {
list[i].Members = members
}
}
} }
return list, nil return list, nil
} }
@@ -1701,7 +1710,15 @@ func (s *Store) GetTaskListByID(userID, listID string) (*domain.TaskList, error)
if err != nil { if err != nil {
return nil, err return nil, err
} }
tl.Members = make([]domain.SpaceMember, 0)
_ = s.db.QueryRow(`SELECT COUNT(*) FROM thoughts WHERE task_list_id = ? AND is_completed = 0 AND (status = 'kept' OR (status = 'converted' AND converted_type = 'task'))`, tl.ID).Scan(&tl.TasksCount) _ = s.db.QueryRow(`SELECT COUNT(*) FROM thoughts WHERE task_list_id = ? AND is_completed = 0 AND (status = 'kept' OR (status = 'converted' AND converted_type = 'task'))`, tl.ID).Scan(&tl.TasksCount)
_ = s.db.QueryRow(`SELECT display_name FROM users WHERE id = ?`, tl.UserID).Scan(&tl.OwnerName)
if tl.SpaceID != "" {
_ = s.db.QueryRow(`SELECT name FROM spaces WHERE id = ?`, tl.SpaceID).Scan(&tl.SpaceName)
if members, err := s.GetSpaceMembers(tl.SpaceID); err == nil && members != nil {
tl.Members = members
}
}
return &tl, nil return &tl, nil
} }
@@ -1730,7 +1747,7 @@ func (s *Store) UpdateTaskList(userID, listID string, title, icon, color, spaceI
if err != nil { if err != nil {
return nil, err return nil, err
} }
return tl, nil return s.GetTaskListByID(userID, listID)
} }
func (s *Store) DeleteTaskList(userID, listID string) error { func (s *Store) DeleteTaskList(userID, listID string) error {