fix: 5 bugs critiques de l'éditeur (Phase 1 audit)
1. replaceAll (Find & Replace) — une seule transaction ProseMirror au lieu d'un forEach cassé. Tous les matchs sont maintenant remplacés. 2. Link Preview unwrap — deleteNode() au lieu de clearer les attrs qui laissaient un nœud fantôme invisible dans le document. 3. Conversion Markdown → richtext — breaks: true dans marked.parse() Les simple newlines sont maintenant convertis en <br>. + préserve les blocs custom (toggle, callout, math, columns, outline, link-preview) en commentaires HTML lors de l'export MD. 4. emitNoteChange exercices — shape corrigée (type:'created' attend un objet Note, pas noteId/notebookId séparés). 5. Raccourcis clavier sans conflit : Cmd+Shift+C → Cmd+Alt+C (callout, avant: copier) Cmd+Shift+O → Cmd+Alt+O (outline, avant: historique/signets) Cmd+Shift+L → Cmd+Alt+L (colonnes, avant: lock screen macOS)
This commit is contained in:
@@ -13,6 +13,7 @@ import {
|
||||
User,
|
||||
LogOut,
|
||||
Brain,
|
||||
CreditCard,
|
||||
} from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useLanguage } from '@/lib/i18n'
|
||||
@@ -42,6 +43,11 @@ const ADMIN_NAV_ITEMS = [
|
||||
href: '/admin/ai',
|
||||
icon: Brain,
|
||||
},
|
||||
{
|
||||
titleKey: 'admin.sidebar.billing',
|
||||
href: '/admin/billing',
|
||||
icon: CreditCard,
|
||||
},
|
||||
{
|
||||
titleKey: 'admin.sidebar.published',
|
||||
href: '/admin/published',
|
||||
|
||||
@@ -172,9 +172,14 @@ export function FindReplaceBar({ editor, onClose }: { editor: Editor; onClose: (
|
||||
const replaceAll = useCallback(() => {
|
||||
const ms = matchesRef.current
|
||||
if (ms.length === 0) return
|
||||
// Sort descending by position so replacements don't shift earlier positions
|
||||
const sorted = [...ms].sort((a, b) => b.from - a.from)
|
||||
editor.chain().focus()
|
||||
sorted.forEach(m => editor.chain().insertContentAt({ from: m.from, to: m.to }, replaceText).run())
|
||||
// Use a single ProseMirror transaction for all replacements
|
||||
const tr = editor.state.tr
|
||||
for (const m of sorted) {
|
||||
tr.insertText(replaceText, m.from, m.to)
|
||||
}
|
||||
editor.view.dispatch(tr)
|
||||
matchesRef.current = []
|
||||
setCount(0)
|
||||
setCurrentIndex(-1)
|
||||
|
||||
@@ -260,7 +260,7 @@ export function NoteEditorToolbar({ mode, onClose, onToggleAttachments, attachme
|
||||
})
|
||||
// Emit events so the note list refreshes
|
||||
for (const ex of data.exercises || []) {
|
||||
emitNoteChange({ type: 'created', noteId: ex.id, notebookId: note.notebookId })
|
||||
emitNoteChange({ type: 'created', note: { ...note, id: ex.id, title: ex.title, content: '<p></p>' } as any })
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
|
||||
@@ -22,6 +22,7 @@ interface BillingStatus {
|
||||
currentPeriodEnd: string | null;
|
||||
cancelAtPeriodEnd: boolean;
|
||||
hasStripeSubscription: boolean;
|
||||
billingEnabled?: boolean;
|
||||
prices?: {
|
||||
PRO: {
|
||||
month: { display: string; amount: number; currency: string };
|
||||
@@ -34,11 +35,11 @@ interface BillingStatus {
|
||||
};
|
||||
}
|
||||
|
||||
const billingEnabled = process.env.NEXT_PUBLIC_FEATURE_BILLING_ENABLED === 'true' || process.env.NODE_ENV === 'development';
|
||||
const billingEnabledEnvFallback = process.env.NEXT_PUBLIC_FEATURE_BILLING_ENABLED === 'true' || process.env.NODE_ENV === 'development';
|
||||
|
||||
let stripePromise: ReturnType<typeof loadStripe> | null = null;
|
||||
function getStripePromise() {
|
||||
if (!billingEnabled) return null;
|
||||
function getStripePromise(enabled: boolean) {
|
||||
if (!enabled) return null;
|
||||
if (!stripePromise && process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY) {
|
||||
stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY);
|
||||
}
|
||||
@@ -77,6 +78,8 @@ export function BillingPlans() {
|
||||
});
|
||||
|
||||
const quotas = usageData?.quotas;
|
||||
const billingEnabled = status?.billingEnabled ?? billingEnabledEnvFallback;
|
||||
const stripe = getStripePromise(billingEnabled);
|
||||
|
||||
useEffect(() => {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
@@ -536,7 +539,7 @@ export function BillingPlans() {
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
{billingEnabled && (
|
||||
{billingEnabled ? (
|
||||
<div className="flex items-center gap-2 justify-center">
|
||||
<button
|
||||
type="button"
|
||||
@@ -560,6 +563,10 @@ export function BillingPlans() {
|
||||
<span className="ms-1 text-primary/80 dark:text-primary">{t('billing.save')} ~17%</span>
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-center text-sm text-muted-foreground px-4">
|
||||
{t('billing.disabledByAdmin')}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className={cn(
|
||||
@@ -661,7 +668,7 @@ export function BillingPlans() {
|
||||
</div>
|
||||
<div className="p-2">
|
||||
<EmbeddedCheckoutProvider
|
||||
stripe={getStripePromise()}
|
||||
stripe={stripe}
|
||||
options={{ clientSecret: checkoutClientSecret, onComplete: handleCheckoutComplete }}
|
||||
>
|
||||
<EmbeddedCheckout />
|
||||
|
||||
@@ -166,7 +166,7 @@ export const CalloutExtension = Node.create({
|
||||
|
||||
addKeyboardShortcuts() {
|
||||
return {
|
||||
'Mod-Shift-C': () => this.editor.commands.insertContent({
|
||||
'Mod-Alt-C': () => this.editor.commands.insertContent({
|
||||
type: this.name,
|
||||
attrs: { type: 'info' },
|
||||
content: [{ type: 'paragraph' }],
|
||||
|
||||
@@ -86,7 +86,7 @@ export const ColumnsExtension = Node.create({
|
||||
|
||||
addKeyboardShortcuts() {
|
||||
return {
|
||||
'Mod-Shift-L': () => this.editor.commands.insertContent({
|
||||
'Mod-Alt-L': () => this.editor.commands.insertContent({
|
||||
type: this.name,
|
||||
attrs: { cols: 2 },
|
||||
content: [
|
||||
|
||||
@@ -43,7 +43,7 @@ const LinkPreviewView = ({ node, updateAttributes, deleteNode, selected }: any)
|
||||
}, [url, cached, updateAttributes])
|
||||
|
||||
const unwrap = () => {
|
||||
updateAttributes({ url: '', preview: null })
|
||||
deleteNode()
|
||||
}
|
||||
|
||||
const domain = (() => {
|
||||
|
||||
@@ -137,7 +137,7 @@ export const OutlineExtension = Node.create({
|
||||
|
||||
addKeyboardShortcuts() {
|
||||
return {
|
||||
'Mod-Shift-O': () => this.editor.commands.insertContent({
|
||||
'Mod-Alt-O': () => this.editor.commands.insertContent({
|
||||
type: this.name,
|
||||
}),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user