diff --git a/frontend/src/lib/utils.tsx b/frontend/src/lib/utils.tsx index 770c7a945f52a..72079393acb98 100644 --- a/frontend/src/lib/utils.tsx +++ b/frontend/src/lib/utils.tsx @@ -1439,6 +1439,14 @@ export function validateJson(value: string): boolean { } } +export function tryJsonParse(value: string, fallback?: any): any { + try { + return JSON.parse(value) + } catch (error) { + return fallback + } +} + export function validateJsonFormItem(_: any, value: string): Promise { return validateJson(value) ? Promise.resolve() : Promise.reject('Not valid JSON!') } diff --git a/frontend/src/scenes/notebooks/Nodes/NodeWrapper.tsx b/frontend/src/scenes/notebooks/Nodes/NodeWrapper.tsx index ee4ca592ef1cd..5f58cad124a1b 100644 --- a/frontend/src/scenes/notebooks/Nodes/NodeWrapper.tsx +++ b/frontend/src/scenes/notebooks/Nodes/NodeWrapper.tsx @@ -5,6 +5,7 @@ import { ReactNodeViewRenderer, ExtendedRegExpMatchArray, Attribute, + NodeViewProps, } from '@tiptap/react' import { ReactNode, useCallback, useRef } from 'react' import clsx from 'clsx' @@ -17,9 +18,8 @@ import { notebookLogic } from '../Notebook/notebookLogic' import { useInView } from 'react-intersection-observer' import { NotebookNodeType } from '~/types' import { ErrorBoundary } from '~/layout/ErrorBoundary' -import { NotebookNodeContext, notebookNodeLogic } from './notebookNodeLogic' -import { uuid } from 'lib/utils' -import { posthogNodePasteRule } from './utils' +import { NotebookNodeContext, NotebookNodeLogicProps, notebookNodeLogic } from './notebookNodeLogic' +import { posthogNodePasteRule, useSyncedAttributes } from './utils' import { NotebookNodeAttributes, NotebookNodeViewProps, @@ -61,6 +61,7 @@ export function NodeWrapper({ minHeight, node, getPos, + attributes, updateAttributes, widgets = [], }: NodeWrapperProps & NotebookNodeViewProps): JSX.Element { @@ -68,11 +69,11 @@ export function NodeWrapper({ const { isEditable } = useValues(mountedNotebookLogic) // nodeId can start null, but should then immediately be generated - const nodeId = node.attrs.nodeId - const nodeLogicProps = { + const nodeId = attributes.nodeId + const nodeLogicProps: NotebookNodeLogicProps = { node, nodeType, - nodeAttributes: node.attrs, + attributes, updateAttributes, nodeId, notebookLogic: mountedNotebookLogic, @@ -90,7 +91,7 @@ export function NodeWrapper({ const contentRef = useRef(null) // If resizeable is true then the node attr "height" is required - const height = node.attrs.height ?? heightEstimate + const height = attributes.height ?? heightEstimate const onResizeStart = useCallback((): void => { if (!resizeable) { @@ -104,14 +105,14 @@ export function NodeWrapper({ if (heightAttr && heightAttr !== initialHeightAttr) { updateAttributes({ height: contentRef.current?.clientHeight, - }) + } as any) } } window.addEventListener('mouseup', onResizedEnd) }, [resizeable, updateAttributes]) - const parsedHref = typeof href === 'function' ? href(node.attrs) : href + const parsedHref = typeof href === 'function' ? href(attributes) : href // 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) @@ -219,19 +220,28 @@ export function createPostHogWidgetNode( attributes, ...wrapperProps }: CreatePostHogWidgetNodeOptions): Node { - const WrappedComponent = (props: NotebookNodeViewProps): JSX.Element => { + // NOTE: We use NodeViewProps here as we convert them to NotebookNodeViewProps + const WrappedComponent = (props: NodeViewProps): JSX.Element => { + const [attributes, updateAttributes] = useSyncedAttributes(props) + if (props.node.attrs.nodeId === null) { // TODO only wrapped in setTimeout because of the flushSync bug setTimeout(() => { props.updateAttributes({ - nodeId: uuid(), + nodeId: attributes.nodeId, }) }, 0) } + const nodeProps: NotebookNodeViewProps = { + ...props, + attributes, + updateAttributes, + } + return ( - - + + ) } diff --git a/frontend/src/scenes/notebooks/Nodes/NotebookNodeEarlyAccessFeature.tsx b/frontend/src/scenes/notebooks/Nodes/NotebookNodeEarlyAccessFeature.tsx index fe4c25393f580..801970c380b51 100644 --- a/frontend/src/scenes/notebooks/Nodes/NotebookNodeEarlyAccessFeature.tsx +++ b/frontend/src/scenes/notebooks/Nodes/NotebookNodeEarlyAccessFeature.tsx @@ -16,7 +16,7 @@ import { PersonList } from 'scenes/early-access-features/EarlyAccessFeature' import { buildFlagContent } from './NotebookNodeFlag' const Component = (props: NotebookNodeViewProps): JSX.Element => { - const { id } = props.node.attrs + const { id } = props.attributes const { earlyAccessFeature, earlyAccessFeatureLoading } = useValues(earlyAccessFeatureLogic({ id })) const { expanded } = useValues(notebookNodeLogic) const { insertAfter } = useActions(notebookNodeLogic) diff --git a/frontend/src/scenes/notebooks/Nodes/NotebookNodeExperiment.tsx b/frontend/src/scenes/notebooks/Nodes/NotebookNodeExperiment.tsx index ae7af2b3c00a3..cb62cd17f301f 100644 --- a/frontend/src/scenes/notebooks/Nodes/NotebookNodeExperiment.tsx +++ b/frontend/src/scenes/notebooks/Nodes/NotebookNodeExperiment.tsx @@ -19,7 +19,7 @@ import { ExperimentResult } from 'scenes/experiments/ExperimentResult' import { ResultsTag, StatusTag } from 'scenes/experiments/Experiment' const Component = (props: NotebookNodeViewProps): JSX.Element => { - const { id } = props.node.attrs + const { id } = props.attributes const { experiment, experimentLoading, isExperimentRunning } = useValues(experimentLogic({ experimentId: id })) const { loadExperiment } = useActions(experimentLogic({ experimentId: id })) const { expanded, nextNode } = useValues(notebookNodeLogic) diff --git a/frontend/src/scenes/notebooks/Nodes/NotebookNodeFlag.tsx b/frontend/src/scenes/notebooks/Nodes/NotebookNodeFlag.tsx index 09ab1aff3c398..0e315def449b9 100644 --- a/frontend/src/scenes/notebooks/Nodes/NotebookNodeFlag.tsx +++ b/frontend/src/scenes/notebooks/Nodes/NotebookNodeFlag.tsx @@ -17,7 +17,7 @@ import { buildEarlyAccessFeatureContent } from './NotebookNodeEarlyAccessFeature import { notebookNodeFlagLogic } from './NotebookNodeFlagLogic' const Component = (props: NotebookNodeViewProps): JSX.Element => { - const { id } = props.node.attrs + const { id } = props.attributes const { featureFlag, featureFlagLoading, diff --git a/frontend/src/scenes/notebooks/Nodes/NotebookNodeFlagCodeExample.tsx b/frontend/src/scenes/notebooks/Nodes/NotebookNodeFlagCodeExample.tsx index 2167d7358b3e7..6249b17f51349 100644 --- a/frontend/src/scenes/notebooks/Nodes/NotebookNodeFlagCodeExample.tsx +++ b/frontend/src/scenes/notebooks/Nodes/NotebookNodeFlagCodeExample.tsx @@ -9,7 +9,7 @@ import { notebookNodeLogic } from './notebookNodeLogic' import api from 'lib/api' const Component = (props: NotebookNodeViewProps): JSX.Element => { - const { id } = props.node.attrs + const { id } = props.attributes const { featureFlag } = useValues(featureFlagLogic({ id })) const { expanded } = useValues(notebookNodeLogic) diff --git a/frontend/src/scenes/notebooks/Nodes/NotebookNodeImage.tsx b/frontend/src/scenes/notebooks/Nodes/NotebookNodeImage.tsx index 808d4e886c0d2..effdf63d7afcf 100644 --- a/frontend/src/scenes/notebooks/Nodes/NotebookNodeImage.tsx +++ b/frontend/src/scenes/notebooks/Nodes/NotebookNodeImage.tsx @@ -9,7 +9,7 @@ import { NotebookNodeViewProps } from '../Notebook/utils' const MAX_DEFAULT_HEIGHT = 1000 const Component = (props: NotebookNodeViewProps): JSX.Element => { - const { file, src, height } = props.node.attrs + const { file, src, height } = props.attributes const [uploading, setUploading] = useState(false) const [error, setError] = useState() diff --git a/frontend/src/scenes/notebooks/Nodes/NotebookNodePerson.tsx b/frontend/src/scenes/notebooks/Nodes/NotebookNodePerson.tsx index 2de3d39f965b8..a8640e956759a 100644 --- a/frontend/src/scenes/notebooks/Nodes/NotebookNodePerson.tsx +++ b/frontend/src/scenes/notebooks/Nodes/NotebookNodePerson.tsx @@ -13,7 +13,7 @@ import { asDisplay } from 'scenes/persons/person-utils' import api from 'lib/api' const Component = (props: NotebookNodeViewProps): JSX.Element => { - const id = props.node.attrs.id + const { id } = props.attributes const logic = personLogic({ id }) const { person, personLoading } = useValues(logic) const { expanded } = useValues(notebookNodeLogic) diff --git a/frontend/src/scenes/notebooks/Nodes/NotebookNodePlaylist.tsx b/frontend/src/scenes/notebooks/Nodes/NotebookNodePlaylist.tsx index 62cd9e2505657..0b801328e378e 100644 --- a/frontend/src/scenes/notebooks/Nodes/NotebookNodePlaylist.tsx +++ b/frontend/src/scenes/notebooks/Nodes/NotebookNodePlaylist.tsx @@ -4,7 +4,6 @@ import { RecordingsLists, SessionRecordingsPlaylistProps, } from 'scenes/session-recordings/playlist/SessionRecordingsPlaylist' -import { useJsonNodeState } from './utils' import { addedAdvancedFilters, getDefaultFilters, @@ -12,31 +11,30 @@ import { } from 'scenes/session-recordings/playlist/sessionRecordingsListLogic' import { useActions, useValues } from 'kea' import { SessionRecordingPlayer } from 'scenes/session-recordings/player/SessionRecordingPlayer' -import { useMemo, useRef, useState } from 'react' -import { fromParamsGivenUrl, uuid } from 'lib/utils' +import { useMemo, useState } from 'react' +import { fromParamsGivenUrl } from 'lib/utils' import { LemonButton } from '@posthog/lemon-ui' import { IconChevronLeft, IconSettings } from 'lib/lemon-ui/icons' import { urls } from 'scenes/urls' import { notebookNodeLogic } from './notebookNodeLogic' -import { JSONContent, NotebookNodeViewProps, NotebookNodeWidgetSettings } from '../Notebook/utils' +import { JSONContent, NotebookNodeViewProps, NotebookNodeAttributeProperties } from '../Notebook/utils' import { SessionRecordingsFilters } from 'scenes/session-recordings/filters/SessionRecordingsFilters' import { ErrorBoundary } from '@sentry/react' const Component = (props: NotebookNodeViewProps): JSX.Element => { - const [filters, setFilters] = useJsonNodeState( - props.node.attrs, - props.updateAttributes, - 'filters' - ) - - const playerKey = useRef(`notebook-${uuid()}`).current + const { filters, nodeId } = props.attributes + const playerKey = `notebook-${nodeId}` const recordingPlaylistLogicProps: SessionRecordingsPlaylistProps = { + logicKey: playerKey, filters, updateSearchParams: false, autoPlay: false, - mode: 'notebook', - onFiltersChange: setFilters, + onFiltersChange: (newFilters) => { + props.updateAttributes({ + filters: newFilters, + }) + }, } const { expanded } = useValues(notebookNodeLogic) @@ -48,6 +46,7 @@ const Component = (props: NotebookNodeViewProps) if (!expanded) { return
20+ recordings
} + const content = !activeSessionRecording?.id ? ( ) : ( @@ -75,12 +74,8 @@ const Component = (props: NotebookNodeViewProps) export const Settings = ({ attributes, updateAttributes, -}: NotebookNodeWidgetSettings): JSX.Element => { - const [filters, setFilters] = useJsonNodeState( - attributes, - updateAttributes, - 'filters' - ) +}: NotebookNodeAttributeProperties): JSX.Element => { + const { filters } = attributes const [showAdvancedFilters, setShowAdvancedFilters] = useState(false) const defaultFilters = getDefaultFilters() @@ -93,9 +88,9 @@ export const Settings = ({ updateAttributes({ filters })} showPropertyFilters - onReset={() => setFilters(undefined)} + onReset={() => updateAttributes({ filters: undefined })} hasAdvancedFilters={hasAdvancedFilters} showAdvancedFilters={showAdvancedFilters} setShowAdvancedFilters={setShowAdvancedFilters} @@ -105,7 +100,7 @@ export const Settings = ({ } type NotebookNodePlaylistAttributes = { - filters: FilterType + filters: RecordingFilters } export const NotebookNodePlaylist = createPostHogWidgetNode({ diff --git a/frontend/src/scenes/notebooks/Nodes/NotebookNodeQuery.tsx b/frontend/src/scenes/notebooks/Nodes/NotebookNodeQuery.tsx index 5bc8456888a68..8391561a29078 100644 --- a/frontend/src/scenes/notebooks/Nodes/NotebookNodeQuery.tsx +++ b/frontend/src/scenes/notebooks/Nodes/NotebookNodeQuery.tsx @@ -3,10 +3,9 @@ import { DataTableNode, InsightVizNode, NodeKind, QuerySchema } from '~/queries/ import { createPostHogWidgetNode } from 'scenes/notebooks/Nodes/NodeWrapper' import { useValues } from 'kea' import { InsightShortId, NotebookNodeType } from '~/types' -import { useJsonNodeState } from './utils' import { useMemo } from 'react' import { notebookNodeLogic } from './notebookNodeLogic' -import { NotebookNodeViewProps, NotebookNodeWidgetSettings } from '../Notebook/utils' +import { NotebookNodeViewProps, NotebookNodeAttributeProperties } from '../Notebook/utils' import clsx from 'clsx' import { IconSettings } from 'lib/lemon-ui/icons' import { urls } from 'scenes/urls' @@ -24,7 +23,7 @@ const DEFAULT_QUERY: QuerySchema = { } const Component = (props: NotebookNodeViewProps): JSX.Element | null => { - const [query] = useJsonNodeState(props.node.attrs, props.updateAttributes, 'query') + const { query } = props.attributes const { expanded } = useValues(notebookNodeLogic) const modifiedQuery = useMemo(() => { @@ -55,7 +54,7 @@ const Component = (props: NotebookNodeViewProps): J
- +
) } @@ -67,11 +66,9 @@ type NotebookNodeQueryAttributes = { export const Settings = ({ attributes, updateAttributes, -}: NotebookNodeWidgetSettings): JSX.Element => { - const [query, setQuery] = useJsonNodeState(attributes, updateAttributes, 'query') - +}: NotebookNodeAttributeProperties): JSX.Element => { const modifiedQuery = useMemo(() => { - const modifiedQuery = { ...query } + const modifiedQuery = { ...attributes.query } if (NodeKind.DataTableNode === modifiedQuery.kind) { // We don't want to show the insights button for now @@ -86,14 +83,19 @@ export const Settings = ({ } return modifiedQuery - }, [query]) + }, [attributes.query]) return (
{ - setQuery({ ...query, source: (t as DataTableNode | InsightVizNode).source } as QuerySchema) + updateAttributes({ + query: { + ...attributes.query, + source: (t as DataTableNode | InsightVizNode).source, + } as QuerySchema, + }) }} readOnly={false} uniqueKey={attributes.nodeId} diff --git a/frontend/src/scenes/notebooks/Nodes/NotebookNodeRecording.tsx b/frontend/src/scenes/notebooks/Nodes/NotebookNodeRecording.tsx index 05cab33e32484..dafa271b98725 100644 --- a/frontend/src/scenes/notebooks/Nodes/NotebookNodeRecording.tsx +++ b/frontend/src/scenes/notebooks/Nodes/NotebookNodeRecording.tsx @@ -16,14 +16,14 @@ import { import { notebookNodeLogic } from './notebookNodeLogic' import { LemonSwitch } from '@posthog/lemon-ui' import { IconSettings } from 'lib/lemon-ui/icons' -import { JSONContent, NotebookNodeViewProps, NotebookNodeWidgetSettings } from '../Notebook/utils' +import { JSONContent, NotebookNodeViewProps, NotebookNodeAttributeProperties } from '../Notebook/utils' const HEIGHT = 500 const MIN_HEIGHT = 400 const Component = (props: NotebookNodeViewProps): JSX.Element => { - const id = props.node.attrs.id - const noInspector: boolean = props.node.attrs.noInspector + const id = props.attributes.id + const noInspector: boolean = props.attributes.noInspector const recordingLogicProps: SessionRecordingPlayerProps = { ...sessionRecordingPlayerProps(id), @@ -58,7 +58,7 @@ const Component = (props: NotebookNodeViewProps export const Settings = ({ attributes, updateAttributes, -}: NotebookNodeWidgetSettings): JSX.Element => { +}: NotebookNodeAttributeProperties): JSX.Element => { return (
): JSX.Element => { - const { id } = props.node.attrs + const { id } = props.attributes const { survey, surveyLoading, hasTargetingFlag } = useValues(surveyLogic({ id })) const { expanded, nextNode } = useValues(notebookNodeLogic) const { insertAfter } = useActions(notebookNodeLogic) diff --git a/frontend/src/scenes/notebooks/Nodes/notebookNodeLogic.ts b/frontend/src/scenes/notebooks/Nodes/notebookNodeLogic.ts index ffc51d5c1280a..ce281db18dc3e 100644 --- a/frontend/src/scenes/notebooks/Nodes/notebookNodeLogic.ts +++ b/frontend/src/scenes/notebooks/Nodes/notebookNodeLogic.ts @@ -15,27 +15,33 @@ import { import type { notebookNodeLogicType } from './notebookNodeLogicType' import { createContext, useContext } from 'react' import { notebookLogicType } from '../Notebook/notebookLogicType' -import { CustomNotebookNodeAttributes, JSONContent, Node, NotebookNodeWidget } from '../Notebook/utils' +import { + CustomNotebookNodeAttributes, + JSONContent, + Node, + NotebookNode, + NotebookNodeAttributeProperties, + NotebookNodeAttributes, + NotebookNodeWidget, +} from '../Notebook/utils' import { NotebookNodeType } from '~/types' import posthog from 'posthog-js' export type NotebookNodeLogicProps = { - node: Node + node: NotebookNode nodeId: string nodeType: NotebookNodeType - nodeAttributes: CustomNotebookNodeAttributes - updateAttributes: (attributes: CustomNotebookNodeAttributes) => void notebookLogic: BuiltLogic getPos: () => number title: string | ((attributes: CustomNotebookNodeAttributes) => Promise) resizeable: boolean | ((attributes: CustomNotebookNodeAttributes) => boolean) widgets: NotebookNodeWidget[] startExpanded: boolean -} +} & NotebookNodeAttributeProperties async function renderTitle( title: NotebookNodeLogicProps['title'], - attrs: NotebookNodeLogicProps['nodeAttributes'] + attrs: NotebookNodeLogicProps['attributes'] ): Promise { if (typeof attrs.title === 'string' && attrs.title.length > 0) { return attrs.title @@ -46,7 +52,7 @@ async function renderTitle( const computeResizeable = ( resizeable: NotebookNodeLogicProps['resizeable'], - attrs: NotebookNodeLogicProps['nodeAttributes'] + attrs: NotebookNodeLogicProps['attributes'] ): boolean => (typeof resizeable === 'function' ? resizeable(attrs) : resizeable) export const notebookNodeLogic = kea([ @@ -59,7 +65,7 @@ export const notebookNodeLogic = kea([ setResizeable: (resizeable: boolean) => ({ resizeable }), insertAfter: (content: JSONContent) => ({ content }), insertAfterLastNodeOfType: (nodeType: string, content: JSONContent) => ({ content, nodeType }), - updateAttributes: (attributes: CustomNotebookNodeAttributes) => ({ attributes }), + updateAttributes: (attributes: Partial>) => ({ attributes }), insertReplayCommentByTimestamp: (timestamp: number, sessionRecordingId: string) => ({ timestamp, sessionRecordingId, @@ -116,7 +122,7 @@ export const notebookNodeLogic = kea([ selectors({ notebookLogic: [(_, p) => [p.notebookLogic], (notebookLogic) => notebookLogic], - nodeAttributes: [(_, p) => [p.nodeAttributes], (nodeAttributes) => nodeAttributes], + nodeAttributes: [(_, p) => [p.attributes], (nodeAttributes) => nodeAttributes], widgets: [(_, p) => [p.widgets], (widgets) => widgets], isShowingWidgets: [ (s, p) => [s.widgetsVisible, p.widgets], @@ -175,9 +181,9 @@ export const notebookNodeLogic = kea([ afterMount(async (logic) => { logic.props.notebookLogic.actions.registerNodeLogic(logic as any) - const renderedTitle = await renderTitle(logic.props.title, logic.props.nodeAttributes) + const renderedTitle = await renderTitle(logic.props.title, logic.props.attributes) logic.actions.setTitle(renderedTitle) - const resizeable = computeResizeable(logic.props.resizeable, logic.props.nodeAttributes) + const resizeable = computeResizeable(logic.props.resizeable, logic.props.attributes) logic.actions.setResizeable(resizeable) logic.actions.updateAttributes({ title: renderedTitle }) }), diff --git a/frontend/src/scenes/notebooks/Nodes/utils.tsx b/frontend/src/scenes/notebooks/Nodes/utils.tsx index 1a845f0ab7c40..c4becf3bd6d23 100644 --- a/frontend/src/scenes/notebooks/Nodes/utils.tsx +++ b/frontend/src/scenes/notebooks/Nodes/utils.tsx @@ -2,28 +2,9 @@ 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' - -export function useJsonNodeState( - attributes: NodeViewProps['node']['attrs'], - updateAttributes: NodeViewProps['updateAttributes'], - key: string -): [T, (value: T) => void] { - let value = attributes[key] - try { - value = typeof value === 'string' ? JSON.parse(value) : value - } catch (e) { - console.error("Couldn't parse query", e) - value = {} - } - - const setValue = (value: any): void => { - updateAttributes({ - [key]: JSON.stringify(value), - }) - } - - return [value, setValue] -} +import { CustomNotebookNodeAttributes, NotebookNodeAttributes } from '../Notebook/utils' +import { useCallback, useMemo, useRef } from 'react' +import { tryJsonParse, uuid } from 'lib/utils' export function createUrlRegex(path: string | RegExp, origin?: string): RegExp { origin = (origin || window.location.origin).replace('.', '\\.') @@ -111,3 +92,48 @@ export function selectFile(options: { contentType: string; multiple: boolean }): input.click() }) } + +export function useSyncedAttributes( + props: NodeViewProps +): [NotebookNodeAttributes, (attrs: Partial>) => void] { + const nodeId = useMemo(() => props.node.attrs.nodeId ?? uuid(), [props.node.attrs.nodeId]) + const previousNodeAttrs = useRef() + const parsedAttrs = useRef>({} as NotebookNodeAttributes) + + if (previousNodeAttrs.current !== props.node.attrs) { + const newParsedAttrs = {} + + Object.keys(props.node.attrs).forEach((key) => { + if (previousNodeAttrs.current?.[key] !== props.node.attrs[key]) { + // If changed, set it whilst trying to parse + newParsedAttrs[key] = tryJsonParse(props.node.attrs[key], props.node.attrs[key]) + } else if (parsedAttrs.current) { + // Otherwise use the old value to preserve object equality + newParsedAttrs[key] = parsedAttrs.current[key] + } + }) + + parsedAttrs.current = newParsedAttrs as NotebookNodeAttributes + parsedAttrs.current.nodeId = nodeId + } + + previousNodeAttrs.current = props.node.attrs + + const updateAttributes = useCallback( + (attrs: Partial>): void => { + // We call the update whilst json stringifying + const stringifiedAttrs = Object.keys(attrs).reduce( + (acc, x) => ({ + ...acc, + [x]: attrs[x] && typeof attrs[x] === 'object' ? JSON.stringify(attrs[x]) : attrs[x], + }), + {} + ) + + props.updateAttributes(stringifiedAttrs) + }, + [props.updateAttributes] + ) + + return [parsedAttrs.current, updateAttributes] +} diff --git a/frontend/src/scenes/notebooks/Notebook/notebookLogic.ts b/frontend/src/scenes/notebooks/Notebook/notebookLogic.ts index 624d3d73a300e..e5f1ac112c284 100644 --- a/frontend/src/scenes/notebooks/Notebook/notebookLogic.ts +++ b/frontend/src/scenes/notebooks/Notebook/notebookLogic.ts @@ -319,7 +319,7 @@ export const notebookLogic = kea([ return ( nodeLogic.props.nodeType === type && attrEntries.every( - ([attr, value]: [string, any]) => nodeLogic.props.node.attrs?.[attr] === value + ([attr, value]: [string, any]) => nodeLogic.props.attributes?.[attr] === value ) ) }) ?? null diff --git a/frontend/src/scenes/notebooks/Notebook/utils.ts b/frontend/src/scenes/notebooks/Notebook/utils.ts index b1038ad4147a3..fb9dc6b92c0dc 100644 --- a/frontend/src/scenes/notebooks/Notebook/utils.ts +++ b/frontend/src/scenes/notebooks/Notebook/utils.ts @@ -28,25 +28,28 @@ export type NotebookNodeAttributes = T & height?: string | number } -type NotebookNode = Omit & { - attrs: NotebookNodeAttributes -} +// NOTE: Pushes users to use the parsed "attributes" instead +export type NotebookNode = Omit -export type NotebookNodeWidgetSettings = { +export type NotebookNodeAttributeProperties = { attributes: NotebookNodeAttributes - updateAttributes: (attributes: Partial) => void + updateAttributes: (attributes: Partial>) => void } -export type NotebookNodeViewProps = Omit & { - node: NotebookNode -} +export type NotebookNodeViewProps = Omit< + NodeViewProps, + 'node' | 'updateAttributes' +> & + NotebookNodeAttributeProperties & { + node: NotebookNode + } export type NotebookNodeWidget = { key: string label: string icon: JSX.Element // using 'any' here shouldn't be necessary but I couldn't figure out how to set a generic on the notebookNodeLogic props - Component: ({ attributes, updateAttributes }: NotebookNodeWidgetSettings) => JSX.Element + Component: ({ attributes, updateAttributes }: NotebookNodeAttributeProperties) => JSX.Element } export interface NotebookEditor { diff --git a/frontend/src/scenes/project-homepage/RecentRecordings.tsx b/frontend/src/scenes/project-homepage/RecentRecordings.tsx index 430861f518d9b..1445a53dd1900 100644 --- a/frontend/src/scenes/project-homepage/RecentRecordings.tsx +++ b/frontend/src/scenes/project-homepage/RecentRecordings.tsx @@ -48,7 +48,7 @@ export function RecordingRow({ recording }: RecordingRowProps): JSX.Element { export function RecentRecordings(): JSX.Element { const { currentTeam } = useValues(teamLogic) - const sessionRecordingsListLogicInstance = sessionRecordingsListLogic({ key: 'projectHomepage' }) + const sessionRecordingsListLogicInstance = sessionRecordingsListLogic({ logicKey: 'projectHomepage' }) const { sessionRecordings, sessionRecordingsResponseLoading } = useValues(sessionRecordingsListLogicInstance) return ( diff --git a/frontend/src/scenes/session-recordings/playlist/SessionRecordingsPlaylist.tsx b/frontend/src/scenes/session-recordings/playlist/SessionRecordingsPlaylist.tsx index 04464fc9a87da..f90ee397989cb 100644 --- a/frontend/src/scenes/session-recordings/playlist/SessionRecordingsPlaylist.tsx +++ b/frontend/src/scenes/session-recordings/playlist/SessionRecordingsPlaylist.tsx @@ -57,19 +57,32 @@ function UnusableEventsWarning(props: { unusableEventsInFilter: string[] }): JSX ) } +export type SessionRecordingsPlaylistProps = SessionRecordingListLogicProps & { + playlistShortId?: string + personUUID?: string + filters?: RecordingFilters + updateSearchParams?: boolean + onFiltersChange?: (filters: RecordingFilters) => void + autoPlay?: boolean + mode?: 'standard' | 'notebook' +} + export function RecordingsLists({ playlistShortId, personUUID, filters: defaultFilters, updateSearchParams, + ...props }: SessionRecordingsPlaylistProps): JSX.Element { - const logicProps = { + const logicProps: SessionRecordingListLogicProps = { + ...props, playlistShortId, personUUID, filters: defaultFilters, updateSearchParams, } const logic = sessionRecordingsListLogic(logicProps) + const { filters, hasNext, @@ -285,33 +298,12 @@ export function RecordingsLists({ ) } -export type SessionRecordingsPlaylistProps = { - playlistShortId?: string - personUUID?: string - filters?: RecordingFilters - updateSearchParams?: boolean - onFiltersChange?: (filters: RecordingFilters) => void - autoPlay?: boolean - mode?: 'standard' | 'notebook' -} - export function SessionRecordingsPlaylist(props: SessionRecordingsPlaylistProps): JSX.Element { - const { - playlistShortId, - personUUID, - filters: defaultFilters, - updateSearchParams, - onFiltersChange, - autoPlay = true, - } = props + const { playlistShortId } = props const logicProps: SessionRecordingListLogicProps = { - playlistShortId, - personUUID, - filters: defaultFilters, - updateSearchParams, - autoPlay, - onFiltersChange, + ...props, + autoPlay: props.autoPlay ?? true, } const logic = sessionRecordingsListLogic(logicProps) const { diff --git a/frontend/src/scenes/session-recordings/playlist/sessionRecordingsListLogic.ts b/frontend/src/scenes/session-recordings/playlist/sessionRecordingsListLogic.ts index 99cb664cebc18..5d44e84618b36 100644 --- a/frontend/src/scenes/session-recordings/playlist/sessionRecordingsListLogic.ts +++ b/frontend/src/scenes/session-recordings/playlist/sessionRecordingsListLogic.ts @@ -157,12 +157,8 @@ export const defaultPageviewPropertyEntityFilter = ( } } -export function generateSessionRecordingListLogicKey(props: SessionRecordingListLogicProps): string { - return `${props.key}-${props.playlistShortId}-${props.personUUID}-${props.updateSearchParams ? '-with-search' : ''}` -} - export interface SessionRecordingListLogicProps { - key?: string + logicKey?: string playlistShortId?: string personUUID?: PersonUUID filters?: RecordingFilters @@ -174,7 +170,12 @@ export interface SessionRecordingListLogicProps { export const sessionRecordingsListLogic = kea([ path((key) => ['scenes', 'session-recordings', 'playlist', 'sessionRecordingsListLogic', key]), props({} as SessionRecordingListLogicProps), - key(generateSessionRecordingListLogicKey), + key( + (props: SessionRecordingListLogicProps) => + `${props.logicKey}-${props.playlistShortId}-${props.personUUID}-${ + props.updateSearchParams ? '-with-search' : '' + }` + ), connect({ actions: [ eventUsageLogic,