@botonic/plugin-flow-builder 0.58.0-alpha.1 → 0.58.0-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/lib/cjs/action/contents-result.d.ts +5 -0
  2. package/lib/cjs/action/contents-result.js +3 -0
  3. package/lib/cjs/action/contents-result.js.map +1 -0
  4. package/lib/cjs/action/first-interaction.js +1 -1
  5. package/lib/cjs/action/first-interaction.js.map +1 -1
  6. package/lib/cjs/action/get-contents.d.ts +3 -2
  7. package/lib/cjs/action/get-contents.js +13 -6
  8. package/lib/cjs/action/get-contents.js.map +1 -1
  9. package/lib/cjs/action/index.d.ts +2 -0
  10. package/lib/cjs/action/index.js +16 -6
  11. package/lib/cjs/action/index.js.map +1 -1
  12. package/lib/cjs/action/payload.d.ts +2 -2
  13. package/lib/cjs/action/payload.js +12 -28
  14. package/lib/cjs/action/payload.js.map +1 -1
  15. package/lib/cjs/action/push-flow-resolver.d.ts +19 -0
  16. package/lib/cjs/action/push-flow-resolver.js +52 -0
  17. package/lib/cjs/action/push-flow-resolver.js.map +1 -0
  18. package/lib/cjs/api.d.ts +1 -1
  19. package/lib/cjs/api.js +7 -2
  20. package/lib/cjs/api.js.map +1 -1
  21. package/lib/cjs/content-fields/flow-do-nothing.js +1 -1
  22. package/lib/cjs/content-fields/flow-do-nothing.js.map +1 -1
  23. package/lib/cjs/content-fields/flow-update-variables.js +5 -9
  24. package/lib/cjs/content-fields/flow-update-variables.js.map +1 -1
  25. package/lib/esm/action/contents-result.d.ts +5 -0
  26. package/lib/esm/action/contents-result.js +3 -0
  27. package/lib/esm/action/contents-result.js.map +1 -0
  28. package/lib/esm/action/first-interaction.js +1 -1
  29. package/lib/esm/action/first-interaction.js.map +1 -1
  30. package/lib/esm/action/get-contents.d.ts +3 -2
  31. package/lib/esm/action/get-contents.js +13 -6
  32. package/lib/esm/action/get-contents.js.map +1 -1
  33. package/lib/esm/action/index.d.ts +2 -0
  34. package/lib/esm/action/index.js +16 -6
  35. package/lib/esm/action/index.js.map +1 -1
  36. package/lib/esm/action/payload.d.ts +2 -2
  37. package/lib/esm/action/payload.js +12 -28
  38. package/lib/esm/action/payload.js.map +1 -1
  39. package/lib/esm/action/push-flow-resolver.d.ts +19 -0
  40. package/lib/esm/action/push-flow-resolver.js +52 -0
  41. package/lib/esm/action/push-flow-resolver.js.map +1 -0
  42. package/lib/esm/api.d.ts +1 -1
  43. package/lib/esm/api.js +7 -2
  44. package/lib/esm/api.js.map +1 -1
  45. package/lib/esm/content-fields/flow-do-nothing.js +1 -1
  46. package/lib/esm/content-fields/flow-do-nothing.js.map +1 -1
  47. package/lib/esm/content-fields/flow-update-variables.js +5 -9
  48. package/lib/esm/content-fields/flow-update-variables.js.map +1 -1
  49. package/package.json +2 -2
  50. package/src/action/contents-result.ts +6 -0
  51. package/src/action/first-interaction.ts +7 -6
  52. package/src/action/get-contents.ts +17 -8
  53. package/src/action/index.tsx +40 -6
  54. package/src/action/payload.ts +16 -46
  55. package/src/action/push-flow-resolver.ts +91 -0
  56. package/src/api.ts +13 -3
  57. package/src/content-fields/flow-do-nothing.tsx +1 -1
  58. package/src/content-fields/flow-update-variables.tsx +10 -8
@@ -1,5 +1,6 @@
1
1
  import { type BotContext, isDev, isWebchat } from '@botonic/core'
2
2
  import {
3
+ CampaignMessageAttributesProvider,
3
4
  Multichannel,
4
5
  RequestContext,
5
6
  WebchatSettings,
@@ -21,6 +22,7 @@ import { getContents } from './get-contents'
21
22
 
22
23
  export type FlowBuilderActionProps = {
23
24
  contents: FlowContent[]
25
+ campaignFirstMessageId?: string
24
26
  webchatSettingsParams?: WebchatSettingsProps
25
27
  }
26
28
 
@@ -46,14 +48,20 @@ export class FlowBuilderAction extends React.Component<FlowBuilderActionProps> {
46
48
  botContext: BotContext,
47
49
  contentID?: string
48
50
  ): Promise<FlowBuilderActionProps> {
49
- const contents = await getContents(botContext, contentID)
51
+ const { contents, campaignFirstMessageId } = await getContents(
52
+ botContext,
53
+ contentID
54
+ )
50
55
 
51
56
  const contentsToRender = await FlowBuilderAction.prepareContentsToRender(
52
57
  botContext,
53
58
  contents
54
59
  )
55
60
 
56
- return { contents: contentsToRender }
61
+ return {
62
+ contents: contentsToRender,
63
+ campaignFirstMessageId,
64
+ }
57
65
  }
58
66
 
59
67
  static async prepareContentsToRender(
@@ -137,7 +145,7 @@ export class FlowBuilderAction extends React.Component<FlowBuilderActionProps> {
137
145
  }
138
146
 
139
147
  render(): JSX.Element | JSX.Element[] {
140
- const { contents } = this.props
148
+ const { contents, campaignFirstMessageId } = this.props
141
149
  const botContext = this.context as BotContext
142
150
  const { shouldSendWebchatSettings, webchatSettingsParams } =
143
151
  this.getWebchatSettingsParams(botContext)
@@ -147,15 +155,37 @@ export class FlowBuilderAction extends React.Component<FlowBuilderActionProps> {
147
155
  {shouldSendWebchatSettings && (
148
156
  <WebchatSettings {...webchatSettingsParams} />
149
157
  )}
150
- {contents.map(content => content.toBotonic(botContext))}
158
+ {FlowBuilderAction.renderContents(
159
+ contents,
160
+ botContext,
161
+ campaignFirstMessageId
162
+ )}
151
163
  </>
152
164
  )
153
165
  }
166
+
167
+ protected static renderContents(
168
+ contents: FlowContent[],
169
+ botContext: BotContext,
170
+ campaignFirstMessageId?: string
171
+ ): JSX.Element | JSX.Element[] {
172
+ const rendered = contents.map(content => content.toBotonic(botContext))
173
+
174
+ if (!campaignFirstMessageId) {
175
+ return rendered
176
+ }
177
+
178
+ return (
179
+ <CampaignMessageAttributesProvider campaignId={campaignFirstMessageId}>
180
+ {rendered}
181
+ </CampaignMessageAttributesProvider>
182
+ )
183
+ }
154
184
  }
155
185
 
156
186
  export class FlowBuilderMultichannelAction extends FlowBuilderAction {
157
187
  render(): JSX.Element | JSX.Element[] {
158
- const { contents } = this.props
188
+ const { contents, campaignFirstMessageId } = this.props
159
189
  const botContext = this.context as BotContext
160
190
  const { shouldSendWebchatSettings, webchatSettingsParams } =
161
191
  this.getWebchatSettingsParams(botContext)
@@ -165,7 +195,11 @@ export class FlowBuilderMultichannelAction extends FlowBuilderAction {
165
195
  {shouldSendWebchatSettings && (
166
196
  <WebchatSettings {...webchatSettingsParams} />
167
197
  )}
168
- {contents.map(content => content.toBotonic(botContext))}
198
+ {FlowBuilderAction.renderContents(
199
+ contents,
200
+ botContext,
201
+ campaignFirstMessageId
202
+ )}
169
203
  </Multichannel>
170
204
  )
171
205
  }
@@ -1,25 +1,22 @@
1
1
  import { EventAction, type EventFeedback, storeCaseRating } from '@botonic/core'
2
2
  import { v7 as uuid } from 'uuid'
3
-
4
- import {
5
- AGENT_RATING_PAYLOAD,
6
- PUSH_FLOW_PAYLOAD,
7
- SEPARATOR,
8
- } from '../constants'
9
- import type { FlowContent } from '../content-fields'
3
+ import { AGENT_RATING_PAYLOAD, PUSH_FLOW_PAYLOAD } from '../constants'
10
4
  import type { HtNodeWithContent } from '../content-fields/hubtype-fields'
11
5
  import { trackEvent } from '../tracking'
6
+ import type { FlowBuilderContentsResult } from './contents-result'
12
7
  import type { FlowBuilderContext } from './context'
8
+ import { PushFlowResolver } from './push-flow-resolver'
13
9
 
14
10
  export async function getContentsByPayload(
15
11
  context: FlowBuilderContext
16
- ): Promise<FlowContent[]> {
12
+ ): Promise<FlowBuilderContentsResult> {
17
13
  const { cmsApi, flowBuilderPlugin, request, contentID } = context
18
14
  if (request.input.payload?.startsWith(AGENT_RATING_PAYLOAD)) {
19
15
  return await resolveRatingPayload(context)
20
16
  }
21
17
  if (request.input.payload?.startsWith(PUSH_FLOW_PAYLOAD)) {
22
- return await resolvePushFlowPayload(context)
18
+ const pushFlow = new PushFlowResolver(context)
19
+ return await pushFlow.getContents()
23
20
  }
24
21
 
25
22
  const id = contentID
@@ -28,19 +25,21 @@ export async function getContentsByPayload(
28
25
  const targetNode = id ? cmsApi.getNodeById<HtNodeWithContent>(id) : undefined
29
26
 
30
27
  if (targetNode) {
31
- return await flowBuilderPlugin.getContentsByNode(targetNode)
28
+ return {
29
+ contents: await flowBuilderPlugin.getContentsByNode(targetNode),
30
+ }
32
31
  }
33
32
 
34
- return []
33
+ return { contents: [] }
35
34
  }
36
35
 
37
36
  async function resolveRatingPayload(
38
37
  context: FlowBuilderContext
39
- ): Promise<FlowContent[]> {
38
+ ): Promise<FlowBuilderContentsResult> {
40
39
  const { cmsApi, flowBuilderPlugin, request } = context
41
40
 
42
41
  if (!request.input.payload) {
43
- return []
42
+ return { contents: [] }
44
43
  }
45
44
 
46
45
  const { target, text, value, possibleOptions, possibleValues } =
@@ -66,39 +65,10 @@ async function resolveRatingPayload(
66
65
  : undefined
67
66
 
68
67
  if (targetNode) {
69
- return await flowBuilderPlugin.getContentsByNode(targetNode)
70
- }
71
-
72
- return []
73
- }
74
-
75
- async function resolvePushFlowPayload(
76
- context: FlowBuilderContext
77
- ): Promise<FlowContent[]> {
78
- const { cmsApi, flowBuilderPlugin, request } = context
79
-
80
- if (!request.input.payload) {
81
- return []
82
- }
83
-
84
- const parts = request.input.payload.split(SEPARATOR)
85
- const pushFlowId = parts.length > 1 ? parts[1] : ''
86
-
87
- if (!pushFlowId) {
88
- return []
89
- }
90
-
91
- try {
92
- const pushFlowNode =
93
- cmsApi.getNodeByCampaignId<HtNodeWithContent>(pushFlowId)
94
-
95
- if (!pushFlowNode) {
96
- return []
68
+ return {
69
+ contents: await flowBuilderPlugin.getContentsByNode(targetNode),
97
70
  }
98
-
99
- return await flowBuilderPlugin.getContentsByNode(pushFlowNode)
100
- } catch {
101
- // Campaign not found - return empty to trigger fallback
102
- return []
103
71
  }
72
+
73
+ return { contents: [] }
104
74
  }
@@ -0,0 +1,91 @@
1
+ import {
2
+ type BotContext,
3
+ BotonicInputContextType,
4
+ getInputContextOfType,
5
+ } from '@botonic/core'
6
+ import type { FlowBuilderApi } from '../api'
7
+ import type { HtNodeWithContent } from '../content-fields/hubtype-fields'
8
+ import type BotonicPluginFlowBuilder from '../index'
9
+ import type { FlowBuilderContentsResult } from './contents-result'
10
+ import type { FlowBuilderContext } from './context'
11
+
12
+ export enum PushFlowErrorCode {
13
+ NoCampaignContext = 'NoCampaignContext',
14
+ NoCampaignStartNode = 'NoCampaignStartNode',
15
+ NoCampaignFollowUpNode = 'NoCampaignFollowUpNode',
16
+ }
17
+
18
+ export class PushFlowError extends Error {
19
+ readonly code: PushFlowErrorCode
20
+
21
+ constructor(code: PushFlowErrorCode, reason: string) {
22
+ super(`push-flow failed: ${reason}`)
23
+ this.code = code
24
+ this.name = 'PushFlowError'
25
+ }
26
+ }
27
+
28
+ export class PushFlowResolver {
29
+ private botContext: BotContext
30
+ private cmsApi: FlowBuilderApi
31
+ private flowBuilderPlugin: BotonicPluginFlowBuilder
32
+
33
+ constructor(context: FlowBuilderContext) {
34
+ this.botContext = context.request
35
+ this.cmsApi = context.cmsApi
36
+ this.flowBuilderPlugin = context.flowBuilderPlugin
37
+ }
38
+
39
+ async getContents(): Promise<FlowBuilderContentsResult> {
40
+ const campaignContext = getInputContextOfType(
41
+ this.botContext.input.context,
42
+ BotonicInputContextType.PushFlow
43
+ )
44
+
45
+ if (!campaignContext) {
46
+ throw new PushFlowError(
47
+ PushFlowErrorCode.NoCampaignContext,
48
+ 'No campaign context found in input context'
49
+ )
50
+ }
51
+
52
+ const {
53
+ campaign_id: campaignId,
54
+ campaign_flow_id: campaignFlowId,
55
+ campaign_follow_up_id: campaignFollowUpId,
56
+ } = campaignContext
57
+
58
+ const campaignStartNode = this.getCampaignStartNode(campaignFlowId)
59
+ const followUpNode = campaignFollowUpId
60
+ ? this.cmsApi.getNodeById<HtNodeWithContent>(campaignFollowUpId)
61
+ : undefined
62
+
63
+ if (campaignFollowUpId && !followUpNode) {
64
+ throw new PushFlowError(
65
+ PushFlowErrorCode.NoCampaignFollowUpNode,
66
+ 'No campaign follow up node found'
67
+ )
68
+ }
69
+
70
+ return {
71
+ contents: await this.flowBuilderPlugin.getContentsByNode(
72
+ followUpNode ?? campaignStartNode
73
+ ),
74
+ campaignFirstMessageId: campaignId,
75
+ }
76
+ }
77
+
78
+ private getCampaignStartNode(campaignId: string): HtNodeWithContent {
79
+ const campaignStartNode =
80
+ this.cmsApi.getNodeByCampaignId<HtNodeWithContent>(campaignId)
81
+
82
+ if (!campaignStartNode) {
83
+ throw new PushFlowError(
84
+ PushFlowErrorCode.NoCampaignStartNode,
85
+ 'No campaign start node found'
86
+ )
87
+ }
88
+
89
+ return campaignStartNode
90
+ }
91
+ }
package/src/api.ts CHANGED
@@ -82,12 +82,22 @@ export class FlowBuilderApi {
82
82
  return this.getNodeById<HtNodeWithContent>(subFlow.start_node_id)
83
83
  }
84
84
 
85
- getNodeByCampaignId<T extends HtNodeComponent>(id: string): T {
85
+ getNodeByCampaignId<T extends HtNodeComponent>(id: string): T | undefined {
86
86
  const campaign = this.flow.campaigns.find(campaign => campaign.id === id)
87
87
  if (!campaign) {
88
- throw Error(`Campaign with id: '${id}' not found`)
88
+ return undefined
89
89
  }
90
- return this.getNodeById<T>(campaign.start_node_id)
90
+
91
+ const node = this.flow.nodes.find(
92
+ flowNode => flowNode.id === campaign.start_node_id
93
+ )
94
+
95
+ if (!node) {
96
+ console.error(`Node with id: '${campaign.start_node_id}' not found`)
97
+ return undefined
98
+ }
99
+
100
+ return node as T
91
101
  }
92
102
 
93
103
  getNodeById<T extends HtNodeComponent>(id: string): T {
@@ -8,7 +8,7 @@ export class FlowDoNothing extends ContentFieldsBase {
8
8
  static fromHubtypeCMS(cmsDoNothing: HtDoNothingNode): FlowDoNothing {
9
9
  const newDoNothing = new FlowDoNothing(cmsDoNothing.id)
10
10
  newDoNothing.code = cmsDoNothing.code
11
- newDoNothing.followUp = cmsDoNothing.follow_up
11
+ newDoNothing.followUp = undefined
12
12
 
13
13
  return newDoNothing
14
14
  }
@@ -41,8 +41,12 @@ export class FlowUpdateVariables extends ContentFieldsBase {
41
41
  value: string | number | boolean | undefined
42
42
  }[] = []
43
43
 
44
- for (const [keyPath, value] of Object.entries(this.setVariables)) {
44
+ const updateVariable = (
45
+ keyPath: string,
46
+ value: string | number | boolean | undefined
47
+ ) => {
45
48
  const fieldName = this.getContactInfoFieldName(keyPath)
49
+
46
50
  if (fieldName) {
47
51
  this.updateSessionContactInfoField(botContext, fieldName, value)
48
52
  contactInfoUpdates.push({ fieldName, value })
@@ -51,14 +55,12 @@ export class FlowUpdateVariables extends ContentFieldsBase {
51
55
  }
52
56
  }
53
57
 
58
+ for (const [keyPath, value] of Object.entries(this.setVariables)) {
59
+ updateVariable(keyPath, value)
60
+ }
61
+
54
62
  for (const keyPath of this.unsetVariables) {
55
- const fieldName = this.getContactInfoFieldName(keyPath)
56
- if (fieldName) {
57
- this.updateSessionContactInfoField(botContext, fieldName, undefined)
58
- contactInfoUpdates.push({ fieldName, value: undefined })
59
- } else {
60
- this.setSessionValueFromKeyPath(botContext, keyPath, undefined)
61
- }
63
+ updateVariable(keyPath, undefined)
62
64
  }
63
65
 
64
66
  if (contactInfoUpdates.length === 0) {