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
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 59s
This commit is contained in:
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}
|
||||
|
||||
Reference in new issue
Block a user