diff --git a/frontend/src/scenes/notebooks/Notebook/Editor.tsx b/frontend/src/scenes/notebooks/Notebook/Editor.tsx index f9cb505a8b0b0..5f2f60e864e1b 100644 --- a/frontend/src/scenes/notebooks/Notebook/Editor.tsx +++ b/frontend/src/scenes/notebooks/Notebook/Editor.tsx @@ -2,7 +2,7 @@ import posthog from 'posthog-js' import { useActions } from 'kea' import { useCallback, useRef } from 'react' -import { Editor as TTEditor, TextSerializer } from '@tiptap/core' +import { Editor as TTEditor } from '@tiptap/core' import { EditorContent, useEditor } from '@tiptap/react' import { FloatingMenu } from '@tiptap/extension-floating-menu' import StarterKit from '@tiptap/starter-kit' @@ -25,7 +25,7 @@ import { lemonToast } from '@posthog/lemon-ui' import { NotebookNodeType } from '~/types' import { NotebookNodeImage } from '../Nodes/NotebookNodeImage' -import { EditorFocusPosition, EditorRange, JSONContent, Node, NotebookEditor } from './utils' +import { EditorFocusPosition, EditorRange, JSONContent, Node, NotebookEditor, textContent } from './utils' import { SlashCommandsExtension } from './SlashCommands' import { BacklinkCommandsExtension } from './BacklinkCommands' import { NotebookNodeEarlyAccessFeature } from '../Nodes/NotebookNodeEarlyAccessFeature' @@ -183,34 +183,7 @@ export function Editor({ onCreate({ getJSON: () => editor.getJSON(), getText: () => { - const customOrTitleSerializer: TextSerializer = (props): string => { - return props.node.type.spec.serializedText(props.node.attrs) || props.node.attrs?.title || '' - } - - const customNodeTextSerializers: Record = { - 'ph-backlink': customOrTitleSerializer, - 'ph-early-access-feature': customOrTitleSerializer, - 'ph-experiment': customOrTitleSerializer, - 'ph-feature-flag': customOrTitleSerializer, - 'ph-feature-flag-code-example': customOrTitleSerializer, - 'ph-image': customOrTitleSerializer, - 'ph-insight': customOrTitleSerializer, - 'ph-person': customOrTitleSerializer, - 'ph-query': customOrTitleSerializer, - 'ph-recording': customOrTitleSerializer, - 'ph-recording-playlist': customOrTitleSerializer, - 'ph-replay-timestamp': (): string => { - // any comment is reported as text from its paragraph component - // and this is linked to a recording which implicitly makes the timestamp searchable by ID - return '' - }, - 'ph-survey': customOrTitleSerializer, - } - - return editor.getText({ - blockSeparator: ' ', - textSerializers: customNodeTextSerializers, - }) + return textContent(editor.state.doc) }, getSelectedNode: () => editor.state.doc.nodeAt(editor.state.selection.$anchor.pos), getAdjacentNodes: (pos: number) => getAdjacentNodes(editor, pos), diff --git a/frontend/src/scenes/notebooks/Notebook/utils.ts b/frontend/src/scenes/notebooks/Notebook/utils.ts index 7ad5ece6527cb..0134cace7b23a 100644 --- a/frontend/src/scenes/notebooks/Notebook/utils.ts +++ b/frontend/src/scenes/notebooks/Notebook/utils.ts @@ -6,10 +6,12 @@ import { getText, JSONContent as TTJSONContent, Range as EditorRange, + TextSerializer, } from '@tiptap/core' import { Node as PMNode } from '@tiptap/pm/model' import { NodeViewProps } from '@tiptap/react' import { NotebookNodeType } from '~/types' +import { formatTimestamp } from 'scenes/notebooks/Nodes/NotebookNodeReplayTimestamp' export interface Node extends PMNode {} export interface JSONContent extends TTJSONContent {} @@ -87,12 +89,42 @@ export const isCurrentNodeEmpty = (editor: TTEditor): boolean => { return false } -const textContent = (node: any): string => { - return getText(node, { - blockSeparator: ' ', - textSerializers: { - [NotebookNodeType.ReplayTimestamp]: ({ node }) => `${node.attrs.playbackTime || '00:00'}: `, +export const textContent = (node: any): string => { + // any node that is created using `createPostHogWidgetNode` + // may have a custom serializedText function defined + const customOrTitleSerializer: TextSerializer = (props): string => { + // TipTap chooses whether to add a separator based on a couple of factors + // but, we always want a separator since this text is for search purposes + const serializedText = props.node.type.spec.serializedText(props.node.attrs) || props.node.attrs?.title || '' + if (serializedText.length > 0 && serializedText[serializedText.length - 1] !== '\n') { + return serializedText + '\n' + } + return serializedText + } + + const customNodeTextSerializers: Record = { + 'ph-backlink': customOrTitleSerializer, + 'ph-early-access-feature': customOrTitleSerializer, + 'ph-experiment': customOrTitleSerializer, + 'ph-feature-flag': customOrTitleSerializer, + 'ph-feature-flag-code-example': customOrTitleSerializer, + 'ph-image': customOrTitleSerializer, + 'ph-insight': customOrTitleSerializer, + 'ph-person': customOrTitleSerializer, + 'ph-query': customOrTitleSerializer, + 'ph-recording': customOrTitleSerializer, + 'ph-recording-playlist': customOrTitleSerializer, + 'ph-replay-timestamp': (props): string => { + // timestamp is not a block so `getText` does not add a separator. + // we need to add it manually + return `${formatTimestamp(props.node.attrs.playbackTime) || '00:00'}:\n` }, + 'ph-survey': customOrTitleSerializer, + } + + return getText(node, { + blockSeparator: '\n', + textSerializers: customNodeTextSerializers, }) }