mirror of
https://github.com/BillyOutlast/posthog.git
synced 2026-02-04 03:01:23 +01:00
feat(messaging): remove campaign Overview tab in favor of workflow editor (#38284)
This commit is contained in:
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user