feat(messaging): remove campaign Overview tab in favor of workflow editor (#38284)

This commit is contained in:
Haven
2025-09-18 15:09:24 -05:00
committed by GitHub
parent 52f8677425
commit 985edf83ba
7 changed files with 175 additions and 202 deletions

View File

@@ -1,159 +0,0 @@
import '@xyflow/react/dist/style.css'
import { Form } from 'kea-forms'
import posthog from 'posthog-js'
import { IconLeave, IconPlusSmall, IconTarget } from '@posthog/icons'
import { LemonButton, LemonLabel, LemonTag, LemonTextArea, lemonToast } from '@posthog/lemon-ui'
import { PropertyFilters } from 'lib/components/PropertyFilters/PropertyFilters'
import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types'
import { LemonDivider } from 'lib/lemon-ui/LemonDivider'
import { LemonField } from 'lib/lemon-ui/LemonField'
import { LemonInput } from 'lib/lemon-ui/LemonInput'
import { LemonRadio } from 'lib/lemon-ui/LemonRadio'
import { LemonSelect } from 'lib/lemon-ui/LemonSelect'
import { CampaignLogicProps, campaignLogic } from './campaignLogic'
export function CampaignOverview(props: CampaignLogicProps): JSX.Element {
return (
<div className="flex flex-col gap-4">
<Form id="campaign-overview" logic={campaignLogic} props={props} formKey="campaign" enableFormOnSubmit>
<div className="flex flex-col flex-wrap gap-4 items-start">
<BasicInfoSection />
<ConversionGoalSection />
<ExitConditionSection />
</div>
</Form>
</div>
)
}
function BasicInfoSection(): JSX.Element {
return (
<div className="flex flex-col gap-2 py-2 w-120">
<LemonField name="name" label="Name">
<LemonInput />
</LemonField>
<LemonField name="description" label="Description">
<LemonTextArea placeholder="Help your teammates understand this campaign" />
</LemonField>
</div>
)
}
function ConversionGoalSection(): JSX.Element {
return (
<div className="flex flex-col py-2 w-full">
<div className="flex flex-col">
<span className="flex items-center gap-1">
<IconTarget className="text-lg" />
<span className="text-lg font-semibold">Conversion goal (optional)</span>
</span>
<p className="mb-0">Define what a user must do to be considered converted.</p>
</div>
<LemonDivider />
<div className="flex gap-1 max-w-240">
<div className="flex flex-col flex-2 gap-4">
<LemonField name={['conversion', 'filters']} label="Detect conversion from property changes">
{({ value, onChange }) => (
<PropertyFilters
buttonText="Add property conversion"
propertyFilters={value ?? []}
taxonomicGroupTypes={[
TaxonomicFilterGroupType.PersonProperties,
TaxonomicFilterGroupType.Cohorts,
TaxonomicFilterGroupType.HogQLExpression,
]}
onChange={onChange}
pageKey="campaign-conversion-properties"
hideBehavioralCohorts
/>
)}
</LemonField>
<div className="flex flex-col gap-1">
<LemonLabel>
Detect conversion from events
<LemonTag>Coming soon</LemonTag>
</LemonLabel>
<LemonButton
type="secondary"
size="small"
icon={<IconPlusSmall />}
onClick={() => {
posthog.capture('messaging campaign event conversion clicked')
lemonToast.info('Event targeting coming soon!')
}}
>
Add event conversion
</LemonButton>
</div>
</div>
<LemonDivider vertical />
<div className="flex-1">
<LemonField
name={['conversion', 'window']}
label="Conversion window"
info="How long after entering the campaign should we check for conversion? After this window, users will be considered for conversion."
>
{({ value, onChange }) => (
<LemonSelect
value={value}
onChange={onChange}
options={[
{ value: 24 * 60 * 60, label: '24 hours' },
{ value: 7 * 24 * 60 * 60, label: '7 days' },
{ value: 14 * 24 * 60 * 60, label: '14 days' },
{ value: 30 * 24 * 60 * 60, label: '30 days' },
]}
/>
)}
</LemonField>
</div>
</div>
</div>
)
}
function ExitConditionSection(): JSX.Element {
return (
<div className="flex flex-col flex-1 w-full py-2">
<div className="flex flex-col">
<span className="flex items-center gap-1">
<IconLeave className="text-lg" />
<span className="text-lg font-semibold">Exit condition</span>
</span>
<p className="mb-0">Choose how your users move through the campaign.</p>
</div>
<LemonDivider />
<LemonField name="exit_condition">
{({ value, onChange }) => (
<LemonRadio
value={value}
onChange={onChange}
options={[
{
value: 'exit_only_at_end',
label: 'Exit at end of workflow',
},
{
value: 'exit_on_trigger_not_matched',
label: 'Exit on trigger not matched',
},
{
value: 'exit_on_conversion',
label: 'Exit on conversion',
},
{
value: 'exit_on_trigger_not_matched_or_conversion',
label: 'Exit on trigger not matched or conversion',
},
]}
/>
)}
</LemonField>
</div>
)
}

View File

@@ -12,7 +12,6 @@ import { urls } from 'scenes/urls'
import { SceneContent } from '~/layout/scenes/components/SceneContent'
import { CampaignMetrics } from './CampaignMetrics'
import { CampaignOverview } from './CampaignOverview'
import { CampaignSceneHeader } from './CampaignSceneHeader'
import { CampaignWorkflow } from './CampaignWorkflow'
import { campaignLogic } from './campaignLogic'
@@ -22,7 +21,7 @@ import { renderWorkflowLogMessage } from './logs/log-utils'
export const scene: SceneExport<CampaignSceneLogicProps> = {
component: CampaignScene,
logic: campaignSceneLogic,
paramsToProps: ({ params: { id, tab } }) => ({ id: id || 'new', tab: tab || 'overview' }),
paramsToProps: ({ params: { id, tab } }) => ({ id: id || 'new', tab: tab || 'workflow' }),
}
export function CampaignScene(props: CampaignSceneLogicProps): JSX.Element {
@@ -40,47 +39,52 @@ export function CampaignScene(props: CampaignSceneLogicProps): JSX.Element {
}
const tabs: (LemonTab<CampaignTab> | null)[] = [
{
label: 'Overview',
key: 'overview',
content: <CampaignOverview {...props} />,
},
{
label: 'Workflow',
key: 'workflow',
content: <CampaignWorkflow {...props} />,
},
props.id && props.id !== 'new'
? {
label: 'Logs',
key: 'logs',
content: (
<LogsViewer
sourceType="hog_flow"
sourceId={props.id}
instanceLabel="workflow run"
renderMessage={(m) => renderWorkflowLogMessage(campaign, m)}
/>
),
}
: null,
props.id && props.id !== 'new'
? {
label: 'Metrics',
key: 'metrics',
content: <CampaignMetrics id={props.id} />,
}
: null,
{
label: 'Logs',
key: 'logs',
content: (
<LogsViewer
sourceType="hog_flow"
/**
* If we're rendering tabs, props.id is guaranteed to be
* defined and not "new" (see return statement below)
*/
sourceId={props.id!}
instanceLabel="workflow run"
renderMessage={(m) => renderWorkflowLogMessage(campaign, m)}
/>
),
},
{
label: 'Metrics',
key: 'metrics',
/**
* If we're rendering tabs, props.id is guaranteed to be
* defined and not "new" (see return statement below)
*/
content: <CampaignMetrics id={props.id!} />,
},
]
return (
<SceneContent className="flex flex-col">
<CampaignSceneHeader {...props} />
<LemonTabs
activeKey={currentTab}
onChange={(tab) => router.actions.push(urls.messagingCampaign(props.id ?? 'new', tab))}
tabs={tabs}
/>
{/* Only show Logs and Metrics tabs if the campaign has already been created */}
{!props.id || props.id === 'new' ? (
<CampaignWorkflow {...props} />
) : (
<LemonTabs
activeKey={currentTab}
onChange={(tab) => router.actions.push(urls.messagingCampaign(props.id ?? 'new', tab))}
tabs={tabs}
/>
)}
</SceneContent>
)
}

View File

@@ -12,7 +12,7 @@ import { CampaignSceneLogicProps } from './campaignSceneLogic'
export const CampaignSceneHeader = (props: CampaignSceneLogicProps = {}): JSX.Element => {
const logic = campaignLogic(props)
const { campaign, campaignChanged, isCampaignSubmitting, campaignLoading, campaignHasErrors } = useValues(logic)
const { saveCampaign, submitCampaign, discardChanges } = useActions(logic)
const { saveCampaign, submitCampaign, discardChanges, setCampaignValue } = useActions(logic)
const isSavedCampaign = props.id && props.id !== 'new'
@@ -70,7 +70,16 @@ export const CampaignSceneHeader = (props: CampaignSceneLogicProps = {}): JSX.El
</>
}
/>
<SceneTitleSection name="Messaging" resourceType={{ type: 'messaging' }} />
<SceneTitleSection
name={campaign?.name}
description={campaign?.description}
resourceType={{ type: 'messaging' }}
canEdit
onNameChange={(name) => setCampaignValue('name', name)}
onDescriptionChange={(description) => setCampaignValue('description', description)}
isLoading={campaignLoading}
renameDebounceMs={200}
/>
</>
)
}

View File

@@ -30,7 +30,7 @@ export type TriggerAction = Extract<HogFlowAction, { type: 'trigger' }>
const NEW_CAMPAIGN: HogFlow = {
id: 'new',
name: '',
name: 'New campaign',
actions: [
{
id: TRIGGER_NODE_ID,

View File

@@ -8,7 +8,7 @@ import { Breadcrumb } from '~/types'
import type { campaignSceneLogicType } from './campaignSceneLogicType'
export const CampaignTabs = ['overview', 'workflow', 'logs', 'metrics'] as const
export const CampaignTabs = ['workflow', 'logs', 'metrics'] as const
export type CampaignTab = (typeof CampaignTabs)[number]
export interface CampaignSceneLogicProps {
@@ -24,7 +24,7 @@ export const campaignSceneLogic = kea<campaignSceneLogicType>([
}),
reducers({
currentTab: [
'overview' as CampaignTab,
'workflow' as CampaignTab,
{
setCurrentTab: (_, { tab }) => tab,
},

View File

@@ -1,12 +1,16 @@
import { Node } from '@xyflow/react'
import { useActions, useValues } from 'kea'
import posthog from 'posthog-js'
import { IconBolt, IconWebhooks } from '@posthog/icons'
import { LemonLabel, LemonSelect } from '@posthog/lemon-ui'
import { IconBolt, IconPlusSmall, IconWebhooks } from '@posthog/icons'
import { LemonButton, LemonDivider, LemonLabel, LemonSelect, LemonTag, lemonToast } from '@posthog/lemon-ui'
import { CodeSnippet } from 'lib/components/CodeSnippet'
import { PropertyFilters } from 'lib/components/PropertyFilters/PropertyFilters'
import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types'
import { useFeatureFlag } from 'lib/hooks/useFeatureFlag'
import { LemonField } from 'lib/lemon-ui/LemonField'
import { LemonRadio } from 'lib/lemon-ui/LemonRadio'
import { publicWebhooksHostOrigin } from 'lib/utils/apiHost'
import { campaignLogic } from '../../campaignLogic'
@@ -112,6 +116,11 @@ function StepTriggerConfigurationEvents({
buttonCopy="Add trigger event"
/>
</LemonField.Pure>
<LemonDivider />
<ConversionGoalSection />
<LemonDivider />
<ExitConditionSection />
</>
)
}
@@ -163,3 +172,113 @@ function StepTriggerConfigurationWebhook({
</>
)
}
function ConversionGoalSection(): JSX.Element {
const { setCampaignValue } = useActions(campaignLogic)
const { campaign } = useValues(campaignLogic)
return (
<div className="flex flex-col py-2 w-full">
<span className="text-md font-semibold">Conversion goal (optional)</span>
<p>Define what a user must do to be considered converted.</p>
<div className="flex gap-1 max-w-240">
<div className="flex flex-col flex-2 gap-4">
<LemonField.Pure label="Detect conversion from property changes">
<PropertyFilters
buttonText="Add property conversion"
propertyFilters={campaign.conversion?.filters ?? []}
taxonomicGroupTypes={[
TaxonomicFilterGroupType.PersonProperties,
TaxonomicFilterGroupType.Cohorts,
TaxonomicFilterGroupType.HogQLExpression,
]}
onChange={(filters) => setCampaignValue('conversion', { ...campaign.conversion, filters })}
pageKey="campaign-conversion-properties"
hideBehavioralCohorts
/>
</LemonField.Pure>
<div className="flex flex-col gap-1">
<LemonLabel>
Detect conversion from events
<LemonTag>Coming soon</LemonTag>
</LemonLabel>
<LemonButton
type="secondary"
size="small"
icon={<IconPlusSmall />}
onClick={() => {
posthog.capture('messaging campaign event conversion clicked')
lemonToast.info('Event targeting coming soon!')
}}
>
Add event conversion
</LemonButton>
</div>
</div>
<LemonDivider vertical />
<div className="flex-1">
<LemonField.Pure
label="Conversion window"
info="How long after entering the campaign should we check for conversion? After this window, users will be considered for conversion."
>
<LemonSelect
value={campaign.conversion?.window_minutes}
onChange={(value) =>
setCampaignValue('conversion', {
...campaign.conversion,
window_minutes: value,
})
}
placeholder="No conversion window"
allowClear
options={[
{ value: 24 * 60 * 60, label: '24 hours' },
{ value: 7 * 24 * 60 * 60, label: '7 days' },
{ value: 14 * 24 * 60 * 60, label: '14 days' },
{ value: 30 * 24 * 60 * 60, label: '30 days' },
]}
/>
</LemonField.Pure>
</div>
</div>
</div>
)
}
function ExitConditionSection(): JSX.Element {
const { setCampaignValue } = useActions(campaignLogic)
const { campaign } = useValues(campaignLogic)
return (
<div className="flex flex-col flex-1 w-full py-2">
<span className="text-md font-semibold">Exit condition</span>
<p>Choose how your users move through the campaign.</p>
<LemonField.Pure>
<LemonRadio
value={campaign.exit_condition ?? 'exit_only_at_end'}
onChange={(value) => setCampaignValue('exit_condition', value)}
options={[
{
value: 'exit_only_at_end',
label: 'Exit at end of workflow',
},
{
value: 'exit_on_trigger_not_matched',
label: 'Exit on trigger not matched',
},
{
value: 'exit_on_conversion',
label: 'Exit on conversion',
},
{
value: 'exit_on_trigger_not_matched_or_conversion',
label: 'Exit on trigger not matched or conversion',
},
]}
/>
</LemonField.Pure>
</div>
)
}

View File

@@ -36,12 +36,12 @@ export const manifest: ProductManifest = {
},
redirects: {
'/messaging': '/messaging/campaigns',
'/messaging/campaigns/new': '/messaging/campaigns/new/overview',
'/messaging/campaigns/new': '/messaging/campaigns/new/workflow',
},
urls: {
messaging: (tab?: MessagingSceneTab): string => `/messaging/${tab || 'campaigns'}`,
messagingCampaign: (id: string, tab?: string): string => `/messaging/campaigns/${id}/${tab || 'overview'}`,
messagingCampaignNew: (): string => '/messaging/campaigns/new/overview',
messagingCampaign: (id: string, tab?: string): string => `/messaging/campaigns/${id}/${tab || 'workflow'}`,
messagingCampaignNew: (): string => '/messaging/campaigns/new/workflow',
messagingLibraryMessage: (id: string): string => `/messaging/library/messages/${id}`,
messagingLibraryTemplate: (id?: string): string => `/messaging/library/templates/${id}`,
messagingLibraryTemplateNew: (): string => '/messaging/library/templates/new',