refactor: remove Tiptap editor dependency and legacy JSON format support from wiki components

This commit is contained in:
taDuc
2026-05-15 00:04:44 +07:00
parent 57e3d6b3e5
commit 3682f25282
10 changed files with 30 additions and 1402 deletions
@@ -20,24 +20,6 @@ type Props = {
onWikiLinkRequest: (request: { slug: string; rect: DOMRect }) => void;
};
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}
function tiptapJsonToPlainText(node: unknown): string {
if (node == null) return "";
if (typeof node === "string") return node;
if (Array.isArray(node)) return node.map(tiptapJsonToPlainText).join("");
if (isRecord(node)) {
if (node.type === "text" && typeof node.text === "string") return node.text;
if (node.type === "hardBreak") return "\n";
if ("content" in node) return tiptapJsonToPlainText(node.content);
}
return "";
}
function escapeHtml(input: string): string {
return input
.replaceAll("&", "&amp;")
@@ -53,17 +35,6 @@ function normalizeWikiContentToHtml(raw: string | null | undefined): string {
if (value[0] === "<") return value;
if (value[0] === "{") {
try {
const json: unknown = JSON.parse(value);
const text = tiptapJsonToPlainText(json).trim();
if (!text.length) return "";
return `<p>${escapeHtml(text).replace(/\n/g, "<br/>")}</p>`;
} catch {
// fall through
}
}
return `<p>${escapeHtml(value).replace(/\n/g, "<br/>")}</p>`;
}
+10 -38
View File
@@ -36,6 +36,13 @@ type QuillLinkFormat = {
__uhmAllowSlugHref?: boolean;
__uhmOriginalSanitize?: unknown;
};
type QuillImageFormatCtor = {
new (): {
domNode: Element;
format: (name: string, value: string) => void;
};
formats: (domNode: Element) => Record<string, string>;
};
const ReactQuillEditor = dynamic<ReactQuillProps>(() => import("react-quill-new"), {
ssr: false,
@@ -113,7 +120,7 @@ export default function WikiSidebarPanel({ projectId, wikis, setWikis, requested
console.error("Failed to load quill-blot-formatter", err);
}
const ImageFormat = Quill.import?.("formats/image") as any;
const ImageFormat = Quill.import?.("formats/image") as QuillImageFormatCtor | undefined;
if (ImageFormat) {
class CustomImage extends ImageFormat {
static formats(domNode: Element) {
@@ -1059,22 +1066,8 @@ function normalizeWikiDocForQuill(doc: string | null): string {
const raw = (doc || "").trim();
if (!raw.length) return "";
// New format (Quill): HTML string.
if (raw[0] === "<") return raw;
// Legacy format (Tiptap): JSON string.
if (raw[0] === "{") {
try {
const json: unknown = JSON.parse(raw);
const text = tiptapJsonToPlainText(json).trim();
if (!text.length) return "";
return `<p>${escapeHtml(text).replace(/\n/g, "<br/>")}</p>`;
} catch {
// fall through
}
}
// Unknown plaintext: treat as plain text.
return `<p>${escapeHtml(raw).replace(/\n/g, "<br/>")}</p>`;
}
@@ -1096,24 +1089,6 @@ function slugifyWikiTitle(raw: string): string {
.slice(0, 80);
}
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}
function tiptapJsonToPlainText(node: unknown): string {
if (node == null) return "";
if (typeof node === "string") return node;
if (Array.isArray(node)) return node.map(tiptapJsonToPlainText).join("");
if (isRecord(node)) {
if (node.type === "text" && typeof node.text === "string") return node.text;
if (node.type === "hardBreak") return "\n";
if ("content" in node) return tiptapJsonToPlainText(node.content);
}
return "";
}
function escapeHtml(input: string): string {
return input
.replaceAll("&", "&amp;")
@@ -1123,15 +1098,12 @@ function escapeHtml(input: string): string {
.replaceAll("'", "&#39;");
}
type WikiDocStorageFormat = "html" | "json" | "text";
type WikiDocStorageFormat = "html" | "text";
function detectWikiDocStorageFormat(doc: string): WikiDocStorageFormat {
const raw = String(doc || "").trim();
if (!raw.length) return "html";
const first = raw[0];
if (first === "<") return "html";
if (first === "{" || first === "[") return "json";
return "text";
return raw[0] === "<" ? "html" : "text";
}
function downloadTextFile(filename: string, contents: string, mime: string): void {
+1 -1
View File
@@ -113,7 +113,7 @@ Các file nên đọc trước:
Các điểm dễ làm hỏng:
- sanitize link của Quill
- compatibility với doc dạng HTML/Tiptap JSON/plain text
- compatibility với doc dạng HTML/plain text
- slug links nội bộ
- sentinel `__missing__`
-1
View File
@@ -218,7 +218,6 @@ Các khả năng đang có:
Storage thực tế của `doc`:
- format mới: HTML string
- format cũ tương thích: Tiptap JSON string
- plaintext fallback
### Internal wiki link
+2 -7
View File
@@ -8,18 +8,16 @@ Wiki trong UHM editor hiện chạy qua hai phần:
## 1. Storage format của wiki doc
Field `doc` trong `WikiSnapshot` hiện là `string | null`.
Frontend đang hỗ trợ ba dạng:
Frontend hiện hỗ trợ hai dạng:
- HTML string
- JSON string kiểu Tiptap cũ
- plain text fallback
Quy ước hiện tại:
- format ghi mới từ editor Quill là HTML
- Tiptap JSON chỉ còn để tương thích dữ liệu cũ
`normalizeWikiDocForQuill()``normalizeWikiContentToHtml()` là hai hàm quan trọng cho compatibility này.
`normalizeWikiDocForQuill()``normalizeWikiContentToHtml()` hiện chỉ xử lý HTML hoặc plain text.
## 2. Editor hiện dùng Quill, không dùng Tiptap
@@ -42,8 +40,6 @@ Toolbar hiện có:
- `image`
- `clean`
Trang `app/user/wikieditor/page.tsx` vẫn dùng Tiptap, nhưng đó là trang riêng và không phải wiki editor chính của project UHM.
## 3. Tạo, sửa và xóa wiki trong project editor
`WikiSidebarPanel` hỗ trợ:
@@ -89,7 +85,6 @@ Export hiện chỉ là download text từ `wikiDocHtml`.
Định dạng file được đoán từ nội dung hiện tại:
- bắt đầu bằng `<` -> `html`
- bắt đầu bằng `{` hoặc `[` -> `json`
- còn lại -> `txt`
Đây là export client-side, không có API export chuyên biệt.
+1 -2
View File
@@ -1,5 +1,5 @@
// BackEndGo snapshot expects wiki doc as a string (stored in DB as TEXT).
// FE stores Tiptap JSON as a JSON-stringified payload.
// FE wiki runtime now stores HTML or plain text in this string field.
export type WikiDoc = string | null;
export type WikiContentSample = {
@@ -20,4 +20,3 @@ export type WikiSnapshot = {
content_sample?: WikiContentSample[];
updated_at?: string;
};