feat(collab): redesign note collaboration with REST auto-save, opt-in live session, and block locking
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 59s

This commit is contained in:
sandrodach committed 2026-10-27 14:02:38 +01:00
1 parent 50b017f387
commit 0e06cc5577
11 files changed
+1373 -239

No files matched your search

@@ -1,6 +1,6 @@
import React, { useState, useRef, useEffect, useCallback, useMemo } from 'react';
import { Plus } from 'lucide-react';
import { DocumentBlock } from '../../lib/types';
import { DocumentBlock, BlockLockInfo } from '../../lib/types';
import { RemoteCursor } from '../../lib/useDocumentSync';
import { OutlinerBlock } from './OutlinerBlock';
@@ -19,6 +19,10 @@ export interface OutlinerTreeProps {
onTagClick?: (tag: string) => void;
onWikiLinkClick?: (pageTitle: string) => void;
initialFocusedBlockId?: string | null;
activeBlockLocks?: Record<string, BlockLockInfo>;
onFocusBlock?: (blockId: string) => void;
onBlurBlock?: (blockId: string) => void;
currentUserId?: string;
}
export const OutlinerTree: React.FC<OutlinerTreeProps> = ({
@@ -36,6 +40,10 @@ export const OutlinerTree: React.FC<OutlinerTreeProps> = ({
onTagClick,
onWikiLinkClick,
initialFocusedBlockId,
activeBlockLocks,
onFocusBlock,
onBlurBlock,
currentUserId,
}) => {
const [focusedBlockId, setFocusedBlockId] = useState<string | null>(null);
const [highlightedBlockId, setHighlightedBlockId] = useState<string | null>(
@@ -319,35 +327,49 @@ export const OutlinerTree: React.FC<OutlinerTreeProps> = ({
return (
<div className="space-y-0.5">
{visibleEntries.map(({ block, index, hasChildren, maxAllowedLevel }) => (
<OutlinerBlock
key={block.id}
block={block}
index={index}
maxAllowedLevel={maxAllowedLevel}
hasChildren={hasChildren}
isFocused={focusedBlockId === block.id}
canEdit={canEdit}
levelOffset={levelOffset}
onZoomBlock={onZoomBlock}
activeRemoteCursors={cursorsByBlock[block.id] || []}
sendCursorMoved={sendCursorMoved}
onFocusBlock={setFocusedBlockId}
onUpdateBlock={onUpdateBlock}
onCreateBlockBelow={handleCreateBlockBelow}
onDeleteBlock={handleDeleteBlock}
onIndentBlock={handleIndentBlock}
onNavigateFocus={handleNavigateFocus}
onTagClick={onTagClick}
onWikiLinkClick={onWikiLinkClick}
onDragStartBlock={handleDragStartBlock}
onDragOverBlock={handleDragOverBlock}
onDropBlock={handleDropBlock}
isDragTarget={dragOverIndex === index && draggedIndex !== index}
isTargeted={highlightedBlockId === block.id}
registerTextareaRef={registerTextareaRef}
/>
))}
{visibleEntries.map(({ block, index, hasChildren, maxAllowedLevel }) => {
const lockInfo = activeBlockLocks?.[block.id];
const isLockedByOther = Boolean(lockInfo && lockInfo.userId !== currentUserId);
return (
<OutlinerBlock
key={block.id}
block={block}
index={index}
maxAllowedLevel={maxAllowedLevel}
hasChildren={hasChildren}
isFocused={focusedBlockId === block.id}
canEdit={canEdit}
levelOffset={levelOffset}
onZoomBlock={onZoomBlock}
activeRemoteCursors={cursorsByBlock[block.id] || []}
sendCursorMoved={sendCursorMoved}
onFocusBlock={(bId) => {
setFocusedBlockId(bId);
onFocusBlock?.(bId);
}}
onBlurBlock={(bId) => {
onBlurBlock?.(bId);
}}
onUpdateBlock={onUpdateBlock}
onCreateBlockBelow={handleCreateBlockBelow}
onDeleteBlock={handleDeleteBlock}
onIndentBlock={handleIndentBlock}
onNavigateFocus={handleNavigateFocus}
onTagClick={onTagClick}
onWikiLinkClick={onWikiLinkClick}
onDragStartBlock={handleDragStartBlock}
onDragOverBlock={handleDragOverBlock}
onDropBlock={handleDropBlock}
isDragTarget={dragOverIndex === index && draggedIndex !== index}
isTargeted={highlightedBlockId === block.id}
registerTextareaRef={registerTextareaRef}
isLockedByOther={isLockedByOther}
lockedByName={lockInfo?.userName}
lockedByColor={lockInfo?.userColor}
/>
);
})}
{canEdit && (
<div className="pt-2 pl-2">