Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Notebooks links #16388

Merged
merged 8 commits into from
Jul 10, 2023
Merged

feat: Notebooks links #16388

merged 8 commits into from
Jul 10, 2023

Conversation

daibhin
Copy link
Contributor

@daibhin daibhin commented Jul 5, 2023

Problem

Towards #15680

  • Links currently turn white when selected

Changes

  1. Link pasting

When you paste a link into a notebook it should automatically unfurl
link-pasting

  1. Link behaviour

link-behavior

  • Clicking an external link will redirect you to a new tab
  • Clicking a PostHog link will transition within the same tab but open the notebook in the sidebar view

How did you test this code?

Locally 👀

@daibhin daibhin changed the title Notebooks links v1 feat: Notebooks links Jul 5, 2023
@daibhin daibhin requested a review from pauldambra July 6, 2023 08:08
Copy link
Member

@pauldambra pauldambra left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

only one thing with useCallback (just because life tends to be easier the more we stick in kea land)

@@ -19,7 +19,7 @@ const Component = (props: NodeViewProps): JSX.Element => {
return (
<NodeWrapper
nodeType={NotebookNodeType.FeatureFlag}
title="FeatureFlag"
title="Feature Flag"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😍

@@ -73,6 +73,10 @@
top: 0;
z-index: 1;
}

span::selection {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
span::selection {
// overriding ::selection is necessary here because
// antd makes it invisible otherwise
span::selection {

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thinking its best to capture the context for the ::selection to make life easier for the future traveller

Comment on lines 51 to 64
const [path, pathStart, internal] = useMemo(() => {
const path = href.replace(window.location.origin, '')
const pathStart = path.split('/')[1]?.toLowerCase()
const internal = href.startsWith(window.location.origin)

return [path, ICON_MAP[pathStart] || <IconLink />]
return [path, pathStart, internal]
}, [href])

const handleOnClick = useCallback(() => {
if (internal && !notebookSideBarShown) {
selectNotebook(shortId)
setNotebookSideBarShown(true)
}
}, [internal, shortId, setNotebookSideBarShown, selectNotebook])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should probably move some or all of this to the notebookSidebarLogic

A selector on that logic that returns a function that takes internal and shortId as args would probably be a step.

The selector would be equivalent to useCallback I think because it also only changes when its dependencies change (with the difference being that internal is provided as a simple first step

Copy link
Contributor Author

@daibhin daibhin Jul 6, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the suggestion! My Kea isn't brilliant but I think I implemented what you suggested and tested that everything still works.

One question I had around Kea as I went was whether there would be a difference between:

selectors(({ actions }) => ({
    notebookLinkClicked: [
        (s) => [s.notebookSideBarShown],
        (notebookSideBarShown): ((shortId: string, internal: boolean) => void) => {
            return (shortId: string, internal: boolean) => {
                if (!notebookSideBarShown && internal) {
                    actions.selectNotebook(shortId)
                    actions.setNotebookSideBarShown(true)
                }
            }
       },
    ],
})),

and

selectors(({ values, actions }) => ({
    notebookLinkClicked: [
        () => [],
        (): ((shortId: string, internal: boolean) => void) => {
            return (shortId: string, internal: boolean) => {
                if (!values.notebookSideBarShown && internal) {
                    actions.selectNotebook(shortId)
                    actions.setNotebookSideBarShown(true)
                }
            }
       },
    ],
})),

They both seem to work. I read that a selector is created for each value - is there a preference for referencing the value over the selector in any case?

Moved away from selectors because it was the wrong paradigm to begin with

@daibhin daibhin force-pushed the dn-feat/notebooks-links-v1 branch from 1731647 to 4583e4d Compare July 6, 2023 10:34
notebookLinkClicked: [
(s) => [s.notebookSideBarShown],
(notebookSideBarShown): ((shortId: string, internal: boolean) => void) => {
return (shortId: string, internal: boolean) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah wait... is this bad advice from me I wonder

@Twixes calling actions in Selectors... verboten?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems to be done elsewhere

selectors(({ actions, values, cache }) => ({
contents: [
(s) => [s.insights, s.infiniteInsights, s.insightsLoading, teamLogic.selectors.currentTeamId],
(insights, infiniteInsights, insightsLoading, currentTeamId) => [
{
key: 'insights',
title: 'Insights',
items: infiniteInsights.map(
(insight) =>
insight &&
({
key: insight.short_id,
name: insight.name || insight.derived_name || 'Untitled',
isNamePlaceholder: !insight.name,
url: urls.insightView(insight.short_id),
searchMatch: findSearchTermInItemName(
insight.name || insight.derived_name || '',
values.searchTerm
),
menuItems: (initiateRename) => [
{
items: [
{
to: urls.insightEdit(insight.short_id),
label: 'Edit',
},
{
onClick: () => {
actions.duplicateInsight(insight)
},
label: 'Duplicate',
},
],
},
{
items: [
{
onClick: initiateRename,
label: 'Rename',
keyboardShortcut: ['enter'],
},
{
onClick: () => {
deleteWithUndo({
object: insight,
endpoint: `projects/${currentTeamId}/insights`,
callback: actions.loadInsights,
})
},
status: 'danger',
label: 'Delete insight',
},
],
},
],
onRename: async (newName) => {
const updatedItem = await api.update(
`api/projects/${teamLogic.values.currentTeamId}/insights/${insight.id}`,
{
name: newName,
}
)
insightsModel.actions.renameInsightSuccess(updatedItem)
},
} as BasicListItem)
),
loading: insightsLoading,
remote: {
isItemLoaded: (index) => !!(cache.requestedInsights[index] || infiniteInsights[index]),
loadMoreItems: async (startIndex) => {
for (let i = startIndex; i < startIndex + INSIGHTS_PER_PAGE; i++) {
cache.requestedInsights[i] = true
}
await savedInsightsLogic.actions.setSavedInsightsFilters(
{ page: Math.floor(startIndex / INSIGHTS_PER_PAGE) + 1 },
true,
false
)
},
itemCount: insights.count,
minimumBatchSize: INSIGHTS_PER_PAGE,
},
} as SidebarCategory,
],
],
activeListItemKey: [
(s) => [s.activeScene, s.sceneParams],
(activeScene, sceneParams) => {
return activeScene === Scene.Insight && sceneParams.params.shortId ? sceneParams.params.shortId : null
},
],
// kea-typegen doesn't like selectors without deps, so searchTerm is just for appearances
debounceSearch: [(s) => [s.searchTerm], () => true],
})),

Whether that means it is right or not 🤷

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps there should be a notebookLinkClicked action with an associated listener that then calls the subsequent actions?

Copy link
Collaborator

@Twixes Twixes Jul 6, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think in some cases that may work, but it is verboten. Selectors are part of the pure functions "ecosystem" in Kea, no side effects allowed (along with actions, and reducers). For side effects, reach for listeners and subscriptions

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@daibhin So, the code you linked doesn't actually call the action in the selector, it just returns a value with a callback to an action. That is is völlig erlaubt, since the selector is still pure that way

Copy link
Member

@pauldambra pauldambra left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 sorry for the diversion through options 😊

@daibhin daibhin force-pushed the dn-feat/notebooks-links-v1 branch from 2aadd14 to eda36bd Compare July 6, 2023 14:38
@daibhin daibhin force-pushed the dn-feat/notebooks-links-v1 branch from eda36bd to 369d120 Compare July 6, 2023 16:27
@daibhin daibhin merged commit ecf0180 into master Jul 10, 2023
@daibhin daibhin deleted the dn-feat/notebooks-links-v1 branch July 10, 2023 08:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants