feat(workspace): per-user note pinning, real-time unlock synchronization, and icon-only share button
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 56s

This commit is contained in:
sandrodach committed 2026-10-25 10:39:52 +01:00
1 parent ec79a2493e
commit af7a9eb200
5 files changed
+183 -47

No files matched your search

+78 -29
View File
@@ -215,6 +215,20 @@ func (s *Store) migrate() error {
CREATE INDEX IF NOT EXISTS idx_doc_shares_user ON doc_shares(user_id);
CREATE INDEX IF NOT EXISTS idx_doc_shares_doc ON doc_shares(doc_id);
CREATE TABLE IF NOT EXISTS doc_pins (
user_id TEXT NOT NULL,
doc_id TEXT NOT NULL,
created_at TEXT NOT NULL,
PRIMARY KEY(user_id, doc_id),
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY(doc_id) REFERENCES documents(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_doc_pins_user ON doc_pins(user_id);
INSERT OR IGNORE INTO doc_pins (user_id, doc_id, created_at)
SELECT user_id, id, updated_at FROM documents WHERE is_pinned = 1;
CREATE TABLE IF NOT EXISTS lists (
id TEXT PRIMARY KEY,
space_id TEXT NOT NULL,
@@ -1153,6 +1167,9 @@ func (s *Store) CreateDocument(userID, spaceID, title, icon, folderPath string,
if err != nil {
return nil, err
}
if isPinned {
_, _ = s.db.Exec(`INSERT OR IGNORE INTO doc_pins (user_id, doc_id, created_at) VALUES (?, ?, ?)`, userID, doc.ID, doc.CreatedAt)
}
s.saveDocumentToDisk(doc)
return doc, nil
}
@@ -1180,12 +1197,16 @@ func scanDocument(rows interface {
}
func (s *Store) GetDocuments(userID string) ([]domain.Document, error) {
// 1. Fetch user's own documents
// 1. Fetch user's own documents (with per-user is_pinned from doc_pins)
rows, err := s.db.Query(
`SELECT id, user_id, space_id, title, icon, content, blocks_json, tags, folder_path, is_pinned, is_archived, created_at, updated_at
FROM documents WHERE user_id = ? AND is_archived = 0
ORDER BY is_pinned DESC, updated_at DESC`,
userID,
`SELECT d.id, d.user_id, d.space_id, d.title, d.icon, d.content, d.blocks_json, d.tags, d.folder_path,
CASE WHEN p.doc_id IS NOT NULL THEN 1 ELSE 0 END AS is_pinned,
d.is_archived, d.created_at, d.updated_at
FROM documents d
LEFT JOIN doc_pins p ON p.doc_id = d.id AND p.user_id = ?
WHERE d.user_id = ? AND d.is_archived = 0
ORDER BY is_pinned DESC, d.updated_at DESC`,
userID, userID,
)
if err != nil {
return nil, err
@@ -1202,16 +1223,19 @@ func (s *Store) GetDocuments(userID string) ([]domain.Document, error) {
}
_ = rows.Close()
// 2. Fetch documents shared with the user
// 2. Fetch documents shared with the user (with per-user is_pinned from doc_pins)
sharedRows, err := s.db.Query(
`SELECT d.id, d.user_id, d.space_id, d.title, d.icon, d.content, d.blocks_json, d.tags, d.folder_path, d.is_pinned, d.is_archived, d.created_at, d.updated_at,
`SELECT d.id, d.user_id, d.space_id, d.title, d.icon, d.content, d.blocks_json, d.tags, d.folder_path,
CASE WHEN p.doc_id IS NOT NULL THEN 1 ELSE 0 END AS is_pinned,
d.is_archived, d.created_at, d.updated_at,
s.permission, u.display_name
FROM documents d
JOIN doc_shares s ON d.id = s.doc_id
JOIN users u ON d.user_id = u.id
LEFT JOIN doc_pins p ON p.doc_id = d.id AND p.user_id = ?
WHERE s.user_id = ? AND d.is_archived = 0
ORDER BY d.updated_at DESC`,
userID,
ORDER BY is_pinned DESC, d.updated_at DESC`,
userID, userID,
)
if err == nil {
for sharedRows.Next() {
@@ -1252,11 +1276,15 @@ func (s *Store) GetDocuments(userID string) ([]domain.Document, error) {
}
func (s *Store) GetDocumentByID(userID, docID string) (*domain.Document, error) {
// Try owned document first
// Try owned document first (with per-user is_pinned)
row := s.db.QueryRow(
`SELECT id, user_id, space_id, title, icon, content, blocks_json, tags, folder_path, is_pinned, is_archived, created_at, updated_at
FROM documents WHERE id = ? AND user_id = ?`,
docID, userID,
`SELECT d.id, d.user_id, d.space_id, d.title, d.icon, d.content, d.blocks_json, d.tags, d.folder_path,
CASE WHEN p.doc_id IS NOT NULL THEN 1 ELSE 0 END AS is_pinned,
d.is_archived, d.created_at, d.updated_at
FROM documents d
LEFT JOIN doc_pins p ON p.doc_id = d.id AND p.user_id = ?
WHERE d.id = ? AND d.user_id = ?`,
userID, docID, userID,
)
doc, err := scanDocument(row)
if err == nil {
@@ -1268,15 +1296,18 @@ func (s *Store) GetDocumentByID(userID, docID string) (*domain.Document, error)
return doc, nil
}
// Try shared document
// Try shared document (with per-user is_pinned)
sharedRow := s.db.QueryRow(
`SELECT d.id, d.user_id, d.space_id, d.title, d.icon, d.content, d.blocks_json, d.tags, d.folder_path, d.is_pinned, d.is_archived, d.created_at, d.updated_at,
`SELECT d.id, d.user_id, d.space_id, d.title, d.icon, d.content, d.blocks_json, d.tags, d.folder_path,
CASE WHEN p.doc_id IS NOT NULL THEN 1 ELSE 0 END AS is_pinned,
d.is_archived, d.created_at, d.updated_at,
s.permission, u.display_name
FROM documents d
JOIN doc_shares s ON d.id = s.doc_id
JOIN users u ON d.user_id = u.id
LEFT JOIN doc_pins p ON p.doc_id = d.id AND p.user_id = ?
WHERE d.id = ? AND s.user_id = ?`,
docID, userID,
userID, docID, userID,
)
d := &domain.Document{}
var blocksStr, tagsStr string
@@ -1311,10 +1342,31 @@ func (s *Store) UpdateDocument(userID, docID string, title, icon, folderPath *st
return nil, err
}
if !doc.IsOwner && doc.Permission != "write" {
isOnlyPinChange := isPinned != nil && title == nil && icon == nil && folderPath == nil && blocks == nil && tags == nil
if !doc.IsOwner && doc.Permission != "write" && !isOnlyPinChange {
return nil, fmt.Errorf("keine Schreibberechtigung für dieses Dokument (nur Lesezugriff)")
}
if isPinned != nil {
doc.IsPinned = *isPinned
if *isPinned {
_, _ = s.db.Exec(`INSERT OR IGNORE INTO doc_pins (user_id, doc_id, created_at) VALUES (?, ?, ?)`, userID, docID, domain.NowISO())
if doc.IsOwner {
_, _ = s.db.Exec(`UPDATE documents SET is_pinned = 1 WHERE id = ?`, docID)
}
} else {
_, _ = s.db.Exec(`DELETE FROM doc_pins WHERE user_id = ? AND doc_id = ?`, userID, docID)
if doc.IsOwner {
_, _ = s.db.Exec(`UPDATE documents SET is_pinned = 0 WHERE id = ?`, docID)
}
}
}
if isOnlyPinChange {
return doc, nil
}
if title != nil {
doc.Title = strings.TrimSpace(*title)
}
@@ -1338,22 +1390,15 @@ func (s *Store) UpdateDocument(userID, docID string, title, icon, folderPath *st
if tags != nil {
doc.Tags = *tags
}
if isPinned != nil {
doc.IsPinned = *isPinned
}
doc.UpdatedAt = domain.NowISO()
blocksJSON, _ := json.Marshal(doc.Blocks)
tagsJSON, _ := json.Marshal(doc.Tags)
pinnedInt := 0
if doc.IsPinned {
pinnedInt = 1
}
_, err = s.db.Exec(
`UPDATE documents SET title = ?, icon = ?, content = ?, blocks_json = ?, tags = ?, folder_path = ?, is_pinned = ?, updated_at = ?
`UPDATE documents SET title = ?, icon = ?, content = ?, blocks_json = ?, tags = ?, folder_path = ?, updated_at = ?
WHERE id = ?`,
doc.Title, doc.Icon, doc.Content, string(blocksJSON), string(tagsJSON), doc.FolderPath, pinnedInt, doc.UpdatedAt, docID,
doc.Title, doc.Icon, doc.Content, string(blocksJSON), string(tagsJSON), doc.FolderPath, doc.UpdatedAt, docID,
)
if err != nil {
return nil, err
@@ -1957,9 +2002,13 @@ func (s *Store) SearchAll(userID, query string) ([]domain.Thought, []domain.Docu
}
dRows, err := s.db.Query(
`SELECT id, user_id, space_id, title, icon, content, blocks_json, tags, folder_path, is_pinned, is_archived, created_at, updated_at
FROM documents WHERE user_id = ? AND (LOWER(title) LIKE ? OR LOWER(content) LIKE ?) ORDER BY updated_at DESC LIMIT 20`,
userID, q, q,
`SELECT d.id, d.user_id, d.space_id, d.title, d.icon, d.content, d.blocks_json, d.tags, d.folder_path,
CASE WHEN p.doc_id IS NOT NULL THEN 1 ELSE 0 END AS is_pinned,
d.is_archived, d.created_at, d.updated_at
FROM documents d
LEFT JOIN doc_pins p ON p.doc_id = d.id AND p.user_id = ?
WHERE d.user_id = ? AND (LOWER(d.title) LIKE ? OR LOWER(d.content) LIKE ?) ORDER BY is_pinned DESC, d.updated_at DESC LIMIT 20`,
userID, userID, q, q,
)
docs := make([]domain.Document, 0)
if err == nil {