diff --git a/frontend/src/scenes/notebooks/Nodes/NodeWrapper.scss b/frontend/src/scenes/notebooks/Nodes/NodeWrapper.scss index 58572fec9bd99b..5455f71ec4c441 100644 --- a/frontend/src/scenes/notebooks/Nodes/NodeWrapper.scss +++ b/frontend/src/scenes/notebooks/Nodes/NodeWrapper.scss @@ -4,7 +4,6 @@ --border-color: var(--border); transform: translate3d(0, 0, 0); - margin: 0.65rem 0px 0.35rem 0px; .NotebookNode__box { transform: translate3d(0, 0, 0); @@ -44,13 +43,35 @@ margin-top: 0; } + .NotebookNode__insertion_prompt { + width: 100%; + padding: 0.1rem 0px; + visibility: hidden; + cursor: pointer; + + .NotebookNode__insertion_prompt__divider { + height: 1px; + } + } + + &:hover { + .NotebookNode__insertion_prompt { + visibility: visible; + + .NotebookNode__insertion_prompt__divider { + background: var(--primary-3000); + } + } + } + &:hover, &--selected { .NotebookNode__actions { opacity: 1; height: 2rem; - margin-top: 0.5rem; + margin-top: 0.2rem; transition: all 150ms linear; + margin-bottom: 19px; } } @@ -111,3 +132,15 @@ } } } + +.react-renderer { + .NotebookNode__actions { + padding-bottom: 19px; + } + + & + & { + .NotebookNode__actions { + padding-bottom: 0px; + } + } +} diff --git a/frontend/src/scenes/notebooks/Nodes/NodeWrapper.tsx b/frontend/src/scenes/notebooks/Nodes/NodeWrapper.tsx index da6c679877f030..fe777dd08ac660 100644 --- a/frontend/src/scenes/notebooks/Nodes/NodeWrapper.tsx +++ b/frontend/src/scenes/notebooks/Nodes/NodeWrapper.tsx @@ -15,6 +15,7 @@ import { IconDragHandle, IconFilter, IconLink, + IconPlus, IconPlusMini, IconUnfoldLess, IconUnfoldMore, @@ -28,7 +29,7 @@ import { useInView } from 'react-intersection-observer' import { NotebookNodeType } from '~/types' import { ErrorBoundary } from '~/layout/ErrorBoundary' import { NotebookNodeContext, NotebookNodeLogicProps, notebookNodeLogic } from './notebookNodeLogic' -import { posthogNodePasteRule, useSyncedAttributes } from './utils' +import { isCustomNode, posthogNodePasteRule, useSyncedAttributes } from './utils' import { NotebookNodeAttributes, NotebookNodeProps, @@ -37,6 +38,7 @@ import { } from '../Notebook/utils' import { useWhyDidIRender } from 'lib/hooks/useWhyDidIRender' import { NotebookNodeTitle } from './components/NotebookNodeTitle' +import { NotebookNodeInsertionPrompt } from './NotebookNodeInsertionPrompt' export interface NodeWrapperProps { nodeType: NotebookNodeType @@ -104,8 +106,8 @@ function NodeWrapper( titlePlaceholder, } const nodeLogic = useMountedLogic(notebookNodeLogic(nodeLogicProps)) - const { resizeable, expanded, actions } = useValues(nodeLogic) - const { setExpanded, deleteNode, toggleEditing } = useActions(nodeLogic) + const { resizeable, expanded, actions, nextNode, previousNode } = useValues(nodeLogic) + const { setExpanded, deleteNode, toggleEditing, insertAfter, insertBefore } = useActions(nodeLogic) useWhyDidIRender('NodeWrapper.logicProps', { resizeable, @@ -146,6 +148,10 @@ function NodeWrapper( // Element is resizable if resizable is set to true. If expandable is set to true then is is only resizable if expanded is true const isResizeable = resizeable && (!expandable || expanded) + const hasActions = actions && actions.length > 0 + + const insertContentBefore = (): void => insertBefore([{ type: 'paragraph' }]) + const insertContentAfter = (): void => insertAfter([{ type: 'paragraph' }]) return ( @@ -158,6 +164,7 @@ function NodeWrapper( 'NotebookNode--auto-hide-metadata': autoHideMetadata, })} > + {!previousNode && }
{!inView ? ( @@ -229,12 +236,22 @@ function NodeWrapper( )}
- {isEditable && actions.length ? ( + {isEditable && hasActions ? (
setTextSelection(getPos() + 1)} > + } + onClick={(e) => { + e.stopPropagation() + insertContentAfter() + }} + /> {actions.map((x, i) => ( ( ))}
) : null} + {(!nextNode || isCustomNode(nextNode)) && !hasActions && ( + + )} diff --git a/frontend/src/scenes/notebooks/Nodes/NotebookNodeInsertionPrompt.tsx b/frontend/src/scenes/notebooks/Nodes/NotebookNodeInsertionPrompt.tsx new file mode 100644 index 00000000000000..adfed70c6b1503 --- /dev/null +++ b/frontend/src/scenes/notebooks/Nodes/NotebookNodeInsertionPrompt.tsx @@ -0,0 +1,9 @@ +export const NotebookNodeInsertionPrompt = ({ onInsert }: { onInsert: () => void }): JSX.Element => { + return ( +
+
+
Add content
+
+
+ ) +} diff --git a/frontend/src/scenes/notebooks/Nodes/notebookNodeLogic.ts b/frontend/src/scenes/notebooks/Nodes/notebookNodeLogic.ts index 27ee45c70b0833..26a76d02149635 100644 --- a/frontend/src/scenes/notebooks/Nodes/notebookNodeLogic.ts +++ b/frontend/src/scenes/notebooks/Nodes/notebookNodeLogic.ts @@ -56,6 +56,7 @@ export const notebookNodeLogic = kea([ setResizeable: (resizeable: boolean) => ({ resizeable }), setActions: (actions: (NotebookNodeAction | undefined)[]) => ({ actions }), insertAfter: (content: JSONContent) => ({ content }), + insertBefore: (content: JSONContent) => ({ content }), insertAfterLastNodeOfType: (nodeType: string, content: JSONContent) => ({ content, nodeType }), updateAttributes: (attributes: Partial>) => ({ attributes }), insertReplayCommentByTimestamp: (timestamp: number, sessionRecordingId: string) => ({ @@ -164,7 +165,12 @@ export const notebookNodeLogic = kea([ insertAfter: ({ content }) => { const logic = values.notebookLogic - logic.values.editor?.insertContentAfterNode(props.getPos(), content) + logic.values.editor?.insertContentAfterNodeAtPosition(props.getPos(), content) + }, + + insertBefore: ({ content }) => { + const logic = values.notebookLogic + logic.values.editor?.insertContentBeforeNodeAtPosition(props.getPos(), content) }, deleteNode: () => { diff --git a/frontend/src/scenes/notebooks/Nodes/utils.tsx b/frontend/src/scenes/notebooks/Nodes/utils.tsx index 70016ffc8f60d1..4eae92addb5a67 100644 --- a/frontend/src/scenes/notebooks/Nodes/utils.tsx +++ b/frontend/src/scenes/notebooks/Nodes/utils.tsx @@ -2,9 +2,10 @@ import { ExtendedRegExpMatchArray, NodeViewProps, PasteRule } from '@tiptap/core import posthog from 'posthog-js' import { NodeType } from '@tiptap/pm/model' import { Editor as TTEditor } from '@tiptap/core' -import { CustomNotebookNodeAttributes, NotebookNodeAttributes } from '../Notebook/utils' +import { CustomNotebookNodeAttributes, NotebookNodeAttributes, Node } from '../Notebook/utils' import { useCallback, useMemo, useRef } from 'react' import { tryJsonParse, uuid } from 'lib/utils' +import { NotebookNodeType } from '~/types' export function createUrlRegex(path: string | RegExp, origin?: string): RegExp { origin = (origin || window.location.origin).replace('.', '\\.') @@ -146,3 +147,20 @@ export function useSyncedAttributes( return [parsedAttrs.current, updateAttributes] } + +export function isCustomNode(node: Node): boolean { + return [ + NotebookNodeType.Query, + NotebookNodeType.Recording, + NotebookNodeType.RecordingPlaylist, + NotebookNodeType.FeatureFlag, + NotebookNodeType.FeatureFlagCodeExample, + NotebookNodeType.Experiment, + NotebookNodeType.EarlyAccessFeature, + NotebookNodeType.Survey, + NotebookNodeType.Person, + NotebookNodeType.Group, + NotebookNodeType.Cohort, + NotebookNodeType.Image, + ].includes(node.type.name as NotebookNodeType) +} diff --git a/frontend/src/scenes/notebooks/Notebook/Editor.tsx b/frontend/src/scenes/notebooks/Notebook/Editor.tsx index 39c6c29115958e..d0596248961cb7 100644 --- a/frontend/src/scenes/notebooks/Notebook/Editor.tsx +++ b/frontend/src/scenes/notebooks/Notebook/Editor.tsx @@ -31,7 +31,6 @@ import { BacklinkCommandsExtension } from './BacklinkCommands' import { NotebookNodeEarlyAccessFeature } from '../Nodes/NotebookNodeEarlyAccessFeature' import { NotebookNodeSurvey } from '../Nodes/NotebookNodeSurvey' import { InlineMenu } from './InlineMenu' -import NodeGapInsertionExtension from './Extensions/NodeGapInsertion' import { notebookLogic } from './notebookLogic' import { sampleOne } from 'lib/utils' import { NotebookNodeGroup } from '../Nodes/NotebookNodeGroup' @@ -111,7 +110,6 @@ export function Editor(): JSX.Element { NotebookNodeImage, SlashCommandsExtension, BacklinkCommandsExtension, - NodeGapInsertionExtension, ], editorProps: { handleDrop: (view, event, _slice, moved) => { @@ -208,13 +206,17 @@ export function Editor(): JSX.Element { destroy: () => editor.destroy(), deleteRange: (range: EditorRange) => editor.chain().focus().deleteRange(range), insertContent: (content: JSONContent) => editor.chain().insertContent(content).focus().run(), - insertContentAfterNode: (position: number, content: JSONContent) => { + insertContentAfterNodeAtPosition: (position: number, content: JSONContent) => { const endPosition = findEndPositionOfNode(editor, position) if (endPosition) { - editor.chain().focus().insertContentAt(endPosition, content).run() + editor.chain().focus().insertContentAt(endPosition, content, { updateSelection: true }).run() editor.commands.scrollIntoView() } }, + insertContentBeforeNodeAtPosition: (position: number, content: JSONContent) => { + editor.chain().focus().insertContentAt(position, content).run() + editor.commands.scrollIntoView() + }, pasteContent: (position: number, text: string) => { editor?.chain().focus().setTextSelection(position).run() editor?.view.pasteText(text) diff --git a/frontend/src/scenes/notebooks/Notebook/Extensions/NodeGapInsertion.ts b/frontend/src/scenes/notebooks/Notebook/Extensions/NodeGapInsertion.ts deleted file mode 100644 index 4fad7ba098a1ad..00000000000000 --- a/frontend/src/scenes/notebooks/Notebook/Extensions/NodeGapInsertion.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Extension } from '@tiptap/core' -import { Plugin, PluginKey } from '@tiptap/pm/state' - -const NodeGapInsertionExtension = Extension.create({ - name: 'nodeGapInsertion', - - addProseMirrorPlugins() { - const { editor } = this - return [ - new Plugin({ - key: new PluginKey('nodeGapInsertion'), - props: { - handleClick(view, pos, event) { - if (!view || !view.editable) { - return false - } - const clickPos = view.posAtCoords({ left: event.clientX, top: event.clientY }) - const node = editor.state.doc.nodeAt(pos) - - if (!clickPos || clickPos.inside > -1 || !node) { - return false - } - - editor.commands.insertContentAt(pos, { type: 'paragraph', content: [] }) - return true - }, - }, - }), - ] - }, -}) - -export default NodeGapInsertionExtension diff --git a/frontend/src/scenes/notebooks/Notebook/notebookLogic.ts b/frontend/src/scenes/notebooks/Notebook/notebookLogic.ts index 20da64681b722d..43eddde0c20801 100644 --- a/frontend/src/scenes/notebooks/Notebook/notebookLogic.ts +++ b/frontend/src/scenes/notebooks/Notebook/notebookLogic.ts @@ -380,7 +380,7 @@ export const notebookLogic = kea([ nextNode = values.editor?.nextNode(insertionPosition) } - values.editor?.insertContentAfterNode(insertionPosition, content) + values.editor?.insertContentAfterNodeAtPosition(insertionPosition, content) } ) }, @@ -404,7 +404,7 @@ export const notebookLogic = kea([ nextNode = values.editor?.nextNode(insertionPosition) } - values.editor?.insertContentAfterNode(insertionPosition, content) + values.editor?.insertContentAfterNodeAtPosition(insertionPosition, content) } ) }, @@ -427,7 +427,7 @@ export const notebookLogic = kea([ } } - values.editor?.insertContentAfterNode( + values.editor?.insertContentAfterNodeAtPosition( insertionPosition, buildTimestampCommentContent({ playbackTime: timestamp, diff --git a/frontend/src/scenes/notebooks/Notebook/utils.ts b/frontend/src/scenes/notebooks/Notebook/utils.ts index 2d0427b2a2ca41..b34f1376a1f059 100644 --- a/frontend/src/scenes/notebooks/Notebook/utils.ts +++ b/frontend/src/scenes/notebooks/Notebook/utils.ts @@ -66,7 +66,8 @@ export interface NotebookEditor { destroy: () => void deleteRange: (range: EditorRange) => EditorCommands insertContent: (content: JSONContent) => void - insertContentAfterNode: (position: number, content: JSONContent) => void + insertContentAfterNodeAtPosition: (position: number, content: JSONContent) => void + insertContentBeforeNodeAtPosition: (position: number, content: JSONContent) => void pasteContent: (position: number, text: string) => void findNode: (position: number) => Node | null findNodePositionByAttrs: (attrs: Record) => any