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

@@ -17,6 +17,7 @@ import {
Maximize2,
Link as LinkIcon,
FileText,
Lock,
} from 'lucide-react';
import { DocumentBlock, BlockType } from '../../lib/types';
import { RemoteCursor } from '../../lib/useDocumentSync';
@@ -65,6 +66,10 @@ export interface OutlinerBlockProps {
isDragTarget?: boolean;
isTargeted?: boolean;
registerTextareaRef: (blockId: string, el: HTMLTextAreaElement | null) => void;
isLockedByOther?: boolean;
lockedByName?: string;
lockedByColor?: string;
onBlurBlock?: (blockId: string) => void;
}
export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
@@ -91,6 +96,10 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
isDragTarget,
isTargeted,
registerTextareaRef,
isLockedByOther,
lockedByName,
lockedByColor,
onBlurBlock,
}) => {
const [localContent, setLocalContent] = useState<string>(block.content || '');
const [slashSearch, setSlashSearch] = useState<string | null>(null);
@@ -109,6 +118,8 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
onUpdateBlockRef.current = onUpdateBlock;
const onFocusBlockRef = useRef(onFocusBlock);
onFocusBlockRef.current = onFocusBlock;
const onBlurBlockRef = useRef(onBlurBlock);
onBlurBlockRef.current = onBlurBlock;
const onCreateBlockBelowRef = useRef(onCreateBlockBelow);
onCreateBlockBelowRef.current = onCreateBlockBelow;
const onDeleteBlockRef = useRef(onDeleteBlock);
@@ -318,6 +329,7 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
flushDebounce(localContentRef.current);
}
sendCursorMovedRef.current?.(null);
onBlurBlockRef.current?.(block.id);
};
const handleClick = (e: React.MouseEvent<HTMLTextAreaElement>) => {
@@ -336,7 +348,7 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
const primaryRemoteCursor = activeRemoteCursors.length > 0 ? activeRemoteCursors[0] : null;
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (!canEdit) return;
if (!canEdit || isLockedByOther) return;
// Slash command navigation
if (slashSearch !== null && filteredSlashCommands.length > 0) {
@@ -486,7 +498,7 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
{/* Bullet Controls: Drag Handle, Collapse Toggle, Bullet / Checkbox */}
<div className="flex items-center gap-0.5 pt-1 select-none shrink-0">
{canEdit && (
{canEdit && !isLockedByOther && (
<button
type="button"
draggable
@@ -520,7 +532,7 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
{block.type === 'todo' ? (
<button
type="button"
disabled={!canEdit}
disabled={!canEdit || isLockedByOther}
onClick={() =>
flushDebounce(localContent, { checked: !block.checked })
}
@@ -555,6 +567,16 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
{/* Main Block Content */}
<div className="flex-1 min-w-0 pr-1 relative">
{isLockedByOther && (
<div
className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[10px] font-semibold text-white shadow-xs mb-1 select-none"
style={{ backgroundColor: lockedByColor || '#f59e0b' }}
title={`Wird gerade bearbeitet von ${lockedByName || 'anderer Nutzer'}`}
>
<Lock className="w-2.5 h-2.5" />
<span>{lockedByName || 'Gesperrt'}</span>
</div>
)}
{block.type === 'divider' ? (
<div className="py-2">
<hr className="border-t border-border" />
@@ -578,7 +600,7 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
}}
rows={1}
value={localContent}
readOnly={!canEdit}
readOnly={!canEdit || isLockedByOther}
onFocus={handleFocus}
onBlur={handleBlur}
onClick={handleClick}
@@ -598,7 +620,7 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
}}
rows={1}
value={localContent}
readOnly={!canEdit}
readOnly={!canEdit || isLockedByOther}
onFocus={handleFocus}
onBlur={handleBlur}
onClick={handleClick}
@@ -618,7 +640,7 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
}}
rows={1}
value={localContent}
readOnly={!canEdit}
readOnly={!canEdit || isLockedByOther}
onFocus={handleFocus}
onBlur={handleBlur}
onClick={handleClick}