Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions apps/desktop/src/session/components/note-input/enhanced/editor.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { forwardRef, useMemo } from "react";
import { forwardRef, useCallback, useMemo } from "react";

import { commands as openerCommands } from "@hypr/plugin-opener2";
import { type JSONContent, TiptapEditor } from "@hypr/tiptap/editor";
import NoteEditor from "@hypr/tiptap/editor";
import { parseJsonContent } from "@hypr/tiptap/shared";
import { isEmptyTiptapContent, parseJsonContent } from "@hypr/tiptap/shared";

import { useSearchEngine } from "~/search/contexts/engine";
import { useImageUpload } from "~/shared/hooks/useImageUpload";
Expand All @@ -26,14 +26,25 @@ export const EnhancedEditor = forwardRef<
[content],
);

const handleChange = main.UI.useSetPartialRowCallback(
const persistChange = main.UI.useSetPartialRowCallback(
"enhanced_notes",
enhancedNoteId,
(input: JSONContent) => ({ content: JSON.stringify(input) }),
[],
main.STORE_ID,
);

const handleChange = useCallback(
(input: JSONContent) => {
if (content === undefined && isEmptyTiptapContent(input)) {
return;
}

persistChange(input);
},
[content, persistChange],
);

const { search } = useSearchEngine();
const sessions = main.UI.useResultTable(
main.QUERIES.timelineSessions,
Expand Down
7 changes: 6 additions & 1 deletion apps/desktop/src/session/components/note-input/raw.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import NoteEditor, {
type TiptapEditor,
} from "@hypr/tiptap/editor";
import {
isEmptyTiptapContent,
parseJsonContent,
type PlaceholderFunction,
} from "@hypr/tiptap/shared";
Expand Down Expand Up @@ -50,6 +51,10 @@ export const RawEditor = forwardRef<

const handleChange = useCallback(
(input: JSONContent) => {
if (rawMd === undefined && isEmptyTiptapContent(input)) {
return;
}

persistChange(input);

if (!hasTrackedWriteRef.current) {
Expand All @@ -63,7 +68,7 @@ export const RawEditor = forwardRef<
}
}
},
[persistChange, hasNonEmptyText],
[hasNonEmptyText, persistChange, rawMd],
);

const { search } = useSearchEngine();
Expand Down
60 changes: 59 additions & 1 deletion packages/tiptap/src/shared/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,65 @@ import type { JSONContent } from "@tiptap/react";
import { describe, expect, test } from "vitest";

import { getExtensions } from "./extensions";
import { isValidTiptapContent, json2md, md2json } from "./utils";
import {
isEmptyTiptapContent,
isValidTiptapContent,
json2md,
md2json,
} from "./utils";

describe("isEmptyTiptapContent", () => {
test("treats empty paragraphs as empty", () => {
expect(
isEmptyTiptapContent({
type: "doc",
content: [{ type: "paragraph" }],
}),
).toBe(true);
});

test("treats whitespace-only text as empty", () => {
expect(
isEmptyTiptapContent({
type: "doc",
content: [
{
type: "paragraph",
content: [{ type: "text", text: " " }],
},
],
}),
).toBe(true);
});

test("treats text content as non-empty", () => {
expect(
isEmptyTiptapContent({
type: "doc",
content: [
{
type: "paragraph",
content: [{ type: "text", text: "hello" }],
},
],
}),
).toBe(false);
});

test("treats non-text nodes as non-empty", () => {
expect(
isEmptyTiptapContent({
type: "doc",
content: [
{
type: "image",
attrs: { src: "https://example.com/image.png" },
},
],
}),
).toBe(false);
});
});

describe("json2md", () => {
test("renders task items without escaping brackets", () => {
Expand Down
35 changes: 35 additions & 0 deletions packages/tiptap/src/shared/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,23 @@ export const EMPTY_TIPTAP_DOC: JSONContent = {
content: [{ type: "paragraph" }],
};

const EMPTY_CONTAINER_NODE_TYPES = new Set([
"blockquote",
"bulletList",
"codeBlock",
"doc",
"heading",
"listItem",
"orderedList",
"paragraph",
"table",
"tableCell",
"tableHeader",
"tableRow",
"taskItem",
"taskList",
]);

let _markdownManager: MarkdownManager | null = null;

function getMarkdownManager(): MarkdownManager {
Expand All @@ -26,6 +43,24 @@ export function isValidTiptapContent(content: unknown): content is JSONContent {
return obj.type === "doc" && Array.isArray(obj.content);
}

export function isEmptyTiptapContent(
content: JSONContent | undefined,
): boolean {
if (!content) {
return true;
}

if (content.type === "text") {
return !content.text?.trim();
}

if (content.content?.length) {
return !content.content.some((child) => !isEmptyTiptapContent(child));
}

return EMPTY_CONTAINER_NODE_TYPES.has(content.type ?? "");
}

export function parseJsonContent(raw: string | undefined | null): JSONContent {
if (typeof raw !== "string" || !raw.trim()) {
return EMPTY_TIPTAP_DOC;
Expand Down
Loading