@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.
- package/__tests__/component-scope-interval.test.ts +120 -0
- package/__tests__/element-trigger-context-fields.test.ts +279 -0
- package/__tests__/global-state-read-only-wiring.test.ts +257 -0
- package/__tests__/interval-control-event.test.ts +217 -0
- package/dist/cjs/trigger-generator.d.ts +2 -0
- package/dist/cjs/trigger-generator.d.ts.map +1 -1
- package/dist/cjs/trigger-generator.js +32 -9
- package/dist/cjs/trigger-generator.js.map +1 -1
- package/dist/cjs/tsconfig.tsbuildinfo +1 -1
- package/dist/cjs/workflow-component-plugin.d.ts +10 -1
- package/dist/cjs/workflow-component-plugin.d.ts.map +1 -1
- package/dist/cjs/workflow-component-plugin.js +79 -25
- package/dist/cjs/workflow-component-plugin.js.map +1 -1
- package/dist/esm/trigger-generator.d.ts +2 -0
- package/dist/esm/trigger-generator.d.ts.map +1 -1
- package/dist/esm/trigger-generator.js +29 -8
- package/dist/esm/trigger-generator.js.map +1 -1
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/dist/esm/workflow-component-plugin.d.ts +10 -1
- package/dist/esm/workflow-component-plugin.d.ts.map +1 -1
- package/dist/esm/workflow-component-plugin.js +76 -25
- package/dist/esm/workflow-component-plugin.js.map +1 -1
- package/package.json +2 -2
- package/src/trigger-generator.ts +41 -10
- package/src/workflow-component-plugin.ts +112 -25
|
@@ -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 {
|
|
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
|
-
|
|
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 =
|
|
1941
|
-
` const triggerContext =
|
|
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
|
-
//
|
|
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 =
|
|
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 = (
|
|
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 =
|
|
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
|
-
|
|
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
|