refactor(lesson-preparation): 主编辑器三栏布局(结构树 + 纸 + 详情面板)
- 移除 NodeEditor/NodeEditPanel 导入和 JSX - 改为 StructureTree + PaperEditor + DetailPanel 三栏布局 - 新增 AnchorMigrationBanner - 移除 aiContentGenerator prop(V4 DetailPanel 自带 AI 按钮) - 删除 node-edit-panel.tsx(不再被引用) - 删除 ai-content-generator-slot.tsx(依赖 node-edit-panel)
This commit is contained in:
@@ -1,27 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { AiLessonContentGenerator } from "@/modules/ai/components/ai-lesson-content-generator";
|
||||
import { useAiClientOptional } from "@/modules/ai/context/ai-client-provider";
|
||||
import type { AiContentGeneratorSlotProps } from "@/modules/lesson-preparation/components/node-edit-panel";
|
||||
|
||||
/**
|
||||
* P0-11 修复:AI 内容生成器 slot 实现。
|
||||
* 此组件在 app 层组合 @/modules/ai 的具体实现,
|
||||
* 通过 props 注入到 lesson-preparation 模块的 NodeEditPanel,
|
||||
* 避免备课模块直接依赖 @/modules/ai。
|
||||
*/
|
||||
export function AiContentGeneratorSlot({
|
||||
topic,
|
||||
textbookId,
|
||||
chapterId,
|
||||
}: AiContentGeneratorSlotProps) {
|
||||
const aiClient = useAiClientOptional();
|
||||
if (!aiClient) return null;
|
||||
return (
|
||||
<AiLessonContentGenerator
|
||||
topic={topic}
|
||||
textbookId={textbookId}
|
||||
chapterId={chapterId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -11,8 +11,6 @@ import { Permissions } from "@/shared/types/permissions"
|
||||
import { Skeleton } from "@/shared/components/ui/skeleton"
|
||||
import { AiClientProvider } from "@/modules/ai/context/ai-client-provider"
|
||||
import { createCoreAiClientService } from "@/modules/ai/context/create-ai-client-service"
|
||||
// P0-11 修复:通过 slot 组件注入 AI 功能,避免备课模块直接 import @/modules/ai
|
||||
import { AiContentGeneratorSlot } from "./ai-content-generator-slot"
|
||||
|
||||
export const dynamic = "force-dynamic"
|
||||
|
||||
@@ -70,7 +68,6 @@ export default async function EditLessonPlanPage({
|
||||
textbookTitle={textbookTitle}
|
||||
chapterTitle={chapterTitle}
|
||||
classes={classes}
|
||||
aiContentGenerator={AiContentGeneratorSlot}
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
|
||||
@@ -3,15 +3,17 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useLessonPlanEditor } from "../hooks/use-lesson-plan-editor";
|
||||
import { NodeEditor } from "./node-editor";
|
||||
import { NodeEditPanel, type AiContentGeneratorSlot } from "./node-edit-panel";
|
||||
import { StructureTree } from "./structure-tree/structure-tree";
|
||||
import { PaperEditor } from "./paper-editor/paper-editor";
|
||||
import { DetailPanel } from "./detail-panel/detail-panel";
|
||||
import { AnchorMigrationBanner } from "./anchor-migration-banner";
|
||||
import { VersionHistoryDrawer } from "./version-history-drawer";
|
||||
import { LessonPlanErrorBoundary } from "./lesson-plan-error-boundary";
|
||||
import {
|
||||
useLessonPlanContextSafe,
|
||||
useLessonPlanTrackerSafe,
|
||||
} from "../providers/lesson-plan-provider";
|
||||
import type { BlockType, LessonPlanStatus } from "../types";
|
||||
import type { LessonPlanStatus } from "../types";
|
||||
import { Button } from "@/shared/components/ui/button";
|
||||
import {
|
||||
AlertDialog,
|
||||
@@ -24,7 +26,7 @@ import {
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "@/shared/components/ui/alert-dialog";
|
||||
import { Plus, Save, History, Book, FileText, Send, Undo2, RotateCw, WifiOff, Printer, Undo, Redo, CalendarClock, ClipboardCheck, Sparkles, Layers } from "lucide-react";
|
||||
import { Save, History, Book, FileText, Send, Undo2, RotateCw, WifiOff, Printer, Undo, Redo, CalendarClock, ClipboardCheck, Sparkles, Layers } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { PrintView } from "./print-view";
|
||||
import { ScheduleDialog } from "./schedule-dialog";
|
||||
@@ -43,25 +45,8 @@ interface Props {
|
||||
textbookTitle?: string;
|
||||
chapterTitle?: string;
|
||||
classes?: { id: string; name: string }[];
|
||||
/** AI 内容生成器(可选,通过 props 注入避免模块耦合,P0-11 修复)*/
|
||||
aiContentGenerator?: AiContentGeneratorSlot;
|
||||
}
|
||||
|
||||
const BLOCK_TYPES_TO_ADD: BlockType[] = [
|
||||
"objective",
|
||||
"key_point",
|
||||
"import",
|
||||
"new_teaching",
|
||||
"consolidation",
|
||||
"summary",
|
||||
"homework",
|
||||
"blackboard",
|
||||
"exercise",
|
||||
"text_study",
|
||||
"rich_text",
|
||||
"reflection",
|
||||
];
|
||||
|
||||
export function LessonPlanEditor({
|
||||
planId,
|
||||
initialTitle,
|
||||
@@ -72,7 +57,6 @@ export function LessonPlanEditor({
|
||||
textbookTitle,
|
||||
chapterTitle,
|
||||
classes,
|
||||
aiContentGenerator,
|
||||
}: Props) {
|
||||
const t = useTranslations("lessonPreparation");
|
||||
const editor = useLessonPlanEditor();
|
||||
@@ -80,7 +64,6 @@ export function LessonPlanEditor({
|
||||
const ctx = useLessonPlanContextSafe();
|
||||
const service = ctx?.service ?? null;
|
||||
const [showVersions, setShowVersions] = useState(false);
|
||||
const [showAddMenu, setShowAddMenu] = useState(false);
|
||||
const [showPrint, setShowPrint] = useState(false); // V5-4:打印视图
|
||||
const [showSchedule, setShowSchedule] = useState(false); // V5-7:安排课时对话框
|
||||
const [showConsistency, setShowConsistency] = useState(false); // V5-19:一致性校验对话框
|
||||
@@ -90,7 +73,6 @@ export function LessonPlanEditor({
|
||||
const [publishing, setPublishing] = useState(false);
|
||||
const autoSaveTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const versionTimer = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
const addMenuRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// 初始化:仅在 planId 变化时 hydrate(修复 P1-3)
|
||||
const initKey = planId;
|
||||
@@ -261,18 +243,6 @@ export function LessonPlanEditor({
|
||||
return () => window.removeEventListener("beforeunload", handleBeforeUnload);
|
||||
}, []);
|
||||
|
||||
// 添加节点菜单点击外部关闭(P3-2)
|
||||
useEffect(() => {
|
||||
if (!showAddMenu) return;
|
||||
function handleClickOutside(e: MouseEvent) {
|
||||
if (addMenuRef.current && !addMenuRef.current.contains(e.target as Node)) {
|
||||
setShowAddMenu(false);
|
||||
}
|
||||
}
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
return () => document.removeEventListener("mousedown", handleClickOutside);
|
||||
}, [showAddMenu]);
|
||||
|
||||
const handleManualSave = useCallback(async () => {
|
||||
if (!service) return;
|
||||
const state = useLessonPlanEditor.getState();
|
||||
@@ -524,53 +494,24 @@ export function LessonPlanEditor({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 主区域:画布 + 侧边面板 */}
|
||||
<div className="flex-1 flex overflow-hidden">
|
||||
{/* 节点画布 */}
|
||||
<div className="flex-1 relative">
|
||||
<LessonPlanErrorBoundary>
|
||||
<NodeEditor />
|
||||
</LessonPlanErrorBoundary>
|
||||
{/* 添加节点浮动按钮 */}
|
||||
<div className="absolute bottom-4 left-4 z-10" ref={addMenuRef}>
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={() => setShowAddMenu(!showAddMenu)}
|
||||
>
|
||||
<Plus className="w-4 h-4 mr-1" /> {t("action.addNode")}
|
||||
</Button>
|
||||
{showAddMenu && (
|
||||
<div className="absolute bottom-12 left-0 bg-surface border border-outline-variant rounded-lg shadow-lg p-2 grid grid-cols-2 gap-1 w-72 max-h-[60vh] overflow-y-auto">
|
||||
{BLOCK_TYPES_TO_ADD.map((blockType) => (
|
||||
<button
|
||||
key={blockType}
|
||||
onClick={() => {
|
||||
editor.addNode(blockType, undefined, t(`blockType.${blockType}`));
|
||||
setShowAddMenu(false);
|
||||
}}
|
||||
className="text-left px-2 py-1 text-sm hover:bg-surface-container-highest rounded"
|
||||
>
|
||||
{t(`blockType.${blockType}`)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{/* V4:v3 锚点失效提示 banner */}
|
||||
<AnchorMigrationBanner />
|
||||
|
||||
{/* 侧边内容编辑面板:直接用 selectedNodeId 控制显示(修复 P1-2) */}
|
||||
{editor.selectedNodeId && (
|
||||
<div className="w-[420px] flex-shrink-0">
|
||||
<NodeEditPanel
|
||||
planId={planId}
|
||||
textbookId={textbookId}
|
||||
chapterId={chapterId}
|
||||
classes={classes}
|
||||
aiContentGenerator={aiContentGenerator}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* V4:三栏布局(结构树 + 纸区 + 详情面板) */}
|
||||
<LessonPlanErrorBoundary>
|
||||
<main
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "260px 1fr 380px",
|
||||
height: "calc(100vh - 52px)",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<StructureTree />
|
||||
<PaperEditor />
|
||||
<DetailPanel />
|
||||
</main>
|
||||
</LessonPlanErrorBoundary>
|
||||
|
||||
<LessonPlanErrorBoundary>
|
||||
<VersionHistoryDrawer
|
||||
|
||||
@@ -1,304 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useState, type ComponentType } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Sparkles, ChevronDown, ChevronUp } from "lucide-react";
|
||||
import { useLessonPlanEditor } from "../hooks/use-lesson-plan-editor";
|
||||
import { BlockRenderer, BLOCK_REGISTRY } from "../config/block-registry";
|
||||
import { LessonPlanErrorBoundary } from "./lesson-plan-error-boundary";
|
||||
import { Button } from "@/shared/components/ui/button";
|
||||
import { Trash2, X } from "lucide-react";
|
||||
import { getNodeColor } from "../lib/node-summary";
|
||||
import type { TeachingStage, DifferentiationLevel } from "../types";
|
||||
import { TEACHING_STAGE_KEYS, DIFFERENTIATION_LEVEL_KEYS } from "../types";
|
||||
|
||||
/**
|
||||
* P0-11 修复:AI 内容生成器 slot 类型。
|
||||
* 通过 props 注入 AI 组件,避免直接 import @/modules/ai。
|
||||
* 调用方(teacher edit 页面)提供具体实现。
|
||||
*/
|
||||
export interface AiContentGeneratorSlotProps {
|
||||
topic: string;
|
||||
textbookId?: string;
|
||||
chapterId?: string;
|
||||
}
|
||||
|
||||
export type AiContentGeneratorSlot = ComponentType<AiContentGeneratorSlotProps>;
|
||||
|
||||
interface Props {
|
||||
textbookId?: string;
|
||||
chapterId?: string;
|
||||
classes?: { id: string; name: string }[];
|
||||
/** V5-5:当前课案 ID(透传给 RichTextBlock 用于素材库 picker) */
|
||||
planId?: string;
|
||||
/** AI 内容生成器(可选,通过 props 注入避免模块耦合)*/
|
||||
aiContentGenerator?: AiContentGeneratorSlot;
|
||||
}
|
||||
|
||||
export function NodeEditPanel({ textbookId, chapterId, classes, planId, aiContentGenerator }: Props) {
|
||||
const t = useTranslations("lessonPreparation");
|
||||
const tAi = useTranslations("ai");
|
||||
const { doc, selectedNodeId, updateNode, removeNode, selectNode, removeAnchor } =
|
||||
useLessonPlanEditor();
|
||||
const [showAiPanel, setShowAiPanel] = useState(false);
|
||||
|
||||
const node = doc.nodes.find((n) => n.id === selectedNodeId);
|
||||
|
||||
if (!node) {
|
||||
return (
|
||||
<div className="h-full flex items-center justify-center text-on-surface-variant text-sm p-4">
|
||||
{t("editor.selectNodeHint")}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// P2-1:正文节点显示操作提示 + 锚点列表(而非误导性的"内容为空")
|
||||
if (node.type === "textbook_content") {
|
||||
// 收集所有锚点,并关联到对应的教学节点
|
||||
const anchorsWithNode = doc.anchors
|
||||
.map((a) => {
|
||||
const linkedNode = doc.nodes.find((n) => n.id === a.nodeId);
|
||||
return { anchor: a, nodeTitle: linkedNode?.title ?? "?", nodeType: linkedNode?.type ?? "rich_text" };
|
||||
})
|
||||
.sort((a, b) => a.anchor.start - b.anchor.start);
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col border-l border-outline-variant bg-surface">
|
||||
<div className="flex items-center gap-2 px-4 py-2 border-b border-outline-variant">
|
||||
<span className="flex-1 font-title-md text-title-md">
|
||||
{t("editor.textbookContent")}
|
||||
</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => selectNode(null)}
|
||||
aria-label={t("action.close")}
|
||||
>
|
||||
<X className="w-4 h-4" aria-hidden="true" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto p-4 space-y-4">
|
||||
{/* 操作提示 */}
|
||||
<div className="rounded-md border border-outline-variant bg-surface-container-low p-3 text-sm text-on-surface-variant">
|
||||
{t("editor.textbookOperateHint")}
|
||||
</div>
|
||||
{/* 锚点列表 */}
|
||||
<div>
|
||||
<div className="text-xs font-medium text-on-surface-variant mb-2">
|
||||
{t("editor.anchorListTitle")}({anchorsWithNode.length})
|
||||
</div>
|
||||
{anchorsWithNode.length === 0 ? (
|
||||
<p className="text-sm text-on-surface-variant italic">
|
||||
{t("editor.anchorListEmpty")}
|
||||
</p>
|
||||
) : (
|
||||
<ul className="space-y-1">
|
||||
{anchorsWithNode.map(({ anchor, nodeTitle, nodeType }) => (
|
||||
<li
|
||||
key={anchor.id}
|
||||
className="flex items-center gap-2 px-2 py-1.5 rounded border border-outline-variant bg-surface-container-lowest text-sm"
|
||||
>
|
||||
<span
|
||||
className="inline-block w-2 h-2 rounded-full flex-shrink-0"
|
||||
style={{ backgroundColor: getNodeColor(nodeType) }}
|
||||
/>
|
||||
<span className="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium bg-secondary text-secondary-foreground flex-shrink-0">
|
||||
{anchor.type === "range"
|
||||
? t("editor.anchorRangeLabel")
|
||||
: t("editor.anchorPointLabel")}
|
||||
</span>
|
||||
<span className="truncate flex-1" title={nodeTitle}>
|
||||
{nodeTitle}
|
||||
</span>
|
||||
{anchor.textPreview && (
|
||||
<span className="truncate text-xs text-on-surface-variant max-w-[120px]" title={anchor.textPreview}>
|
||||
“{anchor.textPreview}”
|
||||
</span>
|
||||
)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="!p-1 !h-6 !w-6 text-on-surface-variant hover:text-error"
|
||||
onClick={() => removeAnchor(anchor.id)}
|
||||
aria-label={t("action.delete")}
|
||||
>
|
||||
<Trash2 className="w-3 h-3" />
|
||||
</Button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 教学节点:textbook_content 分支已上方 return,此处 TypeScript 已收窄为 LessonPlanNode
|
||||
const lessonNode = node;
|
||||
|
||||
// 从节点标题提取主题用于 AI 内容生成
|
||||
const aiTopic = lessonNode.title || t("editor.textbookContent");
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col border-l border-outline-variant bg-surface">
|
||||
{/* 面板头部 */}
|
||||
<div className="flex items-center gap-2 px-4 py-2 border-b border-outline-variant">
|
||||
<input
|
||||
value={lessonNode.title}
|
||||
onChange={(e) => updateNode(lessonNode.id, { title: e.target.value })}
|
||||
className="flex-1 bg-transparent font-title-md text-title-md focus:outline-none"
|
||||
aria-label={lessonNode.title}
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => selectNode(null)}
|
||||
aria-label={t("action.close")}
|
||||
>
|
||||
<X className="w-4 h-4" aria-hidden="true" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* V5-15 T1 + V5-18 W6:节点属性条(教学阶段 + 差异化标记) */}
|
||||
<div className="flex items-center gap-2 px-4 py-2 border-b border-outline-variant bg-surface-container-low">
|
||||
{/* 教学阶段 */}
|
||||
<label className="text-xs text-on-surface-variant flex items-center gap-1">
|
||||
{t("editor.stageLabel")}
|
||||
</label>
|
||||
<select
|
||||
value={lessonNode.stage ?? ""}
|
||||
onChange={(e) => {
|
||||
const v = e.target.value;
|
||||
updateNode(lessonNode.id, {
|
||||
stage: v === "" ? undefined : (v as TeachingStage),
|
||||
});
|
||||
}}
|
||||
className="text-xs border border-outline-variant rounded px-1.5 py-0.5 bg-surface"
|
||||
aria-label={t("editor.stageLabel")}
|
||||
>
|
||||
<option value="">{t("editor.stageNone")}</option>
|
||||
{TEACHING_STAGE_KEYS.map((s) => (
|
||||
<option key={s} value={s}>
|
||||
{t(`editor.stage.${s}`)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
{/* 差异化标记 */}
|
||||
<label className="text-xs text-on-surface-variant flex items-center gap-1 ml-2">
|
||||
{t("editor.differentiationLabel")}
|
||||
</label>
|
||||
<select
|
||||
value={lessonNode.differentiation ?? ""}
|
||||
onChange={(e) => {
|
||||
const v = e.target.value;
|
||||
updateNode(lessonNode.id, {
|
||||
differentiation: v === "" ? undefined : (v as DifferentiationLevel),
|
||||
});
|
||||
}}
|
||||
className="text-xs border border-outline-variant rounded px-1.5 py-0.5 bg-surface"
|
||||
aria-label={t("editor.differentiationLabel")}
|
||||
>
|
||||
<option value="">{t("editor.differentiationNone")}</option>
|
||||
{DIFFERENTIATION_LEVEL_KEYS.map((d) => (
|
||||
<option key={d} value={d}>
|
||||
{t(`editor.differentiation.${d}`)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* 内容编辑区 - 使用 Error Boundary 包裹 + BlockRenderer 配置驱动渲染 */}
|
||||
<div className="flex-1 overflow-y-auto p-3">
|
||||
<LessonPlanErrorBoundary>
|
||||
<BlockRenderer
|
||||
type={lessonNode.type}
|
||||
blockId={lessonNode.id}
|
||||
data={lessonNode.data}
|
||||
textbookId={textbookId}
|
||||
chapterId={chapterId}
|
||||
classes={classes}
|
||||
planId={planId}
|
||||
onUpdate={(d) => updateNode(lessonNode.id, { data: d })}
|
||||
/>
|
||||
{/* BlockRenderer 返回 null 时显示未知类型提示 */}
|
||||
<UnknownBlockHint type={lessonNode.type} t={t} />
|
||||
</LessonPlanErrorBoundary>
|
||||
|
||||
{/* AI 内容生成区(可折叠)— P0-11 修复:通过 props 注入,不直接 import @/modules/ai */}
|
||||
{aiContentGenerator ? (
|
||||
<div className="mt-4 border-t border-outline-variant pt-3">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="w-full justify-between"
|
||||
onClick={() => setShowAiPanel(!showAiPanel)}
|
||||
aria-expanded={showAiPanel}
|
||||
>
|
||||
<span className="flex items-center gap-1">
|
||||
<Sparkles className="w-3.5 h-3.5 text-primary" />
|
||||
{tAi("lessonPrep.generateContent")}
|
||||
</span>
|
||||
{showAiPanel ? (
|
||||
<ChevronUp className="w-3.5 h-3.5" />
|
||||
) : (
|
||||
<ChevronDown className="w-3.5 h-3.5" />
|
||||
)}
|
||||
</Button>
|
||||
{showAiPanel ? (
|
||||
<div className="mt-2">
|
||||
{(() => {
|
||||
const AiGenerator = aiContentGenerator;
|
||||
return (
|
||||
<AiGenerator
|
||||
topic={aiTopic}
|
||||
textbookId={textbookId}
|
||||
chapterId={chapterId}
|
||||
/>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{/* 底部操作 */}
|
||||
<div className="px-4 py-2 border-t border-outline-variant">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="text-error"
|
||||
onClick={() => removeNode(lessonNode.id)}
|
||||
>
|
||||
<Trash2 className="w-3 h-3 mr-1" aria-hidden="true" />
|
||||
{t("action.delete")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 未知 Block 类型提示(配置驱动,当 BlockRenderer 返回 null 时显示)。
|
||||
* 独立组件避免在主组件中条件渲染。
|
||||
*/
|
||||
function UnknownBlockHint({
|
||||
type,
|
||||
t,
|
||||
}: {
|
||||
type: string;
|
||||
t: ReturnType<typeof useTranslations>;
|
||||
}) {
|
||||
// V4 P1-9 修复:knownTypes 从 BLOCK_REGISTRY 派生,消除重复定义
|
||||
const knownTypes: readonly string[] = Object.keys(BLOCK_REGISTRY);
|
||||
if (knownTypes.includes(type)) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className="text-on-surface-variant text-sm p-4">
|
||||
{t("editor.unknownBlockType")}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user