Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions frontend/src/generated/core/api.schemas.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import '@testing-library/jest-dom'

import { act, cleanup, render, waitFor } from '@testing-library/react'

import { sceneLogic } from 'scenes/sceneLogic'
import { Scene } from 'scenes/sceneTypes'

import { ProductKey } from '~/queries/schema/schema-general'
import { initKeaTests } from '~/test/init'

import { MaybeProductPushWelcome } from './MaybeProductPushWelcome'
import { navPanelProductPushWelcomeLogic } from './navPanelProductPushWelcomeLogic'
import type { PendingProductPushWelcome } from './navPanelProductPushWelcomeVisibility'

const WELCOME: PendingProductPushWelcome = {
campaignId: 'campaign-1',
productKey: ProductKey.PRODUCT_ANALYTICS,
label: 'Product analytics',
text: 'Insights, funnels, trends, and retention.',
}

describe('MaybeProductPushWelcome', () => {
afterEach(() => cleanup())

it('opens the pending welcome automatically when the scene releases its hold', async () => {
initKeaTests()
sceneLogic.mount()
sceneLogic.actions.setExportedScene(
{ component: (): null => null, productKey: ProductKey.PRODUCT_ANALYTICS },
Scene.SavedInsights,
undefined,
{ params: {}, searchParams: {}, hashParams: {} }
)
const logic = navPanelProductPushWelcomeLogic()
logic.mount()
logic.actions.holdWelcome('first-run')
logic.actions.setPendingWelcome(WELCOME)

render(<MaybeProductPushWelcome />)
expect(document.querySelector('[data-attr="product-push-welcome"]')).not.toBeInTheDocument()

act(() => logic.actions.releaseWelcome('first-run'))

await waitFor(() => expect(document.querySelector('[data-attr="product-push-welcome"]')).toBeInTheDocument())
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ function ProductPushWelcome({
const { featureFlags, receivedFeatureFlags } = useValues(featureFlagLogic)
const { searchParams } = useValues(router)
const { status, skipped } = useValues(productSetupStatusLogic({ productKey: welcome.productKey }))
const { welcomeHeld } = useValues(navPanelProductPushWelcomeLogic)
const { openWelcome, closeWelcome } = useActions(navPanelProductPushWelcomeLogic)

const sceneEmptyState = activeExportedScene?.emptyState
Expand All @@ -68,12 +69,12 @@ function ProductPushWelcome({
})

useEffect(() => {
if (!isOpen && ready) {
if (!isOpen && ready && !welcomeHeld) {
openWelcome(welcome)
}
}, [isOpen, ready, welcome, openWelcome])
}, [isOpen, ready, welcomeHeld, welcome, openWelcome])

if (!isOpen) {
if (!isOpen || welcomeHeld) {
return null
}
const config = gatesPushedProduct ? sceneEmptyState?.config : undefined
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import posthog from 'posthog-js'

import { ProductKey } from '~/queries/schema/schema-general'
import { initKeaTests } from '~/test/init'

import { navPanelProductPushWelcomeLogic } from './navPanelProductPushWelcomeLogic'
import type { PendingProductPushWelcome } from './navPanelProductPushWelcomeVisibility'

const WELCOME: PendingProductPushWelcome = {
campaignId: 'campaign-1',
productKey: ProductKey.WORKFLOWS,
label: 'Workflows',
text: 'Message users when it matters.',
}

describe('navPanelProductPushWelcomeLogic', () => {
let logic: ReturnType<typeof navPanelProductPushWelcomeLogic.build>

beforeEach(() => {
initKeaTests()
logic = navPanelProductPushWelcomeLogic()
logic.mount()
})

afterEach(() => {
logic.unmount()
jest.restoreAllMocks()
})

it('keeps the welcome waiting while a surface holds it, and opens it once the surface lets go', () => {
const capture = jest.spyOn(posthog, 'capture')
logic.actions.setPendingWelcome(WELCOME)
logic.actions.holdWelcome('first-run')

logic.actions.openWelcome(WELCOME)

expect(logic.values).toMatchObject({ openFor: null, pending: WELCOME })
expect(capture).not.toHaveBeenCalled()

logic.actions.releaseWelcome('first-run')
logic.actions.openWelcome(WELCOME)

expect(logic.values).toMatchObject({ openFor: WELCOME, pending: null })
expect(capture).toHaveBeenCalledWith('nav panel product push welcome shown', expect.anything())
})

it('puts an open welcome back on hold when a welcoming surface mounts', () => {
logic.actions.setPendingWelcome(WELCOME)
logic.actions.openWelcome(WELCOME)

logic.actions.holdWelcome('first-run')

expect(logic.values).toMatchObject({ openFor: null, pending: WELCOME })

logic.actions.releaseWelcome('first-run')
logic.actions.openWelcome(WELCOME)

expect(logic.values).toMatchObject({ openFor: WELCOME, pending: null })
})
})
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { MakeLogicType, actions, kea, listeners, path, reducers } from 'kea'
import { MakeLogicType, actions, kea, listeners, path, reducers, selectors } from 'kea'
import posthog from 'posthog-js'

import { NAV_PANEL_CARD_TYPE } from './navPanelAdShared'
Expand All @@ -11,24 +11,41 @@ export type ProductPushWelcomeCloseSource = 'start_exploring' | 'modal_close'
export interface navPanelProductPushWelcomeLogicValues {
openFor: PendingProductPushWelcome | null
pending: PendingProductPushWelcome | null
welcomeHeld: boolean
welcomeHolders: string[]
}

// Generated by kea-typegen. Update if you're an agent, ignore if you're human.
export interface navPanelProductPushWelcomeLogicActions {
closeWelcome: (source: ProductPushWelcomeCloseSource) => {
source: ProductPushWelcomeCloseSource
}
holdWelcome: (holder: string) => {
holder: string
}
openWelcome: (welcome: PendingProductPushWelcome) => {
welcome: PendingProductPushWelcome
}
releaseWelcome: (holder: string) => {
holder: string
}
setPendingWelcome: (welcome: PendingProductPushWelcome) => {
welcome: PendingProductPushWelcome
}
}

// Generated by kea-typegen. Update if you're an agent, ignore if you're human.
export interface navPanelProductPushWelcomeLogicMeta {
__keaTypeGenInternalSelectorTypes: {
welcomeHeld: (welcomeHolders: string[]) => boolean
}
}

export type navPanelProductPushWelcomeLogicType = MakeLogicType<
navPanelProductPushWelcomeLogicValues,
navPanelProductPushWelcomeLogicActions
navPanelProductPushWelcomeLogicActions,
Record<string, any>,
navPanelProductPushWelcomeLogicMeta
>

/**
Expand All @@ -42,6 +59,8 @@ export const navPanelProductPushWelcomeLogic = kea<navPanelProductPushWelcomeLog
setPendingWelcome: (welcome: PendingProductPushWelcome) => ({ welcome }),
openWelcome: (welcome: PendingProductPushWelcome) => ({ welcome }),
closeWelcome: (source: ProductPushWelcomeCloseSource) => ({ source }),
holdWelcome: (holder: string) => ({ holder }),
releaseWelcome: (holder: string) => ({ holder }),
}),
reducers({
pending: [
Expand All @@ -61,9 +80,28 @@ export const navPanelProductPushWelcomeLogic = kea<navPanelProductPushWelcomeLog
setPendingWelcome: () => null,
},
],
welcomeHolders: [
[] as string[],
{
holdWelcome: (holders, { holder }) => (holders.includes(holder) ? holders : [...holders, holder]),
releaseWelcome: (holders, { holder }) => holders.filter((held) => held !== holder),
},
],
}),
listeners(({ selectors }) => ({
selectors({
welcomeHeld: [(s) => [s.welcomeHolders], (welcomeHolders: string[]): boolean => welcomeHolders.length > 0],
}),
listeners(({ actions, values, selectors }) => ({
holdWelcome: () => {
if (values.openFor) {
actions.setPendingWelcome(values.openFor)
}
},
openWelcome: ({ welcome }) => {
if (values.welcomeHeld) {
actions.setPendingWelcome(welcome)
return
}
posthog.capture('nav panel product push welcome shown', {
campaign_id: welcome.campaignId,
product_key: welcome.productKey,
Expand Down
13 changes: 8 additions & 5 deletions frontend/src/lib/components/ProductSetup/ProductSetupPopover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { useHogfetti } from 'lib/components/Hogfetti/Hogfetti'
import { SetupTaskId } from 'lib/components/ProductSetup'
import { Popover } from 'lib/lemon-ui/Popover'
import { Tooltip } from 'lib/lemon-ui/Tooltip'
import { FeatureFlagsSet, featureFlagLogic } from 'lib/logic/featureFlagLogic'
import { inStorybook, inStorybookTestRunner } from 'lib/utils/dom'
import { addProductIntent } from 'lib/utils/product-intents'
import { teamLogic } from 'scenes/teamLogic'
Expand Down Expand Up @@ -91,7 +92,8 @@ export function ProductSetupPopover({
}

// Calculate other products with remaining tasks
const otherProductsWithTasks = useOtherProductsWithTasks(selectedProduct, savedOnboardingTasks)
const { featureFlags } = useValues(featureFlagLogic)
const otherProductsWithTasks = useOtherProductsWithTasks(selectedProduct, savedOnboardingTasks, featureFlags)

// Hogfetti celebration
const { trigger: triggerHogfetti, HogfettiComponent } = useHogfetti()
Expand Down Expand Up @@ -852,7 +854,8 @@ function TaskActions({

function useOtherProductsWithTasks(
selectedProduct: ProductKey,
savedOnboardingTasks: Record<string, ActivationTaskStatus>
savedOnboardingTasks: Record<string, ActivationTaskStatus>,
featureFlags: FeatureFlagsSet
): ProductWithTasks[] {
return useMemo(() => {
const currentCategory = productCategoryMap[selectedProduct]
Expand All @@ -861,15 +864,15 @@ function useOtherProductsWithTasks(
if (productKey === selectedProduct) {
return false
}
const tasks = getTasksForProduct(productKey)
const tasks = getTasksForProduct(productKey, featureFlags)
const remainingTasks = tasks.filter((task) => {
const status = savedOnboardingTasks[task.id]
return status !== ActivationTaskStatus.COMPLETED && status !== ActivationTaskStatus.SKIPPED
})
return remainingTasks.length > 0
}).map((productKey) => {
const productConfig = getProductSetupConfig(productKey)
const tasks = getTasksForProduct(productKey)
const tasks = getTasksForProduct(productKey, featureFlags)
const remainingCount = tasks.filter((task) => {
const status = savedOnboardingTasks[task.id]
return status !== ActivationTaskStatus.COMPLETED && status !== ActivationTaskStatus.SKIPPED
Expand All @@ -894,5 +897,5 @@ function useOtherProductsWithTasks(
}
return a.name.localeCompare(b.name)
})
}, [selectedProduct, savedOnboardingTasks])
}, [selectedProduct, savedOnboardingTasks, featureFlags])
}
35 changes: 35 additions & 0 deletions frontend/src/lib/components/ProductSetup/productSetupLogic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { router } from 'kea-router'
import { expectLogic } from 'kea-test-utils'

import { SetupTaskId } from 'lib/components/ProductSetup'
import { FEATURE_FLAGS } from 'lib/constants'
import { featureFlagLogic } from 'lib/logic/featureFlagLogic'
import { teamLogic } from 'scenes/teamLogic'
import { urls } from 'scenes/urls'

Expand Down Expand Up @@ -154,4 +156,37 @@ describe('productSetupLogic', () => {
aiLogic.unmount()
}
})

it.each([
{
firstRun: true,
tasks: [
{ id: SetupTaskId.SendWorkflowTestEmail, locked: false },
{ id: SetupTaskId.LaunchWorkflow, locked: false },
{ id: SetupTaskId.SendFromOwnEmailDomain, locked: false },
],
},
{
firstRun: false,
tasks: [
{ id: SetupTaskId.SetUpFirstWorkflowChannel, locked: false },
{ id: SetupTaskId.CreateFirstWorkflow, locked: false },
{ id: SetupTaskId.ConfigureWorkflowTrigger, locked: false },
{ id: SetupTaskId.AddWorkflowAction, locked: false },
{ id: SetupTaskId.LaunchWorkflow, locked: true },
],
},
])('lists the Workflows quick start tasks with the first run flag on: $firstRun', async ({ firstRun, tasks }) => {
featureFlagLogic.actions.setFeatureFlags([], { [FEATURE_FLAGS.WORKFLOWS_FIRST_RUN]: firstRun })
const workflowsLogic = productSetupLogic({ productKey: ProductKey.WORKFLOWS })
workflowsLogic.mount()
try {
await setTeam({ onboarding_tasks: {} })

const productTasks = workflowsLogic.values.tasksWithState.filter((t) => t.taskType !== 'ai')
expect(productTasks.map((t) => ({ id: t.id, locked: !!t.lockedReason }))).toEqual(tasks)
} finally {
workflowsLogic.unmount()
}
})
})
16 changes: 12 additions & 4 deletions frontend/src/lib/components/ProductSetup/productSetupLogic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { router } from 'kea-router'
import posthog from 'posthog-js'

import { SetupTaskId } from 'lib/components/ProductSetup'
import { FeatureFlagsSet, featureFlagLogic } from 'lib/logic/featureFlagLogic'
import { preflightLogic } from 'lib/logic/preflightLogic'
import { organizationLogic } from 'scenes/organizationLogic'
import { inviteLogic } from 'scenes/settings/organization/inviteLogic'
Expand Down Expand Up @@ -30,6 +31,7 @@ export interface productSetupLogicValues {
optimisticTaskStatuses: Record<string, ActivationTaskStatus | null> // globalSetupLogic
isCurrentOrganizationNew: boolean // organizationLogic
isCloudOrDev: boolean | undefined // preflightLogic
featureFlags: FeatureFlagsSet // featureFlagLogic
hasReverseProxy: boolean | null // reverseProxyCheckerLogic
currentTeam: TeamPublicType | TeamType | null // teamLogic
activeTasks: SetupTaskWithState[]
Expand Down Expand Up @@ -116,7 +118,11 @@ export interface productSetupLogicMeta {
key: ProductKey
__keaTypeGenInternalSelectorTypes: {
productConfig: (productKey: ProductKey) => ProductSetupConfig | null
allTasks: (productKey: ProductKey, isCloudOrDev: boolean | undefined) => SetupTask[]
allTasks: (
productKey: ProductKey,
isCloudOrDev: boolean | undefined,
featureFlags: FeatureFlagsSet
) => SetupTask[]
savedOnboardingTasks: (
currentTeam: TeamPublicType | TeamType | null,
optimisticTaskStatuses: Record<string, ActivationTaskStatus | null>
Expand Down Expand Up @@ -176,6 +182,8 @@ export const productSetupLogic = kea<productSetupLogicType>([
['isCurrentOrganizationNew'],
preflightLogic,
['isCloudOrDev'],
featureFlagLogic,
['featureFlags'],
globalSetupLogic,
['optimisticTaskStatuses'],
],
Expand Down Expand Up @@ -238,9 +246,9 @@ export const productSetupLogic = kea<productSetupLogicType>([
selectors({
productConfig: [(_, p) => [p.productKey], (productKey: ProductKey) => getProductSetupConfig(productKey)],
allTasks: [
(s, p) => [p.productKey, s.isCloudOrDev],
(productKey: ProductKey, isCloudOrDev: boolean | undefined) => {
const tasks = getTasksForProduct(productKey)
(s, p) => [p.productKey, s.isCloudOrDev, s.featureFlags],
(productKey: ProductKey, isCloudOrDev: boolean | undefined, featureFlags: FeatureFlagsSet) => {
const tasks = getTasksForProduct(productKey, featureFlags)
if (!isCloudOrDev) {
return tasks.filter((task) => task.id !== SetupTaskId.SetUpReverseProxy)
}
Expand Down
Loading