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
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 56s
This commit is contained in:
1 parent
ec79a2493e
commit
af7a9eb200
5 files changed
+183
-47
No files matched your search
+78
-29
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user