fix: preserve pasted content in new memos
This commit is contained in:
1 parent
7d471dd178
commit
36796994d5
5 files changed
+29
-11
No files matched your search
@@ -162,6 +162,7 @@ type MemoSummaryRow = {
|
||||
title: string | null;
|
||||
excerpt: string;
|
||||
content_text?: string | null;
|
||||
content_markdown?: string | null;
|
||||
tags_json: string;
|
||||
is_pinned: number;
|
||||
is_archived: number;
|
||||
@@ -1534,7 +1535,7 @@ app.get("/api/v1/memos", async (c) => {
|
||||
)
|
||||
SELECT m.id, m.notebook_id, m.title, m.excerpt, m.tags_json, m.is_pinned,
|
||||
m.is_archived, m.is_deleted, m.created_at, m.updated_at, m.deleted_at, mc.revision,
|
||||
mc.content_text
|
||||
mc.content_text, mc.content_markdown
|
||||
FROM search_matches s
|
||||
INNER JOIN memos m ON m.id = s.memo_id
|
||||
INNER JOIN memo_contents mc ON mc.memo_id = m.id
|
||||
@@ -1587,7 +1588,7 @@ app.get("/api/v1/memos", async (c) => {
|
||||
c.env.storage.db.prepare(
|
||||
`SELECT m.id, m.notebook_id, m.title, m.excerpt, m.tags_json, m.is_pinned,
|
||||
m.is_archived, m.is_deleted, m.created_at, m.updated_at, m.deleted_at, mc.revision,
|
||||
mc.content_text
|
||||
mc.content_text, mc.content_markdown
|
||||
FROM memos m
|
||||
INNER JOIN memo_contents mc ON mc.memo_id = m.id
|
||||
WHERE ${searchCursorConditions.join(" AND ")}
|
||||
@@ -1616,7 +1617,7 @@ app.get("/api/v1/memos", async (c) => {
|
||||
c.env.storage.db.prepare(
|
||||
`SELECT m.id, m.notebook_id, m.title, m.excerpt, m.tags_json, m.is_pinned,
|
||||
m.is_archived, m.is_deleted, m.created_at, m.updated_at, m.deleted_at, mc.revision,
|
||||
mc.content_text
|
||||
mc.content_text, mc.content_markdown
|
||||
FROM memos m
|
||||
INNER JOIN memo_contents mc ON mc.memo_id = m.id
|
||||
WHERE ${cursorConditions.join(" AND ")}
|
||||
@@ -4443,7 +4444,7 @@ const mapMemoSummary = (row: MemoSummaryRow): MemoSummary => ({
|
||||
id: row.id,
|
||||
notebookId: row.notebook_id,
|
||||
title: row.title,
|
||||
excerpt: row.excerpt || createExcerpt(row.content_text ?? ""),
|
||||
excerpt: row.excerpt || createExcerpt(row.content_text ?? "") || createExcerpt(docToText(markdownToDoc(row.content_markdown ?? ""))),
|
||||
tags: parseJsonArray(row.tags_json),
|
||||
isPinned: Boolean(row.is_pinned),
|
||||
isArchived: Boolean(row.is_archived),
|
||||
|
||||
@@ -1166,6 +1166,7 @@ const RichEditorPane = ({
|
||||
const [tagsText, setTagsText] = useState("");
|
||||
const [saveState, setSaveState] = useState<"idle" | "saving" | "saved" | "queued" | "error" | "conflict">("idle");
|
||||
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
|
||||
const [hydratedEditorMemoId, setHydratedEditorMemoId] = useState<string | null>(null);
|
||||
const [dirtyVersion, setDirtyVersion] = useState(0);
|
||||
const [, setEditorStateVersion] = useState(0);
|
||||
const [editorContentVersion, setEditorContentVersion] = useState(0);
|
||||
@@ -1413,7 +1414,7 @@ const RichEditorPane = ({
|
||||
content: memo
|
||||
? resolveMemoContentDoc(memo.contentJson, memo.contentMarkdown)
|
||||
: { type: "doc", content: [{ type: "paragraph" }] },
|
||||
editable: Boolean(memo && !effectiveReadOnly),
|
||||
editable: Boolean(memo && !effectiveReadOnly && hydratedEditorMemoId === memo.id),
|
||||
editorProps: {
|
||||
attributes: {
|
||||
class: "prose prose-slate max-w-none focus:outline-none min-h-[300px] px-4 py-3 sm:px-7",
|
||||
@@ -1887,6 +1888,7 @@ const RichEditorPane = ({
|
||||
memoRef.current = null;
|
||||
editSessionRef.current = null;
|
||||
hydratedMemoIdRef.current = null;
|
||||
setHydratedEditorMemoId(null);
|
||||
editingMemoIdRef.current = null;
|
||||
hasUnsavedChangesRef.current = false;
|
||||
setHasUnsavedChanges(false);
|
||||
@@ -1908,6 +1910,7 @@ const RichEditorPane = ({
|
||||
|
||||
if (!sameMemo) {
|
||||
hydratedMemoIdRef.current = null;
|
||||
setHydratedEditorMemoId(null);
|
||||
}
|
||||
|
||||
if (sameMemo && hasUnsavedChangesRef.current && !memo.isDeleted) {
|
||||
@@ -1969,6 +1972,7 @@ const RichEditorPane = ({
|
||||
|
||||
hydratedMemoIdRef.current = memo.id;
|
||||
editSessionRef.current = editSessionResponse?.editSession ?? (requiresLocalEditSession(memo) ? createLocalEditSession(memo) : null);
|
||||
setHydratedEditorMemoId(memo.id);
|
||||
|
||||
window.setTimeout(() => {
|
||||
hydratingRef.current = false;
|
||||
@@ -2001,9 +2005,9 @@ const RichEditorPane = ({
|
||||
|
||||
useEffect(() => {
|
||||
if (isEditorReady(editor)) {
|
||||
editor.setEditable(Boolean(memo && !effectiveReadOnly));
|
||||
editor.setEditable(Boolean(memo && !effectiveReadOnly && hydratedEditorMemoId === memo.id));
|
||||
}
|
||||
}, [editor, effectiveReadOnly, memo]);
|
||||
}, [editor, effectiveReadOnly, hydratedEditorMemoId, memo]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isEditorReady(editor) || !memo) {
|
||||
|
||||
@@ -38,7 +38,7 @@ import {
|
||||
} from "@/lib/mobile-editor";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { isBrowserOffline, isBrowserOnline, verifyBrowserConnectivity } from "@/lib/network-status";
|
||||
import { createExcerpt, docToText, getNotebookDescendantIds, type Notebook, type AuthUser, type MemoSummary, type MemoDetail, type Resource, type MemoTemplate as SavedMemoTemplate } from "@edgeever/shared";
|
||||
import { createExcerpt, docToText, getNotebookDescendantIds, resolveMemoContentDoc, type Notebook, type AuthUser, type MemoSummary, type MemoDetail, type Resource, type MemoTemplate as SavedMemoTemplate } from "@edgeever/shared";
|
||||
import { toggleMobileMemoSelection } from "@edgeever/shared/mobile-ui";
|
||||
import type {
|
||||
Pane,
|
||||
@@ -171,7 +171,7 @@ const memoToSummary = (memo: MemoDetail): MemoSummary => ({
|
||||
id: memo.id,
|
||||
notebookId: memo.notebookId,
|
||||
title: memo.title,
|
||||
excerpt: memo.excerpt || createExcerpt(memo.contentText || docToText(memo.contentJson) || memo.contentMarkdown),
|
||||
excerpt: memo.excerpt || createExcerpt(memo.contentText || docToText(resolveMemoContentDoc(memo.contentJson, memo.contentMarkdown))),
|
||||
tags: memo.tags,
|
||||
isPinned: memo.isPinned,
|
||||
isArchived: memo.isArchived,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { countMemoCharacters, docToMarkdown, markdownToDoc, resolveMemoContentDoc } from "./content.ts";
|
||||
import { countMemoCharacters, docToMarkdown, docToText, markdownToDoc, resolveMemoContentDoc } from "./content.ts";
|
||||
|
||||
describe("memo character count", () => {
|
||||
test("counts punctuation while excluding whitespace and formatting", () => {
|
||||
@@ -59,6 +59,15 @@ describe("Markdown table conversion", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("legacy Markdown body recovery", () => {
|
||||
test("recovers a body when the stored JSON document is empty", () => {
|
||||
const markdown = "权威和反中心化特征,即当一个人成为某种代表性的符号之后";
|
||||
const resolved = resolveMemoContentDoc(markdownToDoc(""), markdown);
|
||||
|
||||
expect(docToText(resolved)).toContain(markdown);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Nested list Markdown conversion", () => {
|
||||
const markdown = [
|
||||
"- Level 1",
|
||||
|
||||
@@ -77,7 +77,11 @@ export const resolveMemoContentDoc = (
|
||||
}
|
||||
|
||||
const markdownDoc = markdownToDoc(contentMarkdown);
|
||||
return docContainsNodeType(markdownDoc, "table") ? markdownDoc : currentDoc;
|
||||
// Some older saves left an empty JSON document behind while retaining the
|
||||
// real body in Markdown. Treat that as a compatibility case too; otherwise
|
||||
// the editor can show the Markdown body while list excerpts see an empty
|
||||
// JSON document.
|
||||
return docContainsNodeType(markdownDoc, "table") || !docToText(currentDoc) ? markdownDoc : currentDoc;
|
||||
};
|
||||
|
||||
const LEGACY_ATTACHMENT_PATTERN = /^(附件:|Attachment:\s*)(.+?)\s+(\/api\/v1\/resources\/\S+|https?:\/\/\S+)$/;
|
||||
|
||||
Reference in new issue
Block a user