Skip to content
Draft
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Meta, StoryObj } from '@storybook/react'

import { EnableWorkflowPrompt } from './EnableWorkflowPrompt'

const meta: Meta<typeof EnableWorkflowPrompt> = {
title: 'Products/Workflows/Enable workflow prompt',
component: EnableWorkflowPrompt,
args: {
onConfirm: () => {},
onCancel: () => {},
},
parameters: {
layout: 'fullscreen',
testOptions: { waitForSelector: '[data-attr="workflow-enable-prompt-confirm"]' },
},
}
export default meta

type Story = StoryObj<typeof EnableWorkflowPrompt>

export const OneUnverifiedDomain: Story = { args: { domains: ['example.com'] } }

export const SeveralUnverifiedDomains: Story = { args: { domains: ['example.com', 'mail.example.org'] } }
53 changes: 53 additions & 0 deletions products/workflows/frontend/Workflows/EnableWorkflowPrompt.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { LemonButton, LemonModal } from '@posthog/lemon-ui'

const domainList = new Intl.ListFormat('en', { type: 'conjunction' })

export interface EnableWorkflowPromptProps {
domains: string[] | null
onConfirm: () => void
onCancel: () => void
loading?: boolean
}

export function EnableWorkflowPrompt({
domains,
onConfirm,
onCancel,
loading,
}: EnableWorkflowPromptProps): JSX.Element {
const several = (domains?.length ?? 0) > 1
return (
<LemonModal
isOpen={!!domains}
onClose={loading ? undefined : onCancel}
title="Enable this workflow?"
width={480}
footer={
<>
<LemonButton
type="secondary"
onClick={onCancel}
disabledReason={loading ? 'Enabling the workflow' : undefined}
data-attr="workflow-enable-prompt-cancel"
>
Cancel
</LemonButton>
<LemonButton
type="primary"
onClick={onConfirm}
loading={loading}
data-attr="workflow-enable-prompt-confirm"
>
Enable
</LemonButton>
</>
}
>
<p className="mb-0">
<strong className="break-all">{domainList.format(domains ?? [])}</strong>{' '}
{several ? "aren't verified yet" : "isn't verified yet"}. The workflow turns on now and starts sending
once {several ? 'these domains are' : 'the domain is'} verified.
</p>
</LemonModal>
)
}
14 changes: 7 additions & 7 deletions products/workflows/frontend/Workflows/WorkflowActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import { WorkflowSceneLogicProps } from './workflowSceneLogic'
import { parseWorkflowTriggerPrefill, TRIGGER_PREFILL_PARAM } from './workflowTriggerPrefill'

export function WorkflowActions(props: WorkflowSceneLogicProps): JSX.Element {

Check warning on line 22 in products/workflows/frontend/Workflows/WorkflowActions.tsx

View workflow job for this annotation

GitHub Actions / Frontend formatting

lint:complexity

`WorkflowActions` has cyclomatic complexity 42 (warn >10)
const {
workflow,
hasUnsavedChanges,
Expand All @@ -33,7 +33,7 @@
showDraftActions,
statusToggleDisabledReason,
} = useValues(workflowLogic)
const { saveWorkflowPartial, submitWorkflow, discardChanges, publishDraft, discardDraft } =
const { saveWorkflowPartial, enableWorkflow, submitWorkflow, discardChanges, publishDraft, discardDraft } =
useActions(workflowLogic)
const { searchParams } = useValues(router)
const editTemplateId = searchParams.editTemplateId as string | undefined
Expand Down Expand Up @@ -82,9 +82,9 @@
// reveals the per-field messages, and saveWorkflowPartial
// guards the actual enable with a toast.
onClick={() =>
saveWorkflowPartial({
status: workflow?.status === 'draft' ? 'active' : 'draft',
})
workflow?.status === 'draft'
? enableWorkflow()
: saveWorkflowPartial({ status: 'draft' })
}
disabledReason={statusToggleDisabledReason}
className="transition-colors duration-300 ease-in-out"
Expand All @@ -105,9 +105,9 @@
// reveals the per-field messages, and saveWorkflowPartial
// guards the actual enable with a toast.
onClick={() =>
saveWorkflowPartial({
status: workflow?.status === 'draft' ? 'active' : 'draft',
})
workflow?.status === 'draft'
? enableWorkflow()
: saveWorkflowPartial({ status: 'draft' })
}
size="small"
disabledReason={statusToggleDisabledReason}
Expand Down
2 changes: 2 additions & 0 deletions products/workflows/frontend/Workflows/WorkflowScene.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
import { WorkflowRevisions } from './WorkflowRevisions'
import { WorkflowSceneHeader } from './WorkflowSceneHeader'
import { WorkflowSceneLogicProps, WorkflowTab, workflowSceneLogic } from './workflowSceneLogic'
import { WorkflowStartedSendingNotice } from './WorkflowStartedSendingNotice'
import { TRIGGER_PREFILL_PARAM } from './workflowTriggerPrefill'

export const scene: SceneExport<WorkflowSceneLogicProps> = {
Expand All @@ -65,7 +66,7 @@
productKey: ProductKey.WORKFLOWS,
}

export function WorkflowScene(props: WorkflowSceneLogicProps): JSX.Element {

Check warning on line 69 in products/workflows/frontend/Workflows/WorkflowScene.tsx

View workflow job for this annotation

GitHub Actions / Frontend formatting

lint:complexity

`WorkflowScene` has cyclomatic complexity 33 (warn >10)
const workflowSceneProps: WorkflowSceneLogicProps = {
id: props.id || 'new',
tab: props.tab || 'workflow',
Expand Down Expand Up @@ -254,6 +255,7 @@
<WorkflowSceneHeader {...props} />
<WorkflowEmailPauseBanner />
{isSavedWorkflow && <WorkflowBroadcastMoveBanner id={props.id!} />}
<WorkflowStartedSendingNotice />
{!isSavedWorkflow && <WorkflowAiEmailDraftStatus {...workflowProps} />}
{suggestionsAvailable && isSavedWorkflow && <WorkflowSuggestionsNotice id={props.id!} />}
{/* Only show Logs and Metrics tabs if the workflow has already been created */}
Expand Down
27 changes: 24 additions & 3 deletions products/workflows/frontend/Workflows/WorkflowSceneHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import { SceneTitleSection } from '~/layout/scenes/components/SceneTitleSection'
import { AccessControlLevel, AccessControlResourceType } from '~/types'

import { EnableWorkflowPrompt } from './EnableWorkflowPrompt'
import { workflowSuggestionsAvailabilityLogic } from './suggestions/workflowSuggestionsAvailabilityLogic'
import { WorkflowSuggestionsMenuItem } from './suggestions/WorkflowSuggestionsMenuItem'
import { SaveAsTemplateModal } from './templates/SaveAsTemplateModal'
Expand All @@ -31,10 +32,24 @@
import { WorkflowSceneLogicProps } from './workflowSceneLogic'
import { WorkflowScenePanel } from './WorkflowScenePanel'

export const WorkflowSceneHeader = (props: WorkflowSceneLogicProps = {}): JSX.Element => {

Check warning on line 35 in products/workflows/frontend/Workflows/WorkflowSceneHeader.tsx

View workflow job for this annotation

GitHub Actions / Frontend formatting

lint:complexity

`WorkflowSceneHeader` has cyclomatic complexity 12 (warn >10)
const { workflow, hasUnsavedChanges, hasStagedDraft, workflowLoading, workflowUserAccessLevel } =
useValues(workflowLogic)
const { setWorkflowValue, duplicate, archiveWorkflow, publishDraft, discardDraft } = useActions(workflowLogic)
const {
workflow,
hasUnsavedChanges,
hasStagedDraft,
workflowLoading,
workflowUserAccessLevel,
enablePromptDomains,
} = useValues(workflowLogic)
const {
setWorkflowValue,
duplicate,
archiveWorkflow,
publishDraft,
discardDraft,
confirmEnablePrompt,
cancelEnablePrompt,
} = useActions(workflowLogic)
const { searchParams } = useValues(router)
const editTemplateId = searchParams.editTemplateId as string | undefined
const { showSaveAsTemplateModal } = useActions(workflowTemplateLogic({ ...props, editTemplateId }))
Expand All @@ -48,6 +63,12 @@
return (
<>
<SaveAsTemplateModal {...props} editTemplateId={editTemplateId} />
<EnableWorkflowPrompt
domains={enablePromptDomains}
onConfirm={confirmEnablePrompt}
onCancel={cancelEnablePrompt}
loading={workflowLoading}
/>
{sceneMenuBarEnabled && isSavedWorkflow && (
<SceneMenuBar>
<SceneMenuBarMenu label="File" dataAttr="workflow-menubar-file">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import '@testing-library/jest-dom'

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

import { FEATURE_FLAGS } from 'lib/constants'
import { featureFlagLogic } from 'lib/logic/featureFlagLogic'

import { useMocks } from '~/mocks/jest'
import { initKeaTests } from '~/test/init'

import { workflowsEmailSuspensionLogic } from '../workflowsEmailSuspensionLogic'
import { HogFlow } from './hogflows/types'
import { workflowLogic } from './workflowLogic'
import { WorkflowStartedSendingNotice } from './WorkflowStartedSendingNotice'

const WORKFLOW_ID = 'wf-started-sending-1'

const WENT_LIVE_AFTER_WAITING: HogFlow = {
id: WORKFLOW_ID,
name: 'Welcome',
actions: [],
edges: [],
conversion: { filters: [] },
exit_condition: 'exit_only_at_end',
version: 1,
status: 'active',
team_id: 1,
trigger: { type: 'event', filters: {} } as HogFlow['trigger'],
created_at: '2026-05-01T00:00:00.000Z',
updated_at: '2026-05-03T08:30:00.000Z',
enable_requested_at: '2026-05-02T10:00:00Z',
went_live_at: '2026-05-03T08:30:00Z',
}

describe('WorkflowStartedSendingNotice', () => {
afterEach(() => {
cleanup()
})

it.each([
{ name: 'shows once the project can send email', status: 200, suspended: false, shown: true },
{ name: 'stays hidden while the project cannot send email', status: 200, suspended: true, shown: false },
{ name: 'stays hidden when the sending status fails to load', status: 500, suspended: false, shown: false },
])('$name', async ({ status, suspended, shown }) => {
let releaseStatus!: () => void
const statusReleased = new Promise<void>((resolve) => {
releaseStatus = resolve
})
useMocks({
get: {
'/api/environments/:team_id/hog_flows/:id/': async ({ params }) => {
if (params.id !== 'email_sending_suspension') {
return [200, WENT_LIVE_AFTER_WAITING]
}
await statusReleased
return [status, { email_sending_suspended: suspended }]
},
'/api/projects/:team_id/hog_function_templates/': { results: [], count: 0 },
},
})
initKeaTests()
featureFlagLogic.mount()
featureFlagLogic.actions.setFeatureFlags([], { [FEATURE_FLAGS.WORKFLOWS_EMAIL_DOMAIN_WIZARD]: true })
const logic = workflowLogic({ id: WORKFLOW_ID })
logic.mount()
await act(async () => {
await logic.asyncActions.loadWorkflow()
})

render(
<Provider>
<BindLogic logic={workflowLogic} props={{ id: WORKFLOW_ID }}>
<WorkflowStartedSendingNotice />
</BindLogic>
</Provider>
)
expect(screen.queryByText('Started sending.')).not.toBeInTheDocument()

releaseStatus()
await waitFor(() => expect(workflowsEmailSuspensionLogic.values.suspensionStatusLoading).toBe(false))
expect(screen.queryByText('Started sending.') !== null).toBe(shown)
logic.unmount()
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { useActions, useValues } from 'kea'

import { LemonBanner } from '@posthog/lemon-ui'

import { workflowsEmailSuspensionLogic } from '../workflowsEmailSuspensionLogic'
import { workflowLogic } from './workflowLogic'

export function WorkflowStartedSendingNotice(): JSX.Element | null {
const { showStartedSendingNotice } = useValues(workflowLogic)

// Mount the project's sending status only when a notice is due, so other editor visits skip the request.
return showStartedSendingNotice ? <StartedSendingBanner /> : null
}

function StartedSendingBanner(): JSX.Element | null {
const { dismissStartedSendingNotice } = useActions(workflowLogic)
const { suspensionStatus } = useValues(workflowsEmailSuspensionLogic)

if (!suspensionStatus || suspensionStatus.email_sending_suspended) {
return null
}

return (
<LemonBanner
type="success"
action={{
children: 'Dismiss',
onClick: dismissStartedSendingNotice,
'data-attr': 'workflow-started-sending-dismiss',
}}
>
<strong>Started sending.</strong> This workflow was waiting for its email domain and is live now.
</LemonBanner>
)
}
11 changes: 11 additions & 0 deletions products/workflows/frontend/Workflows/WorkflowsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import { AutomationEmptyState } from '../setupGuide/AutomationEmptyState'
import { AutomationSuggestionBanner } from '../setupGuide/AutomationSuggestionBanner'
import { MessagingSetupReminderBanner } from '../setupGuide/MessagingSetupReminderBanner'
import { EnableWorkflowPrompt } from './EnableWorkflowPrompt'
import { getHogFlowStep } from './hogflows/steps/HogFlowSteps'
import { HogFlow } from './hogflows/types'
import { workflowSuggestionsAvailabilityLogic } from './suggestions/workflowSuggestionsAvailabilityLogic'
Expand Down Expand Up @@ -121,7 +122,7 @@
)
}

export function WorkflowsTable(): JSX.Element {

Check warning on line 125 in products/workflows/frontend/Workflows/WorkflowsTable.tsx

View workflow job for this annotation

GitHub Actions / Frontend formatting

lint:complexity

`WorkflowsTable` has cyclomatic complexity 14 (warn >10)
const { featureFlags } = useValues(featureFlagLogic)
const overviewTabEnabled = !!featureFlags[FEATURE_FLAGS.WORKFLOWS_OVERVIEW_TAB]
const { suggestionsAvailable } = useValues(workflowSuggestionsAvailabilityLogic)
Expand All @@ -137,6 +138,8 @@
selectedArchivedCount,
localWorkflowTags,
tags: allTags,
enablePrompt,
enablePromptSaving,
} = useValues(logic)
// Only an unfiltered Automations tab is truly empty. With a search or filter, the plain message fits.
const showAutomationEmptyState =
Expand All @@ -162,6 +165,8 @@
updateWorkflowTags,
setTagsFilter,
loadTagsIfNeeded,
confirmEnablePrompt,
cancelEnablePrompt,
} = useActions(logic)

useOnMountEffect(() => {
Expand Down Expand Up @@ -214,7 +219,7 @@
title: 'Name',
key: 'name',
sorter: (a, b) => (a.name || '').localeCompare(b.name || ''),
render: (_, item) => {

Check warning on line 222 in products/workflows/frontend/Workflows/WorkflowsTable.tsx

View workflow job for this annotation

GitHub Actions / Frontend formatting

lint:complexity

`render` has cyclomatic complexity 12 (warn >10)
const stepMatches = findMatchingWorkflowSteps(item, filters.search)
return (
<div className="min-w-48">
Expand Down Expand Up @@ -546,6 +551,12 @@
emptyState={showAutomationEmptyState ? <AutomationEmptyState /> : 'No workflows matching filters'}
/>
<AutomationSuggestionBanner />
<EnableWorkflowPrompt
domains={enablePrompt?.domains ?? null}
onConfirm={confirmEnablePrompt}
onCancel={cancelEnablePrompt}
loading={enablePromptSaving}
/>
</>
</div>
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { HogFlowApi } from '../generated/api.schemas'

type WithUnverifiedSenders = Partial<Pick<HogFlowApi, 'unverified_email_senders'>>

export function unverifiedSenderDomains(workflow: WithUnverifiedSenders | null | undefined): string[] {
return [...new Set((workflow?.unverified_email_senders ?? []).map((sender) => sender.domain))]
}
Loading
Loading