@teleporthq/teleport-plugin-next-workflows 0.45.8 → 0.45.13

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 (35) hide show
  1. package/__tests__/component-scope-interval.test.ts +120 -0
  2. package/__tests__/element-trigger-context-fields.test.ts +279 -0
  3. package/__tests__/interval-control-event.test.ts +217 -0
  4. package/__tests__/newsletter-plugin.test.ts +165 -0
  5. package/dist/cjs/newsletter/helper-code.js +2 -2
  6. package/dist/cjs/newsletter/routes-code.d.ts.map +1 -1
  7. package/dist/cjs/newsletter/routes-code.js +64 -31
  8. package/dist/cjs/newsletter/routes-code.js.map +1 -1
  9. package/dist/cjs/trigger-generator.d.ts +2 -0
  10. package/dist/cjs/trigger-generator.d.ts.map +1 -1
  11. package/dist/cjs/trigger-generator.js +32 -9
  12. package/dist/cjs/trigger-generator.js.map +1 -1
  13. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  14. package/dist/cjs/workflow-component-plugin.d.ts +10 -1
  15. package/dist/cjs/workflow-component-plugin.d.ts.map +1 -1
  16. package/dist/cjs/workflow-component-plugin.js +36 -24
  17. package/dist/cjs/workflow-component-plugin.js.map +1 -1
  18. package/dist/esm/newsletter/helper-code.js +2 -2
  19. package/dist/esm/newsletter/routes-code.d.ts.map +1 -1
  20. package/dist/esm/newsletter/routes-code.js +64 -31
  21. package/dist/esm/newsletter/routes-code.js.map +1 -1
  22. package/dist/esm/trigger-generator.d.ts +2 -0
  23. package/dist/esm/trigger-generator.d.ts.map +1 -1
  24. package/dist/esm/trigger-generator.js +29 -8
  25. package/dist/esm/trigger-generator.js.map +1 -1
  26. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  27. package/dist/esm/workflow-component-plugin.d.ts +10 -1
  28. package/dist/esm/workflow-component-plugin.d.ts.map +1 -1
  29. package/dist/esm/workflow-component-plugin.js +32 -23
  30. package/dist/esm/workflow-component-plugin.js.map +1 -1
  31. package/package.json +2 -2
  32. package/src/newsletter/helper-code.ts +2 -2
  33. package/src/newsletter/routes-code.ts +67 -31
  34. package/src/trigger-generator.ts +41 -10
  35. package/src/workflow-component-plugin.ts +48 -23
@@ -24,6 +24,45 @@ import { elementVisibleObserverCode } from './element-visible-trigger'
24
24
  export const formControlPropertyReads = (elementRef: string): string =>
25
25
  `value: ${elementRef}.value, checked: ${elementRef}.checked, files: ${elementRef}.files`
26
26
 
27
+ /* The channel the "Emit Custom Event" node dispatches on. */
28
+ const CUSTOM_EVENT_CHANNEL = 'workflow:custom:'
29
+
30
+ const intervalControlEventNames = (controlEventName: string): string[] => [
31
+ CUSTOM_EVENT_CHANNEL + controlEventName,
32
+ controlEventName,
33
+ ]
34
+
35
+ /* A timer's control event is heard on the channel the emit node dispatches on,
36
+ and under its bare name for a page that dispatches it by hand. The two names
37
+ differ, so one dispatch reaches the handler once.
38
+ The event may say what it wants: a detail of 'start' or 'stop', or an object
39
+ whose `action` or `command` is one of them, does exactly that, and asking
40
+ for the state the timer is already in changes nothing. Any other event
41
+ toggles the timer. */
42
+ export const intervalControlListenerSource = (safeId: string, controlEventName: string): string =>
43
+ ` var __iv_ctrlHandler_${safeId} = function(event) {\n` +
44
+ ` var detail = event && event.detail;\n` +
45
+ ` var command = typeof detail === 'string' ? detail : detail && (detail.action || detail.command);\n` +
46
+ ` if (command === 'start') { __iv_start_${safeId}(); }\n` +
47
+ ` else if (command === 'stop') { __iv_stop_${safeId}(); }\n` +
48
+ ` else if (__iv_running_${safeId}) { __iv_stop_${safeId}(); }\n` +
49
+ ` else { __iv_start_${safeId}(); }\n` +
50
+ ` };\n` +
51
+ intervalControlEventNames(controlEventName)
52
+ .map(
53
+ (name) =>
54
+ ` window.addEventListener(${JSON.stringify(name)}, __iv_ctrlHandler_${safeId});\n`
55
+ )
56
+ .join('')
57
+
58
+ export const intervalControlTeardownSource = (safeId: string, controlEventName: string): string =>
59
+ intervalControlEventNames(controlEventName)
60
+ .map(
61
+ (name) =>
62
+ ` window.removeEventListener(${JSON.stringify(name)}, __iv_ctrlHandler_${safeId});\n`
63
+ )
64
+ .join('')
65
+
27
66
  export const generateTriggerCode = (workflow: UIDLWorkflow, workflowVarName: string): string => {
28
67
  const trigger = workflow.trigger
29
68
  const triggerType = trigger.type
@@ -733,11 +772,7 @@ const generateIntervalTrigger = (
733
772
  ${
734
773
  controlEventName
735
774
  ? `
736
- var __iv_ctrlHandler_${safeId} = function() {
737
- if (__iv_running_${safeId}) { __iv_stop_${safeId}(); }
738
- else { __iv_start_${safeId}(); }
739
- };
740
- window.addEventListener('${controlEventName}', __iv_ctrlHandler_${safeId});`
775
+ ${intervalControlListenerSource(safeId, controlEventName)}`
741
776
  : ''
742
777
  }
743
778
 
@@ -751,11 +786,7 @@ const generateIntervalTrigger = (
751
786
  ? `document.removeEventListener('visibilitychange', __iv_visHandler_${safeId});`
752
787
  : ''
753
788
  }
754
- ${
755
- controlEventName
756
- ? `window.removeEventListener('${controlEventName}', __iv_ctrlHandler_${safeId});`
757
- : ''
758
- }
789
+ ${controlEventName ? intervalControlTeardownSource(safeId, controlEventName) : ''}
759
790
  };`
760
791
  }
761
792
 
@@ -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'
@@ -1912,7 +1917,20 @@ ${globalStateChangeHandlerLines.join('\n')}
1912
1917
  `
1913
1918
  }
1914
1919
 
1915
- const generateElementHandler = (et: ElementTriggerInfo): string => {
1920
+ /* The bound element's `data-*` attributes, as trigger fields a workflow reads
1921
+ by name: `data-tab-index` is `dataTabIndex`, the name the static HTML export
1922
+ resolves from the same attribute. Values are the attribute strings. The
1923
+ result is the object the trigger's own fields are then assigned onto, so no
1924
+ attribute can replace one of them. */
1925
+ const datasetTriggerFieldsSource = (element: string): string =>
1926
+ `Object.keys((${element} && ${element}.dataset) || {}).reduce(function(fields, key) { fields['data' + key.charAt(0).toUpperCase() + key.slice(1)] = ${element}.dataset[key]; return fields; }, {})`
1927
+
1928
+ /* The submitted fields are spread FIRST so a field literally named `formData`,
1929
+ `formId`, `element` or `timestamp` can never shadow the reserved keys. */
1930
+ const formTriggerContextSource = (elementId: string): string =>
1931
+ `Object.assign({}, formData, { fields: formData, formData: formData, formId: '${elementId}', triggerElement: event.target, element: event.target, timestamp: Date.now() })`
1932
+
1933
+ export const generateElementHandler = (et: ElementTriggerInfo): string => {
1916
1934
  const safeId = et.workflow.id.replace(/[^a-zA-Z0-9]/g, '_')
1917
1935
  const config = et.triggerConfig
1918
1936
  const elementId = et.elementId
@@ -1936,9 +1954,17 @@ const generateElementHandler = (et: ElementTriggerInfo): string => {
1936
1954
  // the bound element when the user clicks a child — e.g. the payment
1937
1955
  // provider row sets `data-provider-name` on the row div, but a click
1938
1956
  // on the inner label span would otherwise resolve to an empty dataset.
1957
+ // A debounced run starts after the event has finished dispatching, when
1958
+ // `currentTarget` is already null, so the bound element is read before
1959
+ // the timer is set.
1960
+ const boundElement = debounce
1961
+ ? '__bound || event.target'
1962
+ : 'event.currentTarget || event.target'
1939
1963
  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` +
1964
+ ` const __te = ${boundElement};\n` +
1965
+ ` const triggerContext = Object.assign(${datasetTriggerFieldsSource(
1966
+ '__te'
1967
+ )}, { elementId: '${elementId}', triggerElement: __te, element: __te, timestamp: Date.now(), clientX: event.clientX, clientY: event.clientY });\n` +
1942
1968
  ` return __execWf(__wfConfig_${safeId}, triggerContext, __wfServerUrls_${safeId});`
1943
1969
 
1944
1970
  if (debounce) {
@@ -1946,6 +1972,7 @@ const generateElementHandler = (et: ElementTriggerInfo): string => {
1946
1972
  `(function() {\n` +
1947
1973
  ` let timer = null;\n` +
1948
1974
  ` return function(event) {\n` +
1975
+ ` const __bound = event.currentTarget;\n` +
1949
1976
  ` clearTimeout(timer);\n` +
1950
1977
  ` timer = setTimeout(async function() {\n` +
1951
1978
  ` ${body}\n` +
@@ -1972,17 +1999,15 @@ const generateElementHandler = (et: ElementTriggerInfo): string => {
1972
1999
  // name as missing. `general-extract-form-data` already returns its fields
1973
2000
  // flat for exactly this reason; the trigger now matches it, so both
1974
2001
  // `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.
2002
+ // The context is built by `formTriggerContextSource`, shared with the
2003
+ // native-listener fallback.
1979
2004
  return (
1980
2005
  `async function(event) {\n` +
1981
2006
  (preventDefault ? ' event.preventDefault();\n' : '') +
1982
2007
  ` const formData = {};\n` +
1983
2008
  ` const fd = new FormData(event.target);\n` +
1984
2009
  ` 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` +
2010
+ ` const triggerContext = ${formTriggerContextSource(elementId)};\n` +
1986
2011
  // Growth analytics: a form whose workflow ran to the end is a lead.
1987
2012
  // __execWf resolves undefined when the run threw, so only a finished
1988
2013
  // run reports.
@@ -2081,7 +2106,10 @@ const generateElementHandler = (et: ElementTriggerInfo): string => {
2081
2106
  }
2082
2107
  }
2083
2108
 
2084
- const generateUnmatchedElementFallback = (et: ElementTriggerInfo, safeId: string): string => {
2109
+ export const generateUnmatchedElementFallback = (
2110
+ et: ElementTriggerInfo,
2111
+ safeId: string
2112
+ ): string => {
2085
2113
  const config = et.triggerConfig
2086
2114
  const elementId = et.elementId
2087
2115
  const domEvent = getDOMEventName(et.workflow.trigger.type, config)
@@ -2104,20 +2132,24 @@ const generateUnmatchedElementFallback = (et: ElementTriggerInfo, safeId: string
2104
2132
  ` const formData = {};\n` +
2105
2133
  ` const fd = new FormData(event.target);\n` +
2106
2134
  ` 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`
2135
+ ` const triggerContext = ${formTriggerContextSource(elementId)};\n`
2108
2136
  } else if (isInput) {
2109
2137
  handlerBody += ` const triggerContext = { value: event.target.value, elementId: '${elementId}', triggerElement: event.target, element: event.target, timestamp: Date.now() };\n`
2110
2138
  } else if (et.workflow.trigger.type === 'event-element-event') {
2111
2139
  // See `event-element-event` in `generateElementHandler` for why we
2112
2140
  // 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(
2141
+ 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
2142
  'event.target'
2115
2143
  )} };\n`
2116
2144
  if (domEvent === 'drop') {
2117
2145
  handlerBody += ` if (event.dataTransfer && event.dataTransfer.files) { triggerContext.files = Array.from(event.dataTransfer.files); }\n`
2118
2146
  }
2119
2147
  } else {
2120
- handlerBody += ` const triggerContext = { elementId: '${elementId}', triggerElement: event.target, element: event.target, timestamp: Date.now(), clientX: event.clientX, clientY: event.clientY };\n`
2148
+ // The listener sits on `__el_<id>`, so that is the element whose
2149
+ // attributes are delivered, whichever child was clicked.
2150
+ handlerBody += ` const triggerContext = Object.assign(${datasetTriggerFieldsSource(
2151
+ `__el_${safeId}`
2152
+ )}, { elementId: '${elementId}', triggerElement: event.target, element: event.target, timestamp: Date.now(), clientX: event.clientX, clientY: event.clientY });\n`
2121
2153
  }
2122
2154
 
2123
2155
  handlerBody += ` __execWf(__wfConfig_${safeId}, triggerContext, __wfServerUrls_${safeId});`
@@ -2169,7 +2201,7 @@ const generateLoadedTriggerRun = (
2169
2201
  ` cleanups.push(function() { clearTimeout(__delay_${safeId}); });\n`
2170
2202
  : ` ${execCall};\n`
2171
2203
 
2172
- const generateLifecycleTrigger = (wf: UIDLWorkflow, safeId: string): string => {
2204
+ export const generateLifecycleTrigger = (wf: UIDLWorkflow, safeId: string): string => {
2173
2205
  const trigger = wf.trigger
2174
2206
  const config = (trigger.config || {}) as Record<string, unknown>
2175
2207
  const execCall = `__execWf(__wfConfig_${safeId}, triggerContext, __wfServerUrls_${safeId})`
@@ -2539,12 +2571,7 @@ const generateLifecycleTrigger = (wf: UIDLWorkflow, safeId: string): string => {
2539
2571
  }
2540
2572
 
2541
2573
  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`
2574
+ intervalCode += intervalControlListenerSource(safeId, controlEventName)
2548
2575
  }
2549
2576
 
2550
2577
  if (autoStart) {
@@ -2558,9 +2585,7 @@ const generateLifecycleTrigger = (wf: UIDLWorkflow, safeId: string): string => {
2558
2585
  (!runWhileHidden
2559
2586
  ? ` document.removeEventListener('visibilitychange', __iv_visHandler_${safeId});\n`
2560
2587
  : '') +
2561
- (controlEventName
2562
- ? ` window.removeEventListener('${controlEventName}', __iv_ctrlHandler_${safeId});\n`
2563
- : '') +
2588
+ (controlEventName ? intervalControlTeardownSource(safeId, controlEventName) : '') +
2564
2589
  ` });`
2565
2590
 
2566
2591
  return intervalCode