revert: sous-page retirée — doublon avec carnets + wikilinks
Momento a déjà une hiérarchie Carnets → Notes + wikilinks [[note]]. Les sous-pages créent une deuxième hiérarchie conflictuelle.
This commit is contained in:
@@ -33,6 +33,7 @@ import { FindReplaceBar, FindReplaceExtension } from './editor-find-replace-bar'
|
|||||||
import { LinkPreviewExtension, insertLinkPreview, isUrl } from './tiptap-link-preview-extension'
|
import { LinkPreviewExtension, insertLinkPreview, isUrl } from './tiptap-link-preview-extension'
|
||||||
import { MathEquationExtension, InlineMathExtension, insertMathEquation } from './tiptap-math-extension'
|
import { MathEquationExtension, InlineMathExtension, insertMathEquation } from './tiptap-math-extension'
|
||||||
import { ColumnsExtension, ColumnNode, insertColumnsBlock } from './tiptap-columns-extension'
|
import { ColumnsExtension, ColumnNode, insertColumnsBlock } from './tiptap-columns-extension'
|
||||||
|
import { SubPageExtension, insertSubPageBlock } from './tiptap-subpage-extension'
|
||||||
import { RtlPreserveExtension } from './tiptap-rtl-preserve-extension'
|
import { RtlPreserveExtension } from './tiptap-rtl-preserve-extension'
|
||||||
import { ClipArticleExtension } from './tiptap-clip-article-extension'
|
import { ClipArticleExtension } from './tiptap-clip-article-extension'
|
||||||
import { BlockPicker, type BlockSuggestion } from './block-picker'
|
import { BlockPicker, type BlockSuggestion } from './block-picker'
|
||||||
@@ -69,7 +70,7 @@ import {
|
|||||||
FileText, Pilcrow, MessageSquare, AlignLeft, AlignCenter, AlignRight,
|
FileText, Pilcrow, MessageSquare, AlignLeft, AlignCenter, AlignRight,
|
||||||
Superscript as SuperscriptIcon, Subscript as SubscriptIcon, Expand, Plus,
|
Superscript as SuperscriptIcon, Subscript as SubscriptIcon, Expand, Plus,
|
||||||
SpellCheck, Languages, BookOpen, Presentation, BarChart3, Database,
|
SpellCheck, Languages, BookOpen, Presentation, BarChart3, Database,
|
||||||
ChevronsRightLeft, MessageSquareWarning, ListTree, FunctionSquare, Columns3, Loader2
|
ChevronsRightLeft, MessageSquareWarning, ListTree, FunctionSquare, Columns3, Loader2, FileOutput
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
@@ -241,6 +242,10 @@ const slashCommands: SlashItem[] = [
|
|||||||
{
|
{
|
||||||
title: 'Écrire avec l\'IA', description: 'Générer du contenu au curseur', icon: Sparkles, category: 'IA Note', isAi: true, aiOption: 'write', command: () => { }
|
title: 'Écrire avec l\'IA', description: 'Générer du contenu au curseur', icon: Sparkles, category: 'IA Note', isAi: true, aiOption: 'write', command: () => { }
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: 'Sub-page', description: 'Create a linked sub-page', icon: FileOutput, category: 'Basic blocks', shortcut: '/page',
|
||||||
|
command: () => { }
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
async function aiReformulate(text: string, option: string, t: any, language?: string): Promise<string> {
|
async function aiReformulate(text: string, option: string, t: any, language?: string): Promise<string> {
|
||||||
@@ -492,6 +497,7 @@ export const RichTextEditor = forwardRef<RichTextEditorHandle, RichTextEditorPro
|
|||||||
InlineMathExtension,
|
InlineMathExtension,
|
||||||
ColumnsExtension,
|
ColumnsExtension,
|
||||||
ColumnNode,
|
ColumnNode,
|
||||||
|
SubPageExtension,
|
||||||
ClipArticleExtension,
|
ClipArticleExtension,
|
||||||
RtlPreserveExtension,
|
RtlPreserveExtension,
|
||||||
Placeholder.configure({
|
Placeholder.configure({
|
||||||
@@ -1725,6 +1731,7 @@ function SlashCommandMenu({ editor, onInsertImage, onSuggestCharts }: { editor:
|
|||||||
{ ...slashCommands[35], title: t('richTextEditor.slashMath'), description: t('richTextEditor.slashMathDesc'), categoryId: 'text', slashKeywords: ['math', 'maths', 'equation', 'équation', 'formula', 'formule', 'latex', 'katex'] },
|
{ ...slashCommands[35], title: t('richTextEditor.slashMath'), description: t('richTextEditor.slashMathDesc'), categoryId: 'text', slashKeywords: ['math', 'maths', 'equation', 'équation', 'formula', 'formule', 'latex', 'katex'] },
|
||||||
{ ...slashCommands[36], title: t('richTextEditor.slashColumns'), description: t('richTextEditor.slashColumnsDesc'), categoryId: 'text', slashKeywords: ['columns', 'colonnes', 'cols', 'layout', 'mise', 'page', 'cote', 'côte'] },
|
{ ...slashCommands[36], title: t('richTextEditor.slashColumns'), description: t('richTextEditor.slashColumnsDesc'), categoryId: 'text', slashKeywords: ['columns', 'colonnes', 'cols', 'layout', 'mise', 'page', 'cote', 'côte'] },
|
||||||
{ ...slashCommands[37], title: t('richTextEditor.slashAiWriter') || 'Écrire avec l\'IA', description: t('richTextEditor.slashAiWriterDesc') || 'Générer du contenu au curseur', categoryId: 'ai', slashKeywords: ['ecrire', 'écrire', 'write', 'ia', 'ai', 'generer', 'générer', 'rediger', 'rédiger'] },
|
{ ...slashCommands[37], title: t('richTextEditor.slashAiWriter') || 'Écrire avec l\'IA', description: t('richTextEditor.slashAiWriterDesc') || 'Générer du contenu au curseur', categoryId: 'ai', slashKeywords: ['ecrire', 'écrire', 'write', 'ia', 'ai', 'generer', 'générer', 'rediger', 'rédiger'] },
|
||||||
|
{ ...slashCommands[38], title: t('richTextEditor.slashSubPage') || 'Sous-page', description: t('richTextEditor.slashSubPageDesc') || 'Créer une note liée', categoryId: 'text', slashKeywords: ['sub', 'subpage', 'sous-page', 'souspage', 'page', 'lien', 'nested', 'imbriquée'] },
|
||||||
{
|
{
|
||||||
title: t('richTextEditor.slashNoteLink'),
|
title: t('richTextEditor.slashNoteLink'),
|
||||||
description: t('richTextEditor.slashNoteLinkDesc'),
|
description: t('richTextEditor.slashNoteLinkDesc'),
|
||||||
@@ -1836,6 +1843,11 @@ function SlashCommandMenu({ editor, onInsertImage, onSuggestCharts }: { editor:
|
|||||||
if (!insertStructuredViewBlockAtSelection(editor, currentNotebookId)) {
|
if (!insertStructuredViewBlockAtSelection(editor, currentNotebookId)) {
|
||||||
toast.error(t('structuredViewBlock.loadError') || 'Impossible de charger les données structurées.')
|
toast.error(t('structuredViewBlock.loadError') || 'Impossible de charger les données structurées.')
|
||||||
}
|
}
|
||||||
|
} else if (item.title === 'Sub-page') {
|
||||||
|
deleteSlashText(); closeMenu()
|
||||||
|
toast.loading('Création de la sous-page...', { id: 'subpage' })
|
||||||
|
await insertSubPageBlock(editor)
|
||||||
|
toast.success('Sous-page créée !', { id: 'subpage' })
|
||||||
} else {
|
} else {
|
||||||
deleteSlashText(); item.command(editor); closeMenu()
|
deleteSlashText(); item.command(editor); closeMenu()
|
||||||
}
|
}
|
||||||
|
|||||||
122
memento-note/components/tiptap-subpage-extension.tsx
Normal file
122
memento-note/components/tiptap-subpage-extension.tsx
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { Node, mergeAttributes } from '@tiptap/core'
|
||||||
|
import { ReactNodeViewRenderer, NodeViewWrapper } from '@tiptap/react'
|
||||||
|
import { FileText, Trash2, ChevronRight } from 'lucide-react'
|
||||||
|
import { useLanguage } from '@/lib/i18n'
|
||||||
|
import { useRouter } from 'next/navigation'
|
||||||
|
|
||||||
|
const SubPageView = ({ node, deleteNode, selected }: any) => {
|
||||||
|
const { t } = useLanguage()
|
||||||
|
const router = useRouter()
|
||||||
|
const noteId = node.attrs.noteId as string
|
||||||
|
const title = (node.attrs.title as string) || t('notes.untitled') || 'Sans titre'
|
||||||
|
|
||||||
|
const open = () => {
|
||||||
|
if (noteId) router.push(`/home?openNote=${noteId}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NodeViewWrapper className="sub-page-block my-2" dir="auto" data-selected={selected}>
|
||||||
|
<div className="group/sub flex items-center gap-2 rounded-xl border border-border bg-card hover:border-brand-accent/40 hover:shadow-sm transition-all p-3 cursor-pointer" onClick={open}>
|
||||||
|
<div className="w-8 h-8 rounded-lg bg-brand-accent/10 flex items-center justify-center flex-shrink-0">
|
||||||
|
<FileText className="h-4 w-4 text-brand-accent" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="text-sm font-medium truncate">{title}</div>
|
||||||
|
<div className="text-[10px] text-muted-foreground">Sous-page</div>
|
||||||
|
</div>
|
||||||
|
<ChevronRight className="h-4 w-4 text-muted-foreground flex-shrink-0 group-hover/sub:text-brand-accent transition-colors" />
|
||||||
|
<button
|
||||||
|
onClick={(e) => { e.stopPropagation(); deleteNode() }}
|
||||||
|
contentEditable={false}
|
||||||
|
className="p-1 rounded hover:bg-destructive/10 text-muted-foreground hover:text-destructive opacity-0 group-hover/sub:opacity-100 transition-opacity flex-shrink-0"
|
||||||
|
title="Supprimer"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-3.5 w-3.5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</NodeViewWrapper>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SubPageExtension = Node.create({
|
||||||
|
name: 'subPageBlock',
|
||||||
|
group: 'block',
|
||||||
|
atom: true,
|
||||||
|
defining: true,
|
||||||
|
|
||||||
|
addAttributes() {
|
||||||
|
return {
|
||||||
|
noteId: {
|
||||||
|
default: '',
|
||||||
|
parseHTML: (el) => el.getAttribute('data-note-id') || '',
|
||||||
|
renderHTML: (attrs) => ({ 'data-note-id': attrs.noteId }),
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
default: '',
|
||||||
|
parseHTML: (el) => el.getAttribute('data-title') || '',
|
||||||
|
renderHTML: (attrs) => ({ 'data-title': attrs.title }),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
parseHTML() {
|
||||||
|
return [{ tag: 'div[data-type="sub-page-block"]' }]
|
||||||
|
},
|
||||||
|
|
||||||
|
renderHTML({ node, HTMLAttributes }) {
|
||||||
|
return [
|
||||||
|
'div',
|
||||||
|
mergeAttributes(HTMLAttributes, {
|
||||||
|
'data-type': 'sub-page-block',
|
||||||
|
'data-note-id': node.attrs.noteId,
|
||||||
|
'data-title': node.attrs.title,
|
||||||
|
class: 'sub-page-block',
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
addNodeView() {
|
||||||
|
return ReactNodeViewRenderer(SubPageView)
|
||||||
|
},
|
||||||
|
|
||||||
|
addKeyboardShortcuts() {
|
||||||
|
return {
|
||||||
|
'Mod-Shift-P': () => this.editor.commands.insertContent({
|
||||||
|
type: this.name,
|
||||||
|
attrs: { noteId: '', title: '' },
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export async function insertSubPageBlock(editor: any): Promise<void> {
|
||||||
|
const notebookId = (editor.storage as any).structuredViewBlock?.notebookId as string | null
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/notes', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
title: 'Sans titre',
|
||||||
|
content: '<p></p>',
|
||||||
|
notebookId: notebookId || undefined,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
const data = await res.json()
|
||||||
|
|
||||||
|
if (!res.ok || !data.data) return
|
||||||
|
|
||||||
|
const note = data.data
|
||||||
|
editor.chain().focus().insertContent({
|
||||||
|
type: 'subPageBlock',
|
||||||
|
attrs: {
|
||||||
|
noteId: note.id,
|
||||||
|
title: note.title || 'Sans titre',
|
||||||
|
},
|
||||||
|
}).run()
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[SubPage] Failed to create note:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2563,6 +2563,8 @@
|
|||||||
"slashAiWriter": "Write with AI",
|
"slashAiWriter": "Write with AI",
|
||||||
"slashAiWriterDesc": "Generate content at cursor",
|
"slashAiWriterDesc": "Generate content at cursor",
|
||||||
"aiWriterPlaceholder": "Describe what you want to write...",
|
"aiWriterPlaceholder": "Describe what you want to write...",
|
||||||
|
"slashSubPage": "Sub-page",
|
||||||
|
"slashSubPageDesc": "Create a linked note inside this note",
|
||||||
"exercisesLoading": "Generating exercises...",
|
"exercisesLoading": "Generating exercises...",
|
||||||
"exercisesGenerated": "exercises created!",
|
"exercisesGenerated": "exercises created!",
|
||||||
"aiGenerateExercises": "Generate exercises",
|
"aiGenerateExercises": "Generate exercises",
|
||||||
|
|||||||
@@ -2567,6 +2567,8 @@
|
|||||||
"slashAiWriter": "Écrire avec l'IA",
|
"slashAiWriter": "Écrire avec l'IA",
|
||||||
"slashAiWriterDesc": "Générer du contenu au curseur",
|
"slashAiWriterDesc": "Générer du contenu au curseur",
|
||||||
"aiWriterPlaceholder": "Décris ce que tu veux écrire...",
|
"aiWriterPlaceholder": "Décris ce que tu veux écrire...",
|
||||||
|
"slashSubPage": "Sous-page",
|
||||||
|
"slashSubPageDesc": "Créer une note liée dans cette note",
|
||||||
"exercisesLoading": "Génération des exercices...",
|
"exercisesLoading": "Génération des exercices...",
|
||||||
"exercisesGenerated": "exercices créés !",
|
"exercisesGenerated": "exercices créés !",
|
||||||
"aiGenerateExercises": "Générer des exercices",
|
"aiGenerateExercises": "Générer des exercices",
|
||||||
|
|||||||
Reference in New Issue
Block a user