Skip to content
Merged
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
2 changes: 1 addition & 1 deletion package-lock.json

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

12 changes: 12 additions & 0 deletions packages/botonic-plugin-flow-builder/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,18 @@ All notable changes to Botonic will be documented in this file.

</details>

## [0.48.2] - 2026-05-07

### Fixed

- [PR-3209](https://github.com/hubtype/botonic/pull/3209):

## [0.48.2] - 2026-04-29

### Fixed

- [PR-3206](https://github.com/hubtype/botonic/pull/3206): Avoid read undefined settings in local

## [0.48.0] - 2026-04-28

### Added
Expand Down
2 changes: 1 addition & 1 deletion packages/botonic-plugin-flow-builder/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@botonic/plugin-flow-builder",
"version": "0.48.1",
"version": "0.48.2",
"main": "./lib/cjs/index.js",
"module": "./lib/esm/index.js",
"description": "Use Flow Builder to show your contents",
Expand Down
9 changes: 3 additions & 6 deletions packages/botonic-plugin-flow-builder/src/action/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {
import React from 'react'

import { FlowAiAgent, type FlowContent } from '../content-fields'
import { filterContents } from '../filters'
import { splitAiAgentContents } from '../utils/ai-agent'
import { getFlowBuilderActionContext } from './context'
import { getContentsByFirstInteraction } from './first-interaction'
Expand Down Expand Up @@ -55,11 +54,9 @@ export class FlowBuilderAction extends React.Component<FlowBuilderActionProps> {
botContext: BotContext,
contents: FlowContent[]
) {
const filteredContents = await filterContents(botContext, contents)

for (const content of filteredContents) {
for (const content of contents) {
if (content instanceof FlowAiAgent) {
const splitContents = splitAiAgentContents(filteredContents)
const splitContents = splitAiAgentContents(contents)
if (!splitContents) {
continue
}
Expand All @@ -70,7 +67,7 @@ export class FlowBuilderAction extends React.Component<FlowBuilderActionProps> {
}
}

return filteredContents
return contents
}

protected getWebchatSettingsParams(botContext: BotContext): {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import type { BotContext } from '@botonic/core'

import {
ACCESS_TOKEN_VARIABLE_KEY,
VARIABLE_PATTERN_GLOBAL,
} from '../constants'
import { ContentFilterExecutor } from '../filters'
import { getFlowBuilderPlugin } from '../utils/get-flow-builder-plugin'
import type {
HtMediaFileLocale,
HtNodeLink,
HtQueueLocale,
HtTextLocale,
HtVideoLocale,
} from './hubtype-fields'
import type { FlowContent } from './index'

export abstract class ContentFieldsBase {
public code: string
Expand All @@ -22,6 +24,22 @@ export abstract class ContentFieldsBase {

abstract processContent(botContext: BotContext): Promise<void>

async filterContent(
botContext: BotContext,
content: FlowContent
): Promise<FlowContent> {
const flowBuilderPlugin = getFlowBuilderPlugin(botContext.plugins)
const contentFilters = flowBuilderPlugin.contentFilters
const contentFilterExecutor = new ContentFilterExecutor({
filters: contentFilters,
})
const filteredContent = await contentFilterExecutor.filter(
botContext,
content
)
return filteredContent
}

static getTextByLocale(locale: string, text: HtTextLocale[]): string {
const result = text.find(t => t.locale === locale)
return result?.message ?? ''
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,6 @@ export class FlowAiAgent extends ContentFieldsBase {
this.aiAgentResponse = aiAgentResponse
await this.trackAiAgentResponse(botContext)
this.messages = aiAgentResponse.messages
await this.messagesToBotonicJSXElements(botContext)
}

return aiAgentResponse
Expand Down Expand Up @@ -196,6 +195,10 @@ export class FlowAiAgent extends ContentFieldsBase {
if (this.messages.length === 0) {
await this.resolveAIAgentResponse(botContext, previousContents)
}
if (this.jsxElements.length === 0) {
await this.filterContent(botContext, this)
await this.messagesToBotonicJSXElements(botContext)
}
return
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export class FlowBotAction extends ContentFieldsBase {
}

async processContent(botContext: BotContext): Promise<void> {
await this.filterContent(botContext, this)
this.doBotAction(botContext)
await this.trackFlow(botContext)
return
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ export class FlowCarousel extends ContentFieldsBase {
}

async processContent(botContext: BotContext): Promise<void> {
await this.filterContent(botContext, this)
await this.trackFlow(botContext)
return
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export class FlowChannelConditional extends ContentFieldsBase {
}

async processContent(botContext: BotContext): Promise<void> {
await this.filterContent(botContext, this)
await this.trackFlow(botContext)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export class FlowCountryConditional extends ContentFieldsBase {
}

async processContent(botContext: BotContext): Promise<void> {
await this.filterContent(botContext, this)
await this.trackFlow(botContext)
return
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ export class FlowCustomConditional extends ContentFieldsBase {
}

async processContent(botContext: BotContext): Promise<void> {
await this.filterContent(botContext, this)
await this.trackFlow(botContext)
return
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export class FlowGoToFlow extends ContentFieldsBase {
}

async processContent(botContext: BotContext): Promise<void> {
await this.filterContent(botContext, this)
await this.trackFlow(botContext)
return
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ export class FlowHandoff extends ContentFieldsBase {
}

async processContent(botContext: BotContext): Promise<void> {
await this.filterContent(botContext, this)
await this.doHandoff(botContext)
return
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export class FlowImage extends ContentFieldsBase {
}

async processContent(botContext: BotContext): Promise<void> {
await this.filterContent(botContext, this)
await this.trackFlow(botContext)
return
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export class FlowKnowledgeBase extends ContentFieldsBase {
}

async processContent(botContext: BotContext): Promise<void> {
await this.filterContent(botContext, this)
await this.trackFlow(botContext)
return
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ export class FlowQueueStatusConditional extends ContentFieldsBase {
}

async processContent(botContext: BotContext): Promise<void> {
await this.filterContent(botContext, this)
await this.trackFlow(botContext)
return
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export class FlowRating extends ContentFieldsBase {
}

async processContent(botContext: BotContext): Promise<void> {
await this.filterContent(botContext, this)
await this.trackFlow(botContext)
return
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ export class FlowText extends ContentFieldsBase {
}

async processContent(botContext: BotContext): Promise<void> {
await this.filterContent(botContext, this)
await this.trackFlow(botContext)
return
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export class FlowVideo extends ContentFieldsBase {
}

async processContent(botContext: BotContext): Promise<void> {
await this.filterContent(botContext, this)
await this.trackFlow(botContext)
return
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ export class FlowWhatsappCtaUrlButtonNode extends ContentFieldsBase {
}

async processContent(botContext: BotContext): Promise<void> {
await this.filterContent(botContext, this)
await this.trackFlow(botContext)
return
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,7 @@ export class FlowWhatsappTemplate extends ContentFieldsBase {
}

async processContent(botContext: BotContext): Promise<void> {
await this.filterContent(botContext, this)
await this.trackFlow(botContext)
return
}
Expand Down
20 changes: 0 additions & 20 deletions packages/botonic-plugin-flow-builder/src/filters/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import type { BotContext } from '@botonic/core'

import type { FlowContent } from '../content-fields'
import type { ContentFilter } from '../types'
import { getFlowBuilderPlugin } from '../utils/get-flow-builder-plugin'

interface ContentFilterExecutorOptions {
filters: ContentFilter[]
Expand All @@ -26,22 +25,3 @@ export class ContentFilterExecutor {
return content
}
}

export async function filterContents(
request: BotContext,
contents: FlowContent[]
): Promise<FlowContent[]> {
const flowBuilderPlugin = getFlowBuilderPlugin(request.plugins)
const contentFilters = flowBuilderPlugin.contentFilters
const contentFilterExecutor = new ContentFilterExecutor({
filters: contentFilters,
})

const filteredContents: FlowContent[] = []
for (const content of contents) {
const filteredContent = await contentFilterExecutor.filter(request, content)
filteredContents.push(filteredContent)
}

return filteredContents
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { INPUT, type InferenceResponse } from '@botonic/core'
import { describe, jest, test } from '@jest/globals'

import { FlowAiAgent, FlowText } from '../../src/content-fields/index'
import { type ContentFilter, ProcessEnvNodeEnvs } from '../../src/types'
// eslint-disable-next-line jest/no-mocks-import
import { mockAiAgentResponse } from '../__mocks__/ai-agent'
// eslint-disable-next-line jest/no-mocks-import
import { mockSmartIntent } from '../__mocks__/smart-intent'
import { trackEventMock } from '../__mocks__/track-event'
import { aiAgentTestFlow } from '../helpers/flows/ai-agent'
import { basicFlow } from '../helpers/flows/basic'
import { createFlowBuilderPluginAndGetContents } from '../helpers/utils'

const removeButtonFilter = jest.fn<ContentFilter>((_request, content) => {
if (content instanceof FlowText) {
content.buttons = content.buttons.filter(
button => button.text !== 'Talk to an agent'
)
}
return content
})

describe('Order of applying filters and tracking', () => {
process.env.NODE_ENV = ProcessEnvNodeEnvs.PRODUCTION

beforeEach(() => {
removeButtonFilter.mockClear()
trackEventMock.mockClear()
mockSmartIntent('Other')
})

test('for a FlowText content, the filter should be applied first, followed by the track', async () => {
const { contents } = await createFlowBuilderPluginAndGetContents({
flowBuilderOptions: {
flow: basicFlow,
trackEvent: trackEventMock,
contentFilters: [removeButtonFilter],
},
requestArgs: {
input: { data: 'Hello', type: INPUT.TEXT },
isFirstInteraction: true,
},
})

expect(removeButtonFilter).toHaveBeenCalledTimes(contents.length)
expect(trackEventMock).toHaveBeenCalledTimes(contents.length)
trackEventMock.mock.invocationCallOrder.forEach(
(trackingCallOrder, callIndex) => {
expect(
removeButtonFilter.mock.invocationCallOrder[callIndex]
).toBeLessThan(trackingCallOrder)
}
)
})
})

describe('For an AiAgent content, the filter should be applied first, followed by the creation of jsx elements', () => {
process.env.NODE_ENV = ProcessEnvNodeEnvs.PRODUCTION

beforeEach(() => {
removeButtonFilter.mockClear()
trackEventMock.mockClear()
mockSmartIntent('Other')
})

test('should be filtered and and then create jsx elements', async () => {
const messagesToBotonicJSXElementsSpy = jest.spyOn(
FlowAiAgent.prototype,
'messagesToBotonicJSXElements'
)
const mockResponse: Partial<InferenceResponse> = {
messages: [
{
type: 'textWithButtons',
content: {
text: 'Text message with buttons generated by the ai agent',
buttons: [{ text: 'Button 1' }, { text: 'Talk to an agent' }],
},
},
],
}

const { contents } = await createFlowBuilderPluginAndGetContents({
flowBuilderOptions: {
flow: aiAgentTestFlow,
getAiAgentResponse: mockAiAgentResponse(mockResponse),
contentFilters: [removeButtonFilter],
},
requestArgs: {
input: {
data: 'How can you help me?',
type: INPUT.TEXT,
},
},
})

expect(contents[0]).toBeInstanceOf(FlowAiAgent)
expect(removeButtonFilter).toHaveBeenCalledTimes(1)
expect(messagesToBotonicJSXElementsSpy).toHaveBeenCalledTimes(1)
expect(removeButtonFilter.mock.invocationCallOrder[0]).toBeLessThan(
messagesToBotonicJSXElementsSpy.mock.invocationCallOrder[0]
)

messagesToBotonicJSXElementsSpy.mockRestore()
})
})
Loading