Skip to content

Commit

Permalink
feat: Generic AddToNotebook button (#17122)
Browse files Browse the repository at this point in the history
* More things

* Added more buttons

* Update UI snapshots for `chromium` (1)

* Update UI snapshots for `webkit` (2)

* Update UI snapshots for `chromium` (1)

* Update UI snapshots for `chromium` (1)

* Update UI snapshots for `chromium` (1)

* fix

* fix connect

* Update UI snapshots for `chromium` (1)

* Update UI snapshots for `webkit` (2)

---------

Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Paul D'Ambra <[email protected]>
  • Loading branch information
3 people authored Aug 24, 2023
1 parent 5767998 commit 254aefc
Show file tree
Hide file tree
Showing 15 changed files with 315 additions and 292 deletions.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 4 additions & 3 deletions frontend/src/models/notebooksModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ export const notebooksModel = kea<notebooksModelType>([
title?: string,
location: NotebookTarget = NotebookTarget.Auto,
content?: JSONContent[],
onCreate?: (notebook: NotebookType) => void
onCreate?: (notebook: BuiltLogic<notebookLogicType>) => void
) => ({
title,
location,
Expand Down Expand Up @@ -104,13 +104,14 @@ export const notebooksModel = kea<notebooksModelType>([
content: defaultNotebookContent(title, content),
})

openNotebook(notebook.short_id, location, 'end')
openNotebook(notebook.short_id, location, 'end', (logic) => {
onCreate?.(logic)
})

posthog.capture(`notebook created`, {
short_id: notebook.short_id,
})

onCreate?.(notebook)
return [notebook, ...values.notebooks]
},

Expand Down
9 changes: 9 additions & 0 deletions frontend/src/scenes/feature-flags/FeatureFlag.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
FeatureFlagType,
ReplayTabs,
FeatureFlagGroupType,
NotebookNodeType,
} from '~/types'
import { Link } from 'lib/lemon-ui/Link'
import { LemonButton } from 'lib/lemon-ui/LemonButton'
Expand Down Expand Up @@ -64,6 +65,7 @@ import { PostHogFeature } from 'posthog-js/react'
import { concatWithPunctuation } from 'scenes/insights/utils'
import { LemonTab, LemonTabs } from 'lib/lemon-ui/LemonTabs'
import { FeatureFlagReleaseConditions } from './FeatureFlagReleaseConditions'
import { NotebookAddButton } from 'scenes/notebooks/NotebookAddButton/NotebookAddButton'

export const scene: SceneExport = {
component: FeatureFlag,
Expand Down Expand Up @@ -512,6 +514,13 @@ export function FeatureFlag({ id }: { id?: string } = {}): JSX.Element {
buttons={
<>
<div className="flex items-center gap-2 mb-2">
<NotebookAddButton
resource={{
type: NotebookNodeType.FeatureFlag,
attrs: { id: featureFlag.id },
}}
type="secondary"
/>
{featureFlags[FEATURE_FLAGS.RECORDINGS_ON_FEATURE_FLAGS] && (
<>
<LemonButton
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/scenes/notebooks/Notebook/notebookLogic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,10 @@ export const notebookLogic = kea<notebookLogicType>([
props({} as NotebookLogicProps),
path((key) => ['scenes', 'notebooks', 'Notebook', 'notebookLogic', key]),
key(({ shortId }) => shortId),
connect({
connect(() => ({
values: [notebooksModel, ['scratchpadNotebook', 'notebookTemplates']],
actions: [notebooksModel, ['receiveNotebookUpdate']],
}),
})),
actions({
setEditor: (editor: NotebookEditor) => ({ editor }),
editorIsReady: true,
Expand Down
2 changes: 0 additions & 2 deletions frontend/src/scenes/notebooks/Notebook/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,8 @@ import { Node as PMNode } from '@tiptap/pm/model'
import { NodeViewProps } from '@tiptap/react'
import { NotebookNodeType, NotebookNodeWidgetSettings } from '~/types'

/* eslint-disable @typescript-eslint/no-empty-interface */
export interface Node extends PMNode {}
export interface JSONContent extends TTJSONContent {}
/* eslint-enable @typescript-eslint/no-empty-interface */

export {
ChainedCommands as EditorCommands,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { ComponentMeta, ComponentStory } from '@storybook/react'
import { NotebookCommentButton } from 'scenes/notebooks/NotebookCommentButton/NotebookCommentButton'
import { NotebookAddButton } from 'scenes/notebooks/NotebookAddButton/NotebookAddButton'
import { useStorybookMocks } from '~/mocks/browser'
import { NotebookNodeType } from '~/types'

export default {
title: 'Scenes-App/Notebooks/Components/Notebook Comment Button',
component: NotebookCommentButton,
} as ComponentMeta<typeof NotebookCommentButton>
component: NotebookAddButton,
} as ComponentMeta<typeof NotebookAddButton>

const Template: ComponentStory<typeof NotebookCommentButton> = (props) => {
const Template: ComponentStory<typeof NotebookAddButton> = (props) => {
useStorybookMocks({
get: {
'/api/projects/:team_id/notebooks/': (req, res, ctx) => {
Expand Down Expand Up @@ -51,31 +52,27 @@ const Template: ComponentStory<typeof NotebookCommentButton> = (props) => {
return (
// the button has its dropdown showing and so needs a container that will include the pop-over
<div className={'min-h-100'}>
<NotebookCommentButton
sessionRecordingId={props.sessionRecordingId}
getCurrentPlayerTime={() => 0}
visible={true}
/>
<NotebookAddButton resource={props.resource} />
</div>
)
}

export const Default = Template.bind({})
Default.args = {
sessionRecordingId: '123',
resource: { type: NotebookNodeType.Recording, attrs: { id: '123' } },
}

export const WithSlowNetworkResponse = Template.bind({})
WithSlowNetworkResponse.args = {
sessionRecordingId: 'very_slow',
resource: { type: NotebookNodeType.Recording, attrs: { id: 'very_slow' } },
}

export const WithNoExistingContainingNotebooks = Template.bind({})
WithNoExistingContainingNotebooks.args = {
sessionRecordingId: 'not_already_contained',
resource: { type: NotebookNodeType.Recording, attrs: { id: 'not_already_contained' } },
}

export const WithNoNotebooks = Template.bind({})
WithNoExistingContainingNotebooks.args = {
sessionRecordingId: 'there_are_no_notebooks',
resource: { type: NotebookNodeType.Recording, attrs: { id: 'there_are_no_notebooks' } },
}
221 changes: 221 additions & 0 deletions frontend/src/scenes/notebooks/NotebookAddButton/NotebookAddButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
import { LemonButton, LemonButtonProps } from 'lib/lemon-ui/LemonButton'

import { IconJournalPlus, IconPlus, IconWithCount } from 'lib/lemon-ui/icons'
import {
NotebookAddButtonLogicProps,
notebookAddButtonLogic,
} from 'scenes/notebooks/NotebookAddButton/notebookAddButtonLogic'
import { BindLogic, BuiltLogic, useActions, useValues } from 'kea'
import { LemonMenuProps } from 'lib/lemon-ui/LemonMenu/LemonMenu'
import { dayjs } from 'lib/dayjs'
import { NotebookListItemType, NotebookTarget } from '~/types'
import { notebooksModel, openNotebook } from '~/models/notebooksModel'
import { useNotebookNode } from 'scenes/notebooks/Nodes/notebookNodeLogic'
import { Popover } from 'lib/lemon-ui/Popover'
import { LemonInput } from 'lib/lemon-ui/LemonInput/LemonInput'
import { LemonDivider } from 'lib/lemon-ui/LemonDivider'
import { notebookLogicType } from '../Notebook/notebookLogicType'
import { notebookNodeLogicType } from '../Nodes/notebookNodeLogicType'
import { FlaggedFeature } from 'lib/components/FlaggedFeature'
import { FEATURE_FLAGS } from 'lib/constants'

type NotebookAddButtonProps = NotebookAddButtonLogicProps &
Omit<LemonButtonProps, 'onClick'> &
Pick<LemonMenuProps, 'visible'> & {
newNotebookTitle?: string
onNotebookOpened?: (
notebookLogic: BuiltLogic<notebookLogicType>,
nodeLogic?: BuiltLogic<notebookNodeLogicType>
) => void
onClick?: () => void
}

function NotebooksChoiceList(props: {
notebooks: NotebookListItemType[]
emptyState: string
onClick: (notebookShortId: NotebookListItemType['short_id']) => void
}): JSX.Element {
return (
<div>
{props.notebooks.length === 0 ? (
<div className={'px-2 py-1'}>{props.emptyState}</div>
) : (
props.notebooks.map((notebook, i) => {
return (
<LemonButton key={i} fullWidth onClick={() => props.onClick(notebook.short_id)}>
{notebook.title || `Untitled (${notebook.short_id})`}
</LemonButton>
)
})
)}
</div>
)
}

function NotebooksChoicePopoverBody(props: NotebookAddButtonProps): JSX.Element {
const { containingNotebooks, containingNotebooksLoading, allNotebooks, allNotebooksLoading, searchQuery } =
useValues(notebookAddButtonLogic)
const { setShowPopover } = useActions(notebookAddButtonLogic)

const openAndAddToNotebook = async (notebookShortId: string, exists: boolean): Promise<void> => {
await openNotebook(notebookShortId, NotebookTarget.Popover, null, (theNotebookLogic) => {
if (!exists) {
theNotebookLogic.actions.insertAfterLastNode([props.resource])
}
props.onNotebookOpened?.(theNotebookLogic)
})
}

if (allNotebooks.length === 0 && containingNotebooks.length === 0) {
return (
<div className={'px-2 py-1 flex flex-row items-center space-x-1'}>
{allNotebooksLoading || containingNotebooksLoading ? (
'Loading...'
) : searchQuery.length ? (
<>No matching notebooks</>
) : (
<>You have no notebooks</>
)}
</div>
)
}

return (
<>
{containingNotebooks.length ? (
<>
<h5>Continue in</h5>
<NotebooksChoiceList
notebooks={containingNotebooks.filter((notebook) => {
// notebook comment logic doesn't know anything about backend filtering 🤔
return (
searchQuery.length === 0 ||
notebook.title?.toLowerCase().includes(searchQuery.toLowerCase())
)
})}
emptyState={searchQuery.length ? 'No matching notebooks' : 'Not already in any notebooks'}
onClick={async (notebookShortId) => {
setShowPopover(false)
await openAndAddToNotebook(notebookShortId, true)
}}
/>
</>
) : null}
{allNotebooks.length > containingNotebooks.length && (
<>
<h5>Add to</h5>
<NotebooksChoiceList
notebooks={allNotebooks.filter((notebook) => {
// TODO follow-up on filtering after https://github.com/PostHog/posthog/pull/17027
const isInExisting = containingNotebooks.some(
(containingNotebook) => containingNotebook.short_id === notebook.short_id
)
return (
!isInExisting &&
(searchQuery.length === 0 ||
notebook.title?.toLowerCase().includes(searchQuery.toLowerCase()))
)
})}
emptyState={searchQuery.length ? 'No matching notebooks' : "You don't have any notebooks"}
onClick={async (notebookShortId) => {
setShowPopover(false)
await openAndAddToNotebook(notebookShortId, false)
}}
/>
</>
)}
</>
)
}

function NotebookAddButtonPopover({ ...props }: NotebookAddButtonProps): JSX.Element {
const { resource, newNotebookTitle, children } = props
const logic = notebookAddButtonLogic(props)
const { showPopover, notebooksLoading, containingNotebooks, searchQuery } = useValues(logic)
const { setShowPopover, setSearchQuery, loadContainingNotebooks } = useActions(logic)
const { createNotebook } = useActions(notebooksModel)

const openNewNotebook = (): void => {
const title = newNotebookTitle ?? `Notes ${dayjs().format('DD/MM')}`

createNotebook(title, NotebookTarget.Popover, [resource], (theNotebookLogic) => {
props.onNotebookOpened?.(theNotebookLogic)
loadContainingNotebooks()
})

setShowPopover(false)
}

return (
<IconWithCount count={containingNotebooks.length ?? 0} showZero={false}>
<Popover
visible={!!showPopover}
onClickOutside={() => {
setShowPopover(false)
}}
actionable
overlay={
<div className="space-y-2 max-w-160 flex flex-col">
<LemonInput
type="search"
placeholder="Search notebooks..."
value={searchQuery}
onChange={(s) => setSearchQuery(s)}
fullWidth
/>
<LemonDivider className="my-1" />
<div>
<BindLogic logic={notebookAddButtonLogic} props={props}>
<NotebooksChoicePopoverBody {...props} />
</BindLogic>
</div>
<LemonDivider className="my-1" />
<LemonButton fullWidth icon={<IconPlus />} onClick={openNewNotebook}>
New notebook
</LemonButton>
</div>
}
>
<LemonButton
icon={<IconJournalPlus />}
sideIcon={null}
{...props}
active={showPopover}
loading={notebooksLoading}
onClick={() => {
props.onClick?.()
setShowPopover(!showPopover)
}}
data-attr={'notebooks-add-button'}
>
{children ?? 'Add to notebook'}
</LemonButton>
</Popover>
</IconWithCount>
)
}

export function NotebookAddButton({ ...props }: NotebookAddButtonProps): JSX.Element {
// if nodeLogic is available then the button is on a resource that _is already and currently in a notebook_
const nodeLogic = useNotebookNode()

return (
<FlaggedFeature flag={FEATURE_FLAGS.NOTEBOOKS} match>
{nodeLogic ? (
<LemonButton
icon={<IconJournalPlus />}
data-attr={'notebooks-add-button-in-a-notebook'}
{...props}
onClick={() => {
props.onClick?.()
props.onNotebookOpened?.(nodeLogic.props.notebookLogic, nodeLogic)
}}
>
{props.children ?? 'Add to notebook'}
</LemonButton>
) : (
<NotebookAddButtonPopover {...props} />
)}
</FlaggedFeature>
)
}
Loading

0 comments on commit 254aefc

Please sign in to comment.