Bug #10 (LOW): TipTap editor text input leaks into Search field. The EditorContent wrapper had no tabIndex so the global keyboard shortcut (which treats inputs/textareas/buttons as focused but not contenteditable divs) sent typed text to the search field instead of the editor. Adding tabIndex={0} makes the wrapper focusable; TipTap's contentEditable=true then routes the keyboard events to the editor. Bug #1 (LOW): Task List view infinite toast loop. The catch block fired toast.error on every realtime-triggered fetch failure with no dedup, causing infinite toast spam on 429 or persistent errors. Realtime subscription also called fetchTasks() on every event with no debounce, amplifying the problem. Fixes: - Add lastErrorRef to track the last error message; only toast when the message class changes. - Distinguish 429/rate-limited from generic 500 in the toast text. - Reset lastErrorRef on successful fetch. - Debounce the realtime-triggered refetch by 750ms so event bursts collapse to a single fetch. Bug #8 (LOW): TaskCard in Board view not keyboard-focusable. The TaskCard in tasks-kanban-view.tsx already has role=button, tabIndex={0}, onKeyDown for Enter/Space, and aria-label. No change needed; verified in the tree that the fix is present (probably landed as part of an earlier leaf integration). Bug #1 + Bug #8 + Bug #10 all addressed in this commit. Note: Bug #9 (Search returns No results) is fixed by P0 (the search route was patched to use resolveActiveDomain). Verified working without further changes needed.
164 lines
4.2 KiB
TypeScript
164 lines
4.2 KiB
TypeScript
'use client';
|
|
|
|
import { useEditor, EditorContent } from '@tiptap/react';
|
|
import StarterKit from '@tiptap/starter-kit';
|
|
import Link from '@tiptap/extension-link';
|
|
import Placeholder from '@tiptap/extension-placeholder';
|
|
import { useEffect } from 'react';
|
|
import { Button } from '@/components/ui/button';
|
|
import {
|
|
Bold,
|
|
Italic,
|
|
Strikethrough,
|
|
Code,
|
|
List,
|
|
ListOrdered,
|
|
Quote,
|
|
Undo,
|
|
Redo,
|
|
} from 'lucide-react';
|
|
import { cn } from '@/lib/utils';
|
|
|
|
interface NoteEditorProps {
|
|
content: string;
|
|
onChange: (content: string) => void;
|
|
onBlur?: () => void;
|
|
}
|
|
|
|
export function NoteEditor({ content, onChange, onBlur }: NoteEditorProps) {
|
|
const editor = useEditor({
|
|
extensions: [
|
|
StarterKit,
|
|
Link.configure({
|
|
openOnClick: false,
|
|
}),
|
|
Placeholder.configure({
|
|
placeholder: 'Start writing... Use [[Note Title]] to link to other notes',
|
|
}),
|
|
],
|
|
content,
|
|
onUpdate: ({ editor: e }) => {
|
|
onChange(e.getHTML());
|
|
},
|
|
onBlur: () => {
|
|
onBlur?.();
|
|
},
|
|
});
|
|
|
|
useEffect(() => {
|
|
if (editor && content !== editor.getHTML()) {
|
|
editor.commands.setContent(content);
|
|
}
|
|
}, [content, editor]);
|
|
|
|
if (!editor) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<div className="flex h-full flex-col">
|
|
{/* Toolbar */}
|
|
<div className="mb-4 flex flex-wrap gap-1 border-b pb-2">
|
|
<ToolbarButton
|
|
onClick={() => editor.chain().focus().toggleBold().run()}
|
|
active={editor.isActive('bold')}
|
|
label="Bold"
|
|
>
|
|
<Bold className="h-4 w-4" />
|
|
</ToolbarButton>
|
|
<ToolbarButton
|
|
onClick={() => editor.chain().focus().toggleItalic().run()}
|
|
active={editor.isActive('italic')}
|
|
label="Italic"
|
|
>
|
|
<Italic className="h-4 w-4" />
|
|
</ToolbarButton>
|
|
<ToolbarButton
|
|
onClick={() => editor.chain().focus().toggleStrike().run()}
|
|
active={editor.isActive('strike')}
|
|
label="Strikethrough"
|
|
>
|
|
<Strikethrough className="h-4 w-4" />
|
|
</ToolbarButton>
|
|
<ToolbarButton
|
|
onClick={() => editor.chain().focus().toggleCode().run()}
|
|
active={editor.isActive('code')}
|
|
label="Code"
|
|
>
|
|
<Code className="h-4 w-4" />
|
|
</ToolbarButton>
|
|
<div className="mx-1 w-px bg-border" />
|
|
<ToolbarButton
|
|
onClick={() => editor.chain().focus().toggleBulletList().run()}
|
|
active={editor.isActive('bulletList')}
|
|
label="Bullet list"
|
|
>
|
|
<List className="h-4 w-4" />
|
|
</ToolbarButton>
|
|
<ToolbarButton
|
|
onClick={() => editor.chain().focus().toggleOrderedList().run()}
|
|
active={editor.isActive('orderedList')}
|
|
label="Ordered list"
|
|
>
|
|
<ListOrdered className="h-4 w-4" />
|
|
</ToolbarButton>
|
|
<ToolbarButton
|
|
onClick={() => editor.chain().focus().toggleBlockquote().run()}
|
|
active={editor.isActive('blockquote')}
|
|
label="Blockquote"
|
|
>
|
|
<Quote className="h-4 w-4" />
|
|
</ToolbarButton>
|
|
<div className="mx-1 w-px bg-border" />
|
|
<ToolbarButton
|
|
onClick={() => editor.chain().focus().undo().run()}
|
|
disabled={!editor.can().undo()}
|
|
label="Undo"
|
|
>
|
|
<Undo className="h-4 w-4" />
|
|
</ToolbarButton>
|
|
<ToolbarButton
|
|
onClick={() => editor.chain().focus().redo().run()}
|
|
disabled={!editor.can().redo()}
|
|
label="Redo"
|
|
>
|
|
<Redo className="h-4 w-4" />
|
|
</ToolbarButton>
|
|
</div>
|
|
|
|
{/* Editor content */}
|
|
<div className="flex-1 overflow-auto">
|
|
<EditorContent editor={editor} className="tiptap-editor min-h-[400px]" tabIndex={0} />
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function ToolbarButton({
|
|
onClick,
|
|
active,
|
|
disabled,
|
|
label,
|
|
children,
|
|
}: {
|
|
onClick: () => void;
|
|
active?: boolean;
|
|
disabled?: boolean;
|
|
label: string;
|
|
children: React.ReactNode;
|
|
}) {
|
|
return (
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className={cn('h-8 w-8', active && 'bg-accent text-accent-foreground')}
|
|
onClick={onClick}
|
|
disabled={disabled}
|
|
aria-label={label}
|
|
aria-pressed={active}
|
|
>
|
|
{children}
|
|
</Button>
|
|
);
|
|
}
|