revert: sous-page retirée — doublon avec carnets + wikilinks
All checks were successful
CI / Lint, Unit Tests & Build (push) Successful in 5m0s
CI / Deploy production (on server) (push) Successful in 1m4s

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:
Antigravity
2026-06-19 20:42:09 +00:00
parent c21cbf84a1
commit 2ec2654282
4 changed files with 139 additions and 1 deletions

View File

@@ -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()
} }

View 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)
}
}

View File

@@ -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",

View File

@@ -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",