@teleporthq/teleport-plugin-next-workflows 0.45.9 → 0.45.14

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.
@@ -19,7 +19,7 @@ import {
19
19
  redactServerNodeConfig,
20
20
  } from './segment-splitter'
21
21
  import { isFireAndForgetSegment } from './await-result'
22
- import { collectSegmentStateKeys } from './segment-context-needs'
22
+ import { ALL_STATE_KEYS, collectSegmentStateKeys } from './segment-context-needs'
23
23
  import {
24
24
  ADMIN_CUSTOM_NODES_MODULE,
25
25
  callsCustomNodeTransitively,
@@ -29,7 +29,12 @@ import { WorkflowExecutionEnv } from './types'
29
29
  import { getAPIRouteFileName, hasStreamingAINode } from './api-route-generator'
30
30
  import { REALTIME_TRIGGER_TYPES, REALTIME_NODE_TYPES } from './graph-utils'
31
31
  import { neutraliseIsLoggedInGates } from './is-logged-in-gate'
32
- import { TRACK_LEAD_SOURCE, formControlPropertyReads } from './trigger-generator'
32
+ import {
33
+ TRACK_LEAD_SOURCE,
34
+ formControlPropertyReads,
35
+ intervalControlListenerSource,
36
+ intervalControlTeardownSource,
37
+ } from './trigger-generator'
33
38
  import { elementVisibleObserverCode } from './element-visible-trigger'
34
39
  import { restoreControlledSelectValue } from './controlled-select'
35
40
  import { resolveScrollPoint } from './scroll-points'
@@ -157,6 +162,58 @@ const hasNodeTypeTransitively = (
157
162
  return false
158
163
  }
159
164
 
165
+ const reachableNodes = (
166
+ nodes: UIDLWorkflowNode[],
167
+ customNodes: Record<string, UIDLCustomWorkflowNode> | undefined,
168
+ visited: Set<string>
169
+ ): UIDLWorkflowNode[] =>
170
+ nodes.reduce((reached: UIDLWorkflowNode[], node) => {
171
+ reached.push(node)
172
+ const customNodeId = ((node.config || {}) as Record<string, unknown>).customNodeId
173
+ if (
174
+ node.type === 'general-custom-node' &&
175
+ customNodes &&
176
+ typeof customNodeId === 'string' &&
177
+ !visited.has(customNodeId)
178
+ ) {
179
+ visited.add(customNodeId)
180
+ const customNode = customNodes[customNodeId]
181
+ if (customNode && Array.isArray(customNode.nodes)) {
182
+ reached.push(...reachableNodes(customNode.nodes, customNodes, visited))
183
+ }
184
+ }
185
+ return reached
186
+ }, [])
187
+
188
+ /* Whether the nodes, or a custom node they call, read a global state. There
189
+ are four ways to: a `state-get-global-state` node; a `{{state.<name>}}`
190
+ token; a context reference that walks into the state values by name; and an
191
+ expression that names the state values as a whole, which counts when it
192
+ also names one of the global states. The last three read by name, so they
193
+ only count for a name the project declares as a global state. */
194
+ const readsGlobalStateTransitively = (
195
+ nodes: UIDLWorkflowNode[],
196
+ customNodes: Record<string, UIDLCustomWorkflowNode> | undefined,
197
+ globalStateNames: string[]
198
+ ): boolean => {
199
+ if (hasNodeTypeTransitively(nodes, 'state-get-global-state', customNodes, new Set<string>())) {
200
+ return true
201
+ }
202
+ if (globalStateNames.length === 0) {
203
+ return false
204
+ }
205
+ const stateKeys = collectSegmentStateKeys(nodes, customNodes)
206
+ if (stateKeys !== ALL_STATE_KEYS) {
207
+ return stateKeys.some((key) => globalStateNames.includes(key))
208
+ }
209
+ const configs = JSON.stringify(
210
+ reachableNodes(nodes, customNodes, new Set<string>()).map((node) => node.config || {})
211
+ )
212
+ return globalStateNames.some((name) =>
213
+ new RegExp(`(^|[^\\w$])${name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}($|[^\\w$])`).test(configs)
214
+ )
215
+ }
216
+
160
217
  // State names that live in GlobalContext (useGlobalContext) rather than in the
161
218
  // per-page or GlobalStateProvider registry. When a workflow reads or writes
162
219
  // any of these via state-{get,update}-global-state, the page must bridge the
@@ -432,11 +489,21 @@ export const createNextWorkflowPlugin: ComponentPluginFactory<WorkflowPluginConf
432
489
  )
433
490
  )
434
491
  const hasGlobalStateChangeTriggers = globalStateChangeWorkflows.length > 0
435
- const needsGlobalState = hasGlobalStateUpdateNodes || hasGlobalStateChangeTriggers
436
492
  const globalStateDefs = (options.globalStateDefinitions || {}) as Record<
437
493
  string,
438
494
  UIDLGlobalStateDefinition
439
495
  >
496
+ // A workflow that only READS a global state needs the same wiring as one
497
+ // that writes it: every read resolves against the state values this file
498
+ // hands the workflow runtime, and a global state is among them only when
499
+ // the file takes it from `useGlobalState()`. Without it the read is
500
+ // undefined, so a condition on a global switch never holds.
501
+ const globalStateNames = Object.values(globalStateDefs).map((def) => def.name)
502
+ const hasGlobalStateReads = activeWorkflows.some((wf) =>
503
+ readsGlobalStateTransitively(wf.nodes, workflows.customNodes, globalStateNames)
504
+ )
505
+ const needsGlobalState =
506
+ hasGlobalStateUpdateNodes || hasGlobalStateChangeTriggers || hasGlobalStateReads
440
507
 
441
508
  const hasRealtimeUsage = activeWorkflows.some(
442
509
  (wf) =>
@@ -1912,7 +1979,20 @@ ${globalStateChangeHandlerLines.join('\n')}
1912
1979
  `
1913
1980
  }
1914
1981
 
1915
- const generateElementHandler = (et: ElementTriggerInfo): string => {
1982
+ /* The bound element's `data-*` attributes, as trigger fields a workflow reads
1983
+ by name: `data-tab-index` is `dataTabIndex`, the name the static HTML export
1984
+ resolves from the same attribute. Values are the attribute strings. The
1985
+ result is the object the trigger's own fields are then assigned onto, so no
1986
+ attribute can replace one of them. */
1987
+ const datasetTriggerFieldsSource = (element: string): string =>
1988
+ `Object.keys((${element} && ${element}.dataset) || {}).reduce(function(fields, key) { fields['data' + key.charAt(0).toUpperCase() + key.slice(1)] = ${element}.dataset[key]; return fields; }, {})`
1989
+
1990
+ /* The submitted fields are spread FIRST so a field literally named `formData`,
1991
+ `formId`, `element` or `timestamp` can never shadow the reserved keys. */
1992
+ const formTriggerContextSource = (elementId: string): string =>
1993
+ `Object.assign({}, formData, { fields: formData, formData: formData, formId: '${elementId}', triggerElement: event.target, element: event.target, timestamp: Date.now() })`
1994
+
1995
+ export const generateElementHandler = (et: ElementTriggerInfo): string => {
1916
1996
  const safeId = et.workflow.id.replace(/[^a-zA-Z0-9]/g, '_')
1917
1997
  const config = et.triggerConfig
1918
1998
  const elementId = et.elementId
@@ -1936,9 +2016,17 @@ const generateElementHandler = (et: ElementTriggerInfo): string => {
1936
2016
  // the bound element when the user clicks a child — e.g. the payment
1937
2017
  // provider row sets `data-provider-name` on the row div, but a click
1938
2018
  // on the inner label span would otherwise resolve to an empty dataset.
2019
+ // A debounced run starts after the event has finished dispatching, when
2020
+ // `currentTarget` is already null, so the bound element is read before
2021
+ // the timer is set.
2022
+ const boundElement = debounce
2023
+ ? '__bound || event.target'
2024
+ : 'event.currentTarget || event.target'
1939
2025
  body +=
1940
- ` const __te = event.currentTarget || event.target;\n` +
1941
- ` const triggerContext = { elementId: '${elementId}', triggerElement: __te, element: __te, timestamp: Date.now(), clientX: event.clientX, clientY: event.clientY };\n` +
2026
+ ` const __te = ${boundElement};\n` +
2027
+ ` const triggerContext = Object.assign(${datasetTriggerFieldsSource(
2028
+ '__te'
2029
+ )}, { elementId: '${elementId}', triggerElement: __te, element: __te, timestamp: Date.now(), clientX: event.clientX, clientY: event.clientY });\n` +
1942
2030
  ` return __execWf(__wfConfig_${safeId}, triggerContext, __wfServerUrls_${safeId});`
1943
2031
 
1944
2032
  if (debounce) {
@@ -1946,6 +2034,7 @@ const generateElementHandler = (et: ElementTriggerInfo): string => {
1946
2034
  `(function() {\n` +
1947
2035
  ` let timer = null;\n` +
1948
2036
  ` return function(event) {\n` +
2037
+ ` const __bound = event.currentTarget;\n` +
1949
2038
  ` clearTimeout(timer);\n` +
1950
2039
  ` timer = setTimeout(async function() {\n` +
1951
2040
  ` ${body}\n` +
@@ -1972,17 +2061,15 @@ const generateElementHandler = (et: ElementTriggerInfo): string => {
1972
2061
  // name as missing. `general-extract-form-data` already returns its fields
1973
2062
  // flat for exactly this reason; the trigger now matches it, so both
1974
2063
  // `params[0].name` and `params[0].formData.name` resolve.
1975
- //
1976
- // The spread comes FIRST so a field literally named `formData`, `formId`,
1977
- // `element` or `timestamp` can never shadow the reserved keys the runtime
1978
- // and the workflow editor depend on.
2064
+ // The context is built by `formTriggerContextSource`, shared with the
2065
+ // native-listener fallback.
1979
2066
  return (
1980
2067
  `async function(event) {\n` +
1981
2068
  (preventDefault ? ' event.preventDefault();\n' : '') +
1982
2069
  ` const formData = {};\n` +
1983
2070
  ` const fd = new FormData(event.target);\n` +
1984
2071
  ` fd.forEach(function(v, k) { formData[k] = v; });\n` +
1985
- ` const triggerContext = Object.assign({}, formData, { fields: formData, formData: formData, formId: '${elementId}', triggerElement: event.target, element: event.target, timestamp: Date.now() });\n` +
2072
+ ` const triggerContext = ${formTriggerContextSource(elementId)};\n` +
1986
2073
  // Growth analytics: a form whose workflow ran to the end is a lead.
1987
2074
  // __execWf resolves undefined when the run threw, so only a finished
1988
2075
  // run reports.
@@ -2081,7 +2168,10 @@ const generateElementHandler = (et: ElementTriggerInfo): string => {
2081
2168
  }
2082
2169
  }
2083
2170
 
2084
- const generateUnmatchedElementFallback = (et: ElementTriggerInfo, safeId: string): string => {
2171
+ export const generateUnmatchedElementFallback = (
2172
+ et: ElementTriggerInfo,
2173
+ safeId: string
2174
+ ): string => {
2085
2175
  const config = et.triggerConfig
2086
2176
  const elementId = et.elementId
2087
2177
  const domEvent = getDOMEventName(et.workflow.trigger.type, config)
@@ -2104,20 +2194,24 @@ const generateUnmatchedElementFallback = (et: ElementTriggerInfo, safeId: string
2104
2194
  ` const formData = {};\n` +
2105
2195
  ` const fd = new FormData(event.target);\n` +
2106
2196
  ` fd.forEach(function(v, k) { formData[k] = v; });\n` +
2107
- ` const triggerContext = { formData: formData, formId: '${elementId}', triggerElement: event.target, element: event.target, timestamp: Date.now() };\n`
2197
+ ` const triggerContext = ${formTriggerContextSource(elementId)};\n`
2108
2198
  } else if (isInput) {
2109
2199
  handlerBody += ` const triggerContext = { value: event.target.value, elementId: '${elementId}', triggerElement: event.target, element: event.target, timestamp: Date.now() };\n`
2110
2200
  } else if (et.workflow.trigger.type === 'event-element-event') {
2111
2201
  // See `event-element-event` in `generateElementHandler` for why we
2112
2202
  // include `value`/`checked`/`files` unconditionally.
2113
- handlerBody += ` const triggerContext = { elementId: '${elementId}', eventType: '${domEvent}', triggerElement: event.target, element: event.target, timestamp: Date.now(), clientX: event.clientX, clientY: event.clientY, offsetX: event.offsetX, offsetY: event.offsetY, button: event.button, deltaX: event.deltaX, deltaY: event.deltaY, deltaMode: event.deltaMode, key: event.key, ${formControlPropertyReads(
2203
+ handlerBody += ` const triggerContext = { elementId: '${elementId}', eventType: '${domEvent}', triggerElement: event.target, element: event.target, target: event.target, currentTarget: __el_${safeId}, timestamp: Date.now(), clientX: event.clientX, clientY: event.clientY, offsetX: event.offsetX, offsetY: event.offsetY, button: event.button, deltaX: event.deltaX, deltaY: event.deltaY, deltaMode: event.deltaMode, key: event.key, ${formControlPropertyReads(
2114
2204
  'event.target'
2115
2205
  )} };\n`
2116
2206
  if (domEvent === 'drop') {
2117
2207
  handlerBody += ` if (event.dataTransfer && event.dataTransfer.files) { triggerContext.files = Array.from(event.dataTransfer.files); }\n`
2118
2208
  }
2119
2209
  } else {
2120
- handlerBody += ` const triggerContext = { elementId: '${elementId}', triggerElement: event.target, element: event.target, timestamp: Date.now(), clientX: event.clientX, clientY: event.clientY };\n`
2210
+ // The listener sits on `__el_<id>`, so that is the element whose
2211
+ // attributes are delivered, whichever child was clicked.
2212
+ handlerBody += ` const triggerContext = Object.assign(${datasetTriggerFieldsSource(
2213
+ `__el_${safeId}`
2214
+ )}, { elementId: '${elementId}', triggerElement: event.target, element: event.target, timestamp: Date.now(), clientX: event.clientX, clientY: event.clientY });\n`
2121
2215
  }
2122
2216
 
2123
2217
  handlerBody += ` __execWf(__wfConfig_${safeId}, triggerContext, __wfServerUrls_${safeId});`
@@ -2169,7 +2263,7 @@ const generateLoadedTriggerRun = (
2169
2263
  ` cleanups.push(function() { clearTimeout(__delay_${safeId}); });\n`
2170
2264
  : ` ${execCall};\n`
2171
2265
 
2172
- const generateLifecycleTrigger = (wf: UIDLWorkflow, safeId: string): string => {
2266
+ export const generateLifecycleTrigger = (wf: UIDLWorkflow, safeId: string): string => {
2173
2267
  const trigger = wf.trigger
2174
2268
  const config = (trigger.config || {}) as Record<string, unknown>
2175
2269
  const execCall = `__execWf(__wfConfig_${safeId}, triggerContext, __wfServerUrls_${safeId})`
@@ -2539,12 +2633,7 @@ const generateLifecycleTrigger = (wf: UIDLWorkflow, safeId: string): string => {
2539
2633
  }
2540
2634
 
2541
2635
  if (controlEventName) {
2542
- intervalCode +=
2543
- ` var __iv_ctrlHandler_${safeId} = function() {\n` +
2544
- ` if (__iv_running_${safeId}) { __iv_stop_${safeId}(); }\n` +
2545
- ` else { __iv_start_${safeId}(); }\n` +
2546
- ` };\n` +
2547
- ` window.addEventListener('${controlEventName}', __iv_ctrlHandler_${safeId});\n`
2636
+ intervalCode += intervalControlListenerSource(safeId, controlEventName)
2548
2637
  }
2549
2638
 
2550
2639
  if (autoStart) {
@@ -2558,9 +2647,7 @@ const generateLifecycleTrigger = (wf: UIDLWorkflow, safeId: string): string => {
2558
2647
  (!runWhileHidden
2559
2648
  ? ` document.removeEventListener('visibilitychange', __iv_visHandler_${safeId});\n`
2560
2649
  : '') +
2561
- (controlEventName
2562
- ? ` window.removeEventListener('${controlEventName}', __iv_ctrlHandler_${safeId});\n`
2563
- : '') +
2650
+ (controlEventName ? intervalControlTeardownSource(safeId, controlEventName) : '') +
2564
2651
  ` });`
2565
2652
 
2566
2653
  return intervalCode