Multiple feature additions and improvements across the application: - NextGen Editor: drag handles, smart paste, block actions - Structured views: Kanban and table layouts for notes - Architectural Grid: new brainstorming/agent interface prototype - Flashcards: SM-2 revision algorithm with AI generation - MCP server: robustness improvements - Graph/PDF chat: fix click propagation and copy behavior - Various UI/UX enhancements and bug fixes Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
118 lines
3.9 KiB
TypeScript
118 lines
3.9 KiB
TypeScript
'use client'
|
|
|
|
import { Node, mergeAttributes } from '@tiptap/core'
|
|
import { ReactNodeViewRenderer, NodeViewWrapper, type NodeViewProps } from '@tiptap/react'
|
|
import { useCallback } from 'react'
|
|
import type { Editor } from '@tiptap/core'
|
|
import type { Node as PMNode } from '@tiptap/pm/model'
|
|
import { DatabaseBlockEditor } from '@/components/database-block-editor'
|
|
import { NOTE_REQUEST_SAVE_EVENT } from '@/lib/note-change-sync'
|
|
import {
|
|
createDefaultDatabaseBlockData,
|
|
parseDatabaseBlockAttrs,
|
|
serializeDatabaseBlockData,
|
|
type DatabaseBlockData,
|
|
} from '@/lib/editor/database-block-types'
|
|
|
|
function DatabaseBlockView({ node, updateAttributes, editor }: NodeViewProps) {
|
|
const data = parseDatabaseBlockAttrs(node.attrs)
|
|
|
|
const requestSave = useCallback(() => {
|
|
const hostNoteId = editor.storage.liveBlock?.hostNoteId
|
|
if (hostNoteId) {
|
|
window.dispatchEvent(new CustomEvent(NOTE_REQUEST_SAVE_EVENT, {
|
|
detail: { noteId: hostNoteId, reason: 'database-block-mutation' },
|
|
}))
|
|
}
|
|
}, [editor])
|
|
|
|
const handleChange = useCallback((next: DatabaseBlockData) => {
|
|
updateAttributes(serializeDatabaseBlockData(next))
|
|
requestSave()
|
|
}, [requestSave, updateAttributes])
|
|
|
|
return (
|
|
<NodeViewWrapper className="database-block-wrapper" data-drag-handle contentEditable={false}>
|
|
<DatabaseBlockEditor data={data} onChange={handleChange} />
|
|
</NodeViewWrapper>
|
|
)
|
|
}
|
|
|
|
export const DatabaseBlockExtension = Node.create({
|
|
name: 'databaseBlock',
|
|
group: 'block',
|
|
atom: true,
|
|
draggable: true,
|
|
selectable: true,
|
|
|
|
addAttributes() {
|
|
return {
|
|
dbId: {
|
|
default: '',
|
|
parseHTML: (element) => element.getAttribute('data-db-id') || '',
|
|
renderHTML: (attributes) => (attributes.dbId ? { 'data-db-id': attributes.dbId } : {}),
|
|
},
|
|
dbView: {
|
|
default: 'table',
|
|
parseHTML: (element) => element.getAttribute('data-db-view') || 'table',
|
|
renderHTML: (attributes) => ({ 'data-db-view': attributes.dbView || 'table' }),
|
|
},
|
|
dbAuthorsJson: {
|
|
default: '[]',
|
|
parseHTML: (element) => element.getAttribute('data-db-authors') || '[]',
|
|
renderHTML: (attributes) => ({ 'data-db-authors': attributes.dbAuthorsJson || '[]' }),
|
|
},
|
|
dbBooksJson: {
|
|
default: '[]',
|
|
parseHTML: (element) => element.getAttribute('data-db-books') || '[]',
|
|
renderHTML: (attributes) => ({ 'data-db-books': attributes.dbBooksJson || '[]' }),
|
|
},
|
|
}
|
|
},
|
|
|
|
parseHTML() {
|
|
return [{ tag: 'div[data-database-block]' }]
|
|
},
|
|
|
|
renderHTML({ HTMLAttributes }) {
|
|
return ['div', mergeAttributes(HTMLAttributes, { 'data-database-block': 'true' })]
|
|
},
|
|
|
|
addNodeView() {
|
|
return ReactNodeViewRenderer(DatabaseBlockView)
|
|
},
|
|
})
|
|
|
|
export function insertDatabaseBlockAtSelection(editor: Editor): boolean {
|
|
const type = editor.schema.nodes.databaseBlock
|
|
if (!type) return false
|
|
|
|
const attrs = serializeDatabaseBlockData(createDefaultDatabaseBlockData())
|
|
const { empty, $from } = editor.state.selection
|
|
const parent = $from.parent
|
|
|
|
if (empty && parent.type.name === 'paragraph' && parent.content.size === 0) {
|
|
const pos = $from.before()
|
|
return editor
|
|
.chain()
|
|
.focus()
|
|
.command(({ tr, dispatch }) => {
|
|
tr.replaceWith(pos, pos + parent.nodeSize, type.create(attrs))
|
|
if (dispatch) dispatch(tr)
|
|
return true
|
|
})
|
|
.run()
|
|
}
|
|
|
|
return editor.chain().focus().insertContent({ type: 'databaseBlock', attrs }).run()
|
|
}
|
|
|
|
export function replaceBlockWithDatabase(editor: Editor, blockPos: number, blockNode: PMNode): void {
|
|
const type = editor.schema.nodes.databaseBlock
|
|
if (!type || blockPos < 0 || !blockNode) return
|
|
const attrs = serializeDatabaseBlockData(createDefaultDatabaseBlockData())
|
|
const dbNode = type.create(attrs)
|
|
editor.view.dispatch(editor.state.tr.replaceWith(blockPos, blockPos + blockNode.nodeSize, dbNode))
|
|
editor.commands.focus()
|
|
}
|