Dev #2
No files matched your search
@@ -43,6 +43,7 @@ import {
|
|||||||
Lock,
|
Lock,
|
||||||
Unlock,
|
Unlock,
|
||||||
Eye,
|
Eye,
|
||||||
|
Share,
|
||||||
Share2,
|
Share2,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { BlockType, Document, DocumentBlock, DocLockInfo, DocShare, User } from '../../lib/types';
|
import { BlockType, Document, DocumentBlock, DocLockInfo, DocShare, User } from '../../lib/types';
|
||||||
@@ -373,15 +374,28 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
};
|
};
|
||||||
}, [selectedDoc?.id]);
|
}, [selectedDoc?.id]);
|
||||||
|
|
||||||
// Flush pending save before tab closes or unmounts
|
// Flush pending save and release lock before tab closes or unmounts
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleBeforeUnload = () => {
|
const handleBeforeUnload = () => {
|
||||||
flushPendingSave();
|
flushPendingSave();
|
||||||
|
if (currentDocIdRef.current && holdsLockRef.current) {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('mindsift_token') || sessionStorage.getItem('mindsift_token');
|
||||||
|
fetch(`/api/v1/documents/${currentDocIdRef.current}/lock`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
||||||
|
keepalive: true,
|
||||||
|
}).catch(() => {});
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
window.addEventListener('beforeunload', handleBeforeUnload);
|
window.addEventListener('beforeunload', handleBeforeUnload);
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('beforeunload', handleBeforeUnload);
|
window.removeEventListener('beforeunload', handleBeforeUnload);
|
||||||
flushPendingSave();
|
flushPendingSave();
|
||||||
|
if (currentDocIdRef.current && holdsLockRef.current) {
|
||||||
|
releaseMyLock(currentDocIdRef.current);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -399,8 +413,9 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (msg.event === 'DOC_UNLOCKED') {
|
} else if (msg.event === 'DOC_UNLOCKED') {
|
||||||
const payload = msg.payload as { doc_id: string };
|
const payload = msg.payload as { docId?: string; doc_id?: string; id?: string };
|
||||||
if (payload && payload.doc_id === selectedDoc?.id) {
|
const unlockedDocId = payload?.docId || payload?.doc_id || payload?.id;
|
||||||
|
if (unlockedDocId && unlockedDocId === selectedDoc?.id) {
|
||||||
setActiveLock(null);
|
setActiveLock(null);
|
||||||
}
|
}
|
||||||
} else if (msg.event === 'DOC_CREATED') {
|
} else if (msg.event === 'DOC_CREATED') {
|
||||||
@@ -413,11 +428,11 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
const updated = msg.payload as Document;
|
const updated = msg.payload as Document;
|
||||||
if (updated && updated.id) {
|
if (updated && updated.id) {
|
||||||
setDocs((prev) =>
|
setDocs((prev) =>
|
||||||
prev.map((d) => (d.id === updated.id ? { ...d, ...updated } : d))
|
prev.map((d) => (d.id === updated.id ? { ...d, ...updated, isPinned: d.isPinned } : d))
|
||||||
);
|
);
|
||||||
if (selectedDoc?.id === updated.id) {
|
if (selectedDoc?.id === updated.id) {
|
||||||
if (!holdsLockRef.current) {
|
if (!holdsLockRef.current) {
|
||||||
setSelectedDoc((prev) => (prev ? { ...prev, ...updated } : updated));
|
setSelectedDoc((prev) => (prev ? { ...prev, ...updated, isPinned: prev.isPinned } : updated));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onStatsChange();
|
onStatsChange();
|
||||||
@@ -1915,16 +1930,10 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={openShareModal}
|
onClick={openShareModal}
|
||||||
className="btn-icon flex items-center gap-1 px-2 text-xs"
|
className="btn-icon"
|
||||||
title="Notiz teilen und Berechtigungen"
|
title="Notiz teilen und Berechtigungen"
|
||||||
>
|
>
|
||||||
<Share2 className="w-3.5 h-3.5" />
|
<Share className="w-3.5 h-3.5" />
|
||||||
<span className="hidden sm:inline">Teilen</span>
|
|
||||||
{selectedDoc.sharedWith && selectedDoc.sharedWith.length > 0 && (
|
|
||||||
<span className="ml-0.5 px-1 rounded-full bg-accent/20 text-accent text-[10px] font-bold">
|
|
||||||
{selectedDoc.sharedWith.length}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -792,7 +792,9 @@ func (s *Server) handleUpdateDocument(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeError(w, http.StatusBadRequest, err.Error())
|
writeError(w, http.StatusBadRequest, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
s.wsHub.BroadcastDocEvent(docID, "DOC_UPDATED", doc)
|
if req.Title != nil || req.Icon != nil || req.FolderPath != nil || req.Blocks != nil || req.Tags != nil {
|
||||||
|
s.wsHub.BroadcastDocEvent(docID, "DOC_UPDATED", doc)
|
||||||
|
}
|
||||||
writeJSON(w, http.StatusOK, doc)
|
writeJSON(w, http.StatusOK, doc)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -927,7 +929,7 @@ func (s *Server) handleUnlockDoc(w http.ResponseWriter, r *http.Request) {
|
|||||||
docID := chi.URLParam(r, "id")
|
docID := chi.URLParam(r, "id")
|
||||||
released := s.wsHub.ReleaseDocLock(docID, userID)
|
released := s.wsHub.ReleaseDocLock(docID, userID)
|
||||||
if released {
|
if released {
|
||||||
s.wsHub.BroadcastDocEvent(docID, "DOC_UNLOCKED", map[string]string{"docId": docID})
|
s.wsHub.BroadcastDocEvent(docID, "DOC_UNLOCKED", map[string]string{"docId": docID, "doc_id": docID})
|
||||||
}
|
}
|
||||||
writeJSON(w, http.StatusOK, map[string]bool{"unlocked": released})
|
writeJSON(w, http.StatusOK, map[string]bool{"unlocked": released})
|
||||||
}
|
}
|
||||||
|
|||||||
+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_user ON doc_shares(user_id);
|
||||||
CREATE INDEX IF NOT EXISTS idx_doc_shares_doc ON doc_shares(doc_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 (
|
CREATE TABLE IF NOT EXISTS lists (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
space_id TEXT NOT NULL,
|
space_id TEXT NOT NULL,
|
||||||
@@ -1153,6 +1167,9 @@ func (s *Store) CreateDocument(userID, spaceID, title, icon, folderPath string,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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)
|
s.saveDocumentToDisk(doc)
|
||||||
return doc, nil
|
return doc, nil
|
||||||
}
|
}
|
||||||
@@ -1180,12 +1197,16 @@ func scanDocument(rows interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) GetDocuments(userID string) ([]domain.Document, error) {
|
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(
|
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
|
`SELECT d.id, d.user_id, d.space_id, d.title, d.icon, d.content, d.blocks_json, d.tags, d.folder_path,
|
||||||
FROM documents WHERE user_id = ? AND is_archived = 0
|
CASE WHEN p.doc_id IS NOT NULL THEN 1 ELSE 0 END AS is_pinned,
|
||||||
ORDER BY is_pinned DESC, updated_at DESC`,
|
d.is_archived, d.created_at, d.updated_at
|
||||||
userID,
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -1202,16 +1223,19 @@ func (s *Store) GetDocuments(userID string) ([]domain.Document, error) {
|
|||||||
}
|
}
|
||||||
_ = rows.Close()
|
_ = 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(
|
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
|
s.permission, u.display_name
|
||||||
FROM documents d
|
FROM documents d
|
||||||
JOIN doc_shares s ON d.id = s.doc_id
|
JOIN doc_shares s ON d.id = s.doc_id
|
||||||
JOIN users u ON d.user_id = u.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
|
WHERE s.user_id = ? AND d.is_archived = 0
|
||||||
ORDER BY d.updated_at DESC`,
|
ORDER BY is_pinned DESC, d.updated_at DESC`,
|
||||||
userID,
|
userID, userID,
|
||||||
)
|
)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
for sharedRows.Next() {
|
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) {
|
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(
|
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
|
`SELECT d.id, d.user_id, d.space_id, d.title, d.icon, d.content, d.blocks_json, d.tags, d.folder_path,
|
||||||
FROM documents WHERE id = ? AND user_id = ?`,
|
CASE WHEN p.doc_id IS NOT NULL THEN 1 ELSE 0 END AS is_pinned,
|
||||||
docID, userID,
|
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)
|
doc, err := scanDocument(row)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -1268,15 +1296,18 @@ func (s *Store) GetDocumentByID(userID, docID string) (*domain.Document, error)
|
|||||||
return doc, nil
|
return doc, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try shared document
|
// Try shared document (with per-user is_pinned)
|
||||||
sharedRow := s.db.QueryRow(
|
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
|
s.permission, u.display_name
|
||||||
FROM documents d
|
FROM documents d
|
||||||
JOIN doc_shares s ON d.id = s.doc_id
|
JOIN doc_shares s ON d.id = s.doc_id
|
||||||
JOIN users u ON d.user_id = u.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 = ?`,
|
WHERE d.id = ? AND s.user_id = ?`,
|
||||||
docID, userID,
|
userID, docID, userID,
|
||||||
)
|
)
|
||||||
d := &domain.Document{}
|
d := &domain.Document{}
|
||||||
var blocksStr, tagsStr string
|
var blocksStr, tagsStr string
|
||||||
@@ -1311,10 +1342,31 @@ func (s *Store) UpdateDocument(userID, docID string, title, icon, folderPath *st
|
|||||||
return nil, err
|
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)")
|
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 {
|
if title != nil {
|
||||||
doc.Title = strings.TrimSpace(*title)
|
doc.Title = strings.TrimSpace(*title)
|
||||||
}
|
}
|
||||||
@@ -1338,22 +1390,15 @@ func (s *Store) UpdateDocument(userID, docID string, title, icon, folderPath *st
|
|||||||
if tags != nil {
|
if tags != nil {
|
||||||
doc.Tags = *tags
|
doc.Tags = *tags
|
||||||
}
|
}
|
||||||
if isPinned != nil {
|
|
||||||
doc.IsPinned = *isPinned
|
|
||||||
}
|
|
||||||
|
|
||||||
doc.UpdatedAt = domain.NowISO()
|
doc.UpdatedAt = domain.NowISO()
|
||||||
blocksJSON, _ := json.Marshal(doc.Blocks)
|
blocksJSON, _ := json.Marshal(doc.Blocks)
|
||||||
tagsJSON, _ := json.Marshal(doc.Tags)
|
tagsJSON, _ := json.Marshal(doc.Tags)
|
||||||
pinnedInt := 0
|
|
||||||
if doc.IsPinned {
|
|
||||||
pinnedInt = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = s.db.Exec(
|
_, 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 = ?`,
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -1957,9 +2002,13 @@ func (s *Store) SearchAll(userID, query string) ([]domain.Thought, []domain.Docu
|
|||||||
}
|
}
|
||||||
|
|
||||||
dRows, err := s.db.Query(
|
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
|
`SELECT d.id, d.user_id, d.space_id, d.title, d.icon, d.content, d.blocks_json, d.tags, d.folder_path,
|
||||||
FROM documents WHERE user_id = ? AND (LOWER(title) LIKE ? OR LOWER(content) LIKE ?) ORDER BY updated_at DESC LIMIT 20`,
|
CASE WHEN p.doc_id IS NOT NULL THEN 1 ELSE 0 END AS is_pinned,
|
||||||
userID, q, q,
|
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)
|
docs := make([]domain.Document, 0)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
|||||||
@@ -164,6 +164,82 @@ func TestStoreLifecycle(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPerUserDocPinning(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
dbPath := filepath.Join(tmpDir, "pin_test.db")
|
||||||
|
|
||||||
|
store, err := NewStore(dbPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewStore failed: %v", err)
|
||||||
|
}
|
||||||
|
defer store.Close()
|
||||||
|
|
||||||
|
user1, _ := store.CreateUser("user1@example.com", "hash", "User 1", "admin")
|
||||||
|
user2, _ := store.CreateUser("user2@example.com", "hash", "User 2", "member")
|
||||||
|
|
||||||
|
// user1 creates document and pins it
|
||||||
|
doc, err := store.CreateDocument(user1.ID, "", "Doc 1", "📝", "", []domain.DocumentBlock{
|
||||||
|
{ID: "b1", Type: "text", Content: "Hello"},
|
||||||
|
}, []string{"test"}, true)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CreateDocument failed: %v", err)
|
||||||
|
}
|
||||||
|
if !doc.IsPinned {
|
||||||
|
t.Fatalf("expected doc to be pinned for user1")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Share with user2 as read-only
|
||||||
|
_, err = store.CreateDocShare(user1.ID, doc.ID, user2.ID, "read")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CreateDocShare failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// user2 reads document: should NOT be pinned for user2
|
||||||
|
docForUser2, err := store.GetDocumentByID(user2.ID, doc.ID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetDocumentByID user2 failed: %v", err)
|
||||||
|
}
|
||||||
|
if docForUser2.IsPinned {
|
||||||
|
t.Fatalf("expected doc to NOT be pinned for user2 initially")
|
||||||
|
}
|
||||||
|
|
||||||
|
// user2 pins the shared document (even with read-only access)
|
||||||
|
pinTrue := true
|
||||||
|
updatedDoc2, err := store.UpdateDocument(user2.ID, doc.ID, nil, nil, nil, nil, nil, &pinTrue)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("user2 pinning read-only doc failed: %v", err)
|
||||||
|
}
|
||||||
|
if !updatedDoc2.IsPinned {
|
||||||
|
t.Fatalf("expected doc to be pinned for user2")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify user1's doc is still pinned
|
||||||
|
docForUser1, err := store.GetDocumentByID(user1.ID, doc.ID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetDocumentByID user1 failed: %v", err)
|
||||||
|
}
|
||||||
|
if !docForUser1.IsPinned {
|
||||||
|
t.Fatalf("expected doc to still be pinned for user1")
|
||||||
|
}
|
||||||
|
|
||||||
|
// user1 unpins the doc
|
||||||
|
pinFalse := false
|
||||||
|
_, err = store.UpdateDocument(user1.ID, doc.ID, nil, nil, nil, nil, nil, &pinFalse)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("user1 unpinning doc failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// user1 doc is now unpinned, but user2 doc must STILL be pinned!
|
||||||
|
docForUser1, _ = store.GetDocumentByID(user1.ID, doc.ID)
|
||||||
|
if docForUser1.IsPinned {
|
||||||
|
t.Fatalf("expected doc to be unpinned for user1")
|
||||||
|
}
|
||||||
|
docForUser2, _ = store.GetDocumentByID(user2.ID, doc.ID)
|
||||||
|
if !docForUser2.IsPinned {
|
||||||
|
t.Fatalf("expected doc to remain pinned for user2")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestEnsureDemoAccountsAndLoginLookup(t *testing.T) {
|
func TestEnsureDemoAccountsAndLoginLookup(t *testing.T) {
|
||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
dbPath := filepath.Join(tmpDir, "demo_test.db")
|
dbPath := filepath.Join(tmpDir, "demo_test.db")
|
||||||
|
|||||||
+3
-3
@@ -117,7 +117,7 @@ func (h *Hub) Unregister(c *Client) {
|
|||||||
|
|
||||||
// Broadcast unlock events for released locks
|
// Broadcast unlock events for released locks
|
||||||
for _, docID := range releasedDocIDs {
|
for _, docID := range releasedDocIDs {
|
||||||
h.BroadcastToAll("DOC_UNLOCKED", map[string]string{"docId": docID})
|
h.BroadcastToAll("DOC_UNLOCKED", map[string]string{"docId": docID, "doc_id": docID})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,7 +233,7 @@ func (h *Hub) cleanupLocksLoop() {
|
|||||||
h.mu.Unlock()
|
h.mu.Unlock()
|
||||||
|
|
||||||
for _, docID := range expiredDocIDs {
|
for _, docID := range expiredDocIDs {
|
||||||
h.BroadcastToAll("DOC_UNLOCKED", map[string]string{"docId": docID})
|
h.BroadcastToAll("DOC_UNLOCKED", map[string]string{"docId": docID, "doc_id": docID})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -388,7 +388,7 @@ func (c *Client) readPump() {
|
|||||||
c.hub.HeartbeatDocLock(docID, c.userID, 30*time.Second)
|
c.hub.HeartbeatDocLock(docID, c.userID, 30*time.Second)
|
||||||
case "UNLOCK_DOC":
|
case "UNLOCK_DOC":
|
||||||
if c.hub.ReleaseDocLock(docID, c.userID) {
|
if c.hub.ReleaseDocLock(docID, c.userID) {
|
||||||
c.hub.BroadcastDocEvent(docID, "DOC_UNLOCKED", map[string]string{"docId": docID})
|
c.hub.BroadcastDocEvent(docID, "DOC_UNLOCKED", map[string]string{"docId": docID, "doc_id": docID})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user