@teleporthq/teleport-plugin-next-workflows 0.43.75 → 0.44.0

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 (129) hide show
  1. package/__tests__/booking-calendar-feed-route.test.ts +494 -0
  2. package/__tests__/custom-node-result-taken-branch.test.ts +107 -0
  3. package/__tests__/data-api-date-parser.test.ts +45 -0
  4. package/__tests__/invoice-billing-details.test.ts +278 -0
  5. package/__tests__/middleware-admin-panel-roles.test.ts +127 -0
  6. package/__tests__/page-arrival-remount.test.ts +247 -0
  7. package/__tests__/payment-drivers-module.test.ts +19 -0
  8. package/__tests__/raw-query-error-code.test.ts +47 -0
  9. package/__tests__/runtime-template-token-resolution.test.ts +16 -0
  10. package/__tests__/segment-reply-changed-keys.test.ts +31 -0
  11. package/__tests__/segment-route-skipped-branch.test.ts +101 -0
  12. package/dist/cjs/api-route-generator.d.ts.map +1 -1
  13. package/dist/cjs/api-route-generator.js +23 -13
  14. package/dist/cjs/api-route-generator.js.map +1 -1
  15. package/dist/cjs/auth-generator.d.ts.map +1 -1
  16. package/dist/cjs/auth-generator.js +75 -27
  17. package/dist/cjs/auth-generator.js.map +1 -1
  18. package/dist/cjs/booking-calendar-feed-route-generator.d.ts +28 -0
  19. package/dist/cjs/booking-calendar-feed-route-generator.d.ts.map +1 -0
  20. package/dist/cjs/booking-calendar-feed-route-generator.js +296 -0
  21. package/dist/cjs/booking-calendar-feed-route-generator.js.map +1 -0
  22. package/dist/cjs/data-api-route-generator.d.ts.map +1 -1
  23. package/dist/cjs/data-api-route-generator.js +6 -1
  24. package/dist/cjs/data-api-route-generator.js.map +1 -1
  25. package/dist/cjs/executor-generator.d.ts.map +1 -1
  26. package/dist/cjs/executor-generator.js +30 -2
  27. package/dist/cjs/executor-generator.js.map +1 -1
  28. package/dist/cjs/index.d.ts +1 -1
  29. package/dist/cjs/index.d.ts.map +1 -1
  30. package/dist/cjs/index.js +2 -1
  31. package/dist/cjs/index.js.map +1 -1
  32. package/dist/cjs/invoice/invoice-assembly-code.d.ts.map +1 -1
  33. package/dist/cjs/invoice/invoice-assembly-code.js +46 -6
  34. package/dist/cjs/invoice/invoice-assembly-code.js.map +1 -1
  35. package/dist/cjs/invoice/invoice-html-code.d.ts.map +1 -1
  36. package/dist/cjs/invoice/invoice-html-code.js +10 -2
  37. package/dist/cjs/invoice/invoice-html-code.js.map +1 -1
  38. package/dist/cjs/nodes/data/data-raw-query.d.ts.map +1 -1
  39. package/dist/cjs/nodes/data/data-raw-query.js +11 -1
  40. package/dist/cjs/nodes/data/data-raw-query.js.map +1 -1
  41. package/dist/cjs/nodes/general/general-custom-js.d.ts.map +1 -1
  42. package/dist/cjs/nodes/general/general-custom-js.js +11 -0
  43. package/dist/cjs/nodes/general/general-custom-js.js.map +1 -1
  44. package/dist/cjs/page-arrival.d.ts +10 -0
  45. package/dist/cjs/page-arrival.d.ts.map +1 -0
  46. package/dist/cjs/page-arrival.js +86 -0
  47. package/dist/cjs/page-arrival.js.map +1 -0
  48. package/dist/cjs/payments/index.d.ts +10 -4
  49. package/dist/cjs/payments/index.d.ts.map +1 -1
  50. package/dist/cjs/payments/index.js +18 -7
  51. package/dist/cjs/payments/index.js.map +1 -1
  52. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  53. package/dist/cjs/workflow-component-plugin.d.ts.map +1 -1
  54. package/dist/cjs/workflow-component-plugin.js +35 -149
  55. package/dist/cjs/workflow-component-plugin.js.map +1 -1
  56. package/dist/cjs/workflow-project-plugin.d.ts.map +1 -1
  57. package/dist/cjs/workflow-project-plugin.js +14 -4
  58. package/dist/cjs/workflow-project-plugin.js.map +1 -1
  59. package/dist/cjs/workflow-scope.d.ts +19 -0
  60. package/dist/cjs/workflow-scope.d.ts.map +1 -0
  61. package/dist/cjs/workflow-scope.js +147 -0
  62. package/dist/cjs/workflow-scope.js.map +1 -0
  63. package/dist/esm/api-route-generator.d.ts.map +1 -1
  64. package/dist/esm/api-route-generator.js +23 -13
  65. package/dist/esm/api-route-generator.js.map +1 -1
  66. package/dist/esm/auth-generator.d.ts.map +1 -1
  67. package/dist/esm/auth-generator.js +75 -27
  68. package/dist/esm/auth-generator.js.map +1 -1
  69. package/dist/esm/booking-calendar-feed-route-generator.d.ts +28 -0
  70. package/dist/esm/booking-calendar-feed-route-generator.d.ts.map +1 -0
  71. package/dist/esm/booking-calendar-feed-route-generator.js +291 -0
  72. package/dist/esm/booking-calendar-feed-route-generator.js.map +1 -0
  73. package/dist/esm/data-api-route-generator.d.ts.map +1 -1
  74. package/dist/esm/data-api-route-generator.js +6 -1
  75. package/dist/esm/data-api-route-generator.js.map +1 -1
  76. package/dist/esm/executor-generator.d.ts.map +1 -1
  77. package/dist/esm/executor-generator.js +30 -2
  78. package/dist/esm/executor-generator.js.map +1 -1
  79. package/dist/esm/index.d.ts +1 -1
  80. package/dist/esm/index.d.ts.map +1 -1
  81. package/dist/esm/index.js +1 -1
  82. package/dist/esm/index.js.map +1 -1
  83. package/dist/esm/invoice/invoice-assembly-code.d.ts.map +1 -1
  84. package/dist/esm/invoice/invoice-assembly-code.js +46 -6
  85. package/dist/esm/invoice/invoice-assembly-code.js.map +1 -1
  86. package/dist/esm/invoice/invoice-html-code.d.ts.map +1 -1
  87. package/dist/esm/invoice/invoice-html-code.js +10 -2
  88. package/dist/esm/invoice/invoice-html-code.js.map +1 -1
  89. package/dist/esm/nodes/data/data-raw-query.d.ts.map +1 -1
  90. package/dist/esm/nodes/data/data-raw-query.js +11 -1
  91. package/dist/esm/nodes/data/data-raw-query.js.map +1 -1
  92. package/dist/esm/nodes/general/general-custom-js.d.ts.map +1 -1
  93. package/dist/esm/nodes/general/general-custom-js.js +11 -0
  94. package/dist/esm/nodes/general/general-custom-js.js.map +1 -1
  95. package/dist/esm/page-arrival.d.ts +10 -0
  96. package/dist/esm/page-arrival.d.ts.map +1 -0
  97. package/dist/esm/page-arrival.js +81 -0
  98. package/dist/esm/page-arrival.js.map +1 -0
  99. package/dist/esm/payments/index.d.ts +10 -4
  100. package/dist/esm/payments/index.d.ts.map +1 -1
  101. package/dist/esm/payments/index.js +16 -6
  102. package/dist/esm/payments/index.js.map +1 -1
  103. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  104. package/dist/esm/workflow-component-plugin.d.ts.map +1 -1
  105. package/dist/esm/workflow-component-plugin.js +32 -146
  106. package/dist/esm/workflow-component-plugin.js.map +1 -1
  107. package/dist/esm/workflow-project-plugin.d.ts.map +1 -1
  108. package/dist/esm/workflow-project-plugin.js +14 -4
  109. package/dist/esm/workflow-project-plugin.js.map +1 -1
  110. package/dist/esm/workflow-scope.d.ts +19 -0
  111. package/dist/esm/workflow-scope.d.ts.map +1 -0
  112. package/dist/esm/workflow-scope.js +142 -0
  113. package/dist/esm/workflow-scope.js.map +1 -0
  114. package/package.json +4 -4
  115. package/src/api-route-generator.ts +23 -13
  116. package/src/auth-generator.ts +80 -25
  117. package/src/booking-calendar-feed-route-generator.ts +293 -0
  118. package/src/data-api-route-generator.ts +6 -1
  119. package/src/executor-generator.ts +30 -2
  120. package/src/index.ts +1 -0
  121. package/src/invoice/invoice-assembly-code.ts +46 -6
  122. package/src/invoice/invoice-html-code.ts +10 -2
  123. package/src/nodes/data/data-raw-query.ts +11 -1
  124. package/src/nodes/general/general-custom-js.ts +10 -0
  125. package/src/page-arrival.ts +115 -0
  126. package/src/payments/index.ts +18 -5
  127. package/src/workflow-component-plugin.ts +43 -188
  128. package/src/workflow-project-plugin.ts +15 -4
  129. package/src/workflow-scope.ts +184 -0
@@ -0,0 +1,115 @@
1
+ import {
2
+ ComponentUIDL,
3
+ UIDLElement,
4
+ UIDLNode,
5
+ UIDLWorkflow,
6
+ UIDLWorkflows,
7
+ } from '@teleporthq/teleport-types'
8
+ import { UIDLUtils } from '@teleporthq/teleport-shared'
9
+ import { getRelevantWorkflows, resolveLifecycleTriggerElementId } from './workflow-scope'
10
+
11
+ /**
12
+ * Lifecycle triggers that fire because the page appeared. They run when the
13
+ * page or component carrying them mounts, so the page has to remount on every
14
+ * arrival (see `PageArrival.remountPageOnArrival`).
15
+ */
16
+ const PAGE_ARRIVAL_TRIGGER_TYPES = new Set([
17
+ 'event-page-loaded',
18
+ 'event-element-visible',
19
+ 'event-component-loaded',
20
+ ])
21
+
22
+ export const isPageArrivalWorkflow = (workflow: UIDLWorkflow): boolean =>
23
+ PAGE_ARRIVAL_TRIGGER_TYPES.has(workflow.trigger.type)
24
+
25
+ /** Every static `id` in a node tree — the ids an element-visible trigger can observe. */
26
+ const collectStaticElementIds = (node: UIDLNode): Set<string> => {
27
+ const ids = new Set<string>()
28
+ UIDLUtils.traverseElements(node, (element) => {
29
+ const id = element.attrs?.id
30
+ if (id?.type === 'static' && typeof id.content === 'string') {
31
+ ids.add(id.content)
32
+ }
33
+ })
34
+ return ids
35
+ }
36
+
37
+ /**
38
+ * The project component a node renders. Local dependencies are resolved before
39
+ * the pages are generated, which rewrites `semanticType` to the component's
40
+ * class name — the original key still matches a component that was not renamed.
41
+ */
42
+ const findRenderedComponent = (
43
+ element: UIDLElement,
44
+ projectComponents: Record<string, ComponentUIDL>
45
+ ): ComponentUIDL | undefined => {
46
+ if (element.dependency?.type !== 'local') {
47
+ return undefined
48
+ }
49
+ const reference = element.semanticType || element.elementType
50
+ return (
51
+ projectComponents[reference] ||
52
+ Object.values(projectComponents).find(
53
+ (component) => UIDLUtils.getComponentClassName(component) === reference
54
+ )
55
+ )
56
+ }
57
+
58
+ /** The components a node tree renders, directly or through other components. */
59
+ const collectRenderedComponents = (
60
+ node: UIDLNode,
61
+ projectComponents: Record<string, ComponentUIDL>,
62
+ rendered: Set<ComponentUIDL>
63
+ ): Set<ComponentUIDL> => {
64
+ UIDLUtils.traverseElements(node, (element) => {
65
+ const component = findRenderedComponent(element, projectComponents)
66
+ if (component && !rendered.has(component)) {
67
+ rendered.add(component)
68
+ collectRenderedComponents(component.node, projectComponents, rendered)
69
+ }
70
+ })
71
+ return rendered
72
+ }
73
+
74
+ /**
75
+ * Mirrors what the workflow plugin emits into the component: the workflows it
76
+ * finds relevant, with an element-visible trigger kept only when the observed
77
+ * element is rendered by the component itself.
78
+ */
79
+ const componentRunsOnArrival = (component: ComponentUIDL, workflows: UIDLWorkflows): boolean => {
80
+ const arrivalWorkflows = getRelevantWorkflows(workflows, component, false).filter(
81
+ isPageArrivalWorkflow
82
+ )
83
+ if (arrivalWorkflows.length === 0) {
84
+ return false
85
+ }
86
+ const elementIds = collectStaticElementIds(component.node)
87
+ return arrivalWorkflows.some((workflow) => {
88
+ const targetId = resolveLifecycleTriggerElementId(
89
+ workflow.trigger.type,
90
+ (workflow.trigger.config || {}) as Record<string, unknown>
91
+ )
92
+ return (
93
+ workflow.trigger.type !== 'event-element-visible' ||
94
+ (targetId !== null && elementIds.has(targetId))
95
+ )
96
+ })
97
+ }
98
+
99
+ /**
100
+ * True when a component the page renders runs a workflow when it appears — an
101
+ * element-visible trigger on one of its elements, a component-loaded trigger.
102
+ * The component mounts again only when the page around it does, so such a page
103
+ * remounts on every arrival even when it has no arrival trigger of its own.
104
+ */
105
+ export const pageRendersArrivalComponent = (
106
+ pageNode: UIDLNode,
107
+ workflows: UIDLWorkflows,
108
+ projectComponents?: Record<string, ComponentUIDL>
109
+ ): boolean => {
110
+ if (!projectComponents) {
111
+ return false
112
+ }
113
+ const rendered = collectRenderedComponents(pageNode, projectComponents, new Set())
114
+ return Array.from(rendered).some((component) => componentRunsOnArrival(component, workflows))
115
+ }
@@ -2,6 +2,7 @@ import {
2
2
  FileType,
3
3
  ProjectPluginStructure,
4
4
  ProjectUIDL,
5
+ UIDLEcommercePaymentProvider,
5
6
  UIDLWorkflows,
6
7
  } from '@teleporthq/teleport-types'
7
8
  import { generatePaymentCoreCode } from './payment-core-code'
@@ -63,6 +64,18 @@ export const isPaymentWebhookConfig = (webhookConfig: unknown): boolean =>
63
64
  ((webhookConfig || {}) as { signatureAlgorithm?: unknown }).signatureAlgorithm ===
64
65
  PAYMENT_DRIVER_SIGNATURE_ALGORITHM
65
66
 
67
+ /**
68
+ * The providers the site is paid through: the store's (`ecommerceSettings`),
69
+ * or — on a site that takes payments without a store, for its bookings —
70
+ * `paymentSettings`'.
71
+ */
72
+ export const readUidlPaymentProviders = (
73
+ uidl: Pick<ProjectUIDL, 'ecommerceSettings' | 'paymentSettings'> | undefined
74
+ ): UIDLEcommercePaymentProvider[] =>
75
+ (uidl?.ecommerceSettings
76
+ ? uidl.ecommerceSettings.paymentProviders
77
+ : uidl?.paymentSettings?.paymentProviders) || []
78
+
66
79
  /**
67
80
  * The providers this store needs a driver for: every provider its checkout
68
81
  * offers, every provider a payment webhook verifies through, and Stripe +
@@ -71,11 +84,11 @@ export const isPaymentWebhookConfig = (webhookConfig: unknown): boolean =>
71
84
  * payment, in which case no driver module is written.
72
85
  */
73
86
  export const resolveStorePaymentDriverIds = (
74
- uidl: Pick<ProjectUIDL, 'ecommerceSettings'> & { workflows?: UIDLWorkflows },
87
+ uidl: Pick<ProjectUIDL, 'ecommerceSettings' | 'paymentSettings'> & { workflows?: UIDLWorkflows },
75
88
  usedNodeTypes: Iterable<string>
76
89
  ): PaymentDriverId[] => {
77
90
  const wanted = new Set<PaymentDriverId>()
78
- for (const provider of uidl.ecommerceSettings?.paymentProviders || []) {
91
+ for (const provider of readUidlPaymentProviders(uidl)) {
79
92
  const id = String(provider?.type || '').toLowerCase()
80
93
  if (isPaymentDriverId(id)) {
81
94
  wanted.add(id)
@@ -98,13 +111,13 @@ const ENV_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/
98
111
 
99
112
  /**
100
113
  * The secret the merchant saved each live provider credential under, as the
101
- * editor exported it (`ecommerceSettings.paymentProviders[].credentials`).
114
+ * editor exported it (`paymentProviders[].credentials` of either block).
102
115
  */
103
116
  export const readDeclaredPaymentCredentials = (
104
- uidl: Pick<ProjectUIDL, 'ecommerceSettings'> | undefined
117
+ uidl: Pick<ProjectUIDL, 'ecommerceSettings' | 'paymentSettings'> | undefined
105
118
  ): DeclaredPaymentCredentials => {
106
119
  const declared: DeclaredPaymentCredentials = {}
107
- for (const provider of uidl?.ecommerceSettings?.paymentProviders || []) {
120
+ for (const provider of readUidlPaymentProviders(uidl)) {
108
121
  const id = String(provider?.type || '').toLowerCase()
109
122
  if (!isPaymentDriverId(id)) {
110
123
  continue
@@ -12,7 +12,7 @@ import {
12
12
  UIDLCustomWorkflowNode,
13
13
  } from '@teleporthq/teleport-types'
14
14
  import * as types from '@babel/types'
15
- import { GenericUtils, JSIdentifiers, StringUtils } from '@teleporthq/teleport-shared'
15
+ import { GenericUtils, JSIdentifiers, PageArrival, StringUtils } from '@teleporthq/teleport-shared'
16
16
  import {
17
17
  splitIntoSegments,
18
18
  resolveNodeExecutionEnv,
@@ -34,6 +34,8 @@ import { elementVisibleObserverCode } from './element-visible-trigger'
34
34
  import { restoreControlledSelectValue } from './controlled-select'
35
35
  import { resolveScrollPoint } from './scroll-points'
36
36
  import { findFormStateKeyForHydration } from './admin-form-hydration'
37
+ import { getRelevantWorkflows, resolveLifecycleTriggerElementId } from './workflow-scope'
38
+ import { isPageArrivalWorkflow, pageRendersArrivalComponent } from './page-arrival'
37
39
 
38
40
  interface WorkflowPluginConfig {
39
41
  isPage?: boolean
@@ -85,29 +87,6 @@ const DOM_TO_REACT_EVENT: Record<string, string> = {
85
87
  drag: 'onDrag',
86
88
  }
87
89
 
88
- /**
89
- * Trigger types that run as LIFECYCLE code (no React event prop) but are still
90
- * bound to ONE element, so the workflow only belongs on the page that renders
91
- * it. Returns the DOM id to prune against, or null when the trigger is not
92
- * element-scoped.
93
- *
94
- * Same fallback chain as the element-trigger branch: the HTML id first, the
95
- * project-document node id only as a last resort.
96
- */
97
- const resolveLifecycleTriggerElementId = (
98
- triggerType: string,
99
- config: Record<string, unknown>
100
- ): string | null => {
101
- if (
102
- triggerType !== 'event-element-visible' &&
103
- triggerType !== 'event-chapter-reached' &&
104
- triggerType !== 'event-scene-scrolled-past'
105
- ) {
106
- return null
107
- }
108
- return ((config.elementHtmlId || config.nodeId) as string) || null
109
- }
110
-
111
90
  const getReactEventProp = (triggerType: string, config: Record<string, unknown>): string | null => {
112
91
  switch (triggerType) {
113
92
  case 'event-element-clicked':
@@ -229,6 +208,12 @@ export const createNextWorkflowPlugin: ComponentPluginFactory<WorkflowPluginConf
229
208
  return structure
230
209
  }
231
210
 
211
+ // Decided before the page's own workflows: a page can have none and still
212
+ // render a component that runs one when it appears.
213
+ if (isPage && pageRendersArrivalComponent(uidl.node, workflows, options.projectComponents)) {
214
+ PageArrival.remountPageOnArrival(structure)
215
+ }
216
+
232
217
  const relevantWorkflows = getRelevantWorkflows(workflows, uidl, isPage)
233
218
 
234
219
  if (relevantWorkflows.length === 0) {
@@ -533,6 +518,13 @@ export const createNextWorkflowPlugin: ComponentPluginFactory<WorkflowPluginConf
533
518
  globalStateChangeWorkflows
534
519
  )
535
520
 
521
+ const runsOnPageArrival = lifecycleWorkflows.some(
522
+ (wf) => activeWorkflowIds.has(wf.id) && isPageArrivalWorkflow(wf)
523
+ )
524
+ if (isPage && runsOnPageArrival) {
525
+ PageArrival.remountPageOnArrival(structure)
526
+ }
527
+
536
528
  // For update/detail pages: initialize form state from props
537
529
  const initialPropsData = uidl.outputOptions?.initialPropsData
538
530
  if (initialPropsData?.exposeAs?.name && formDataStateKeyForHydration) {
@@ -2047,13 +2039,18 @@ const generateElementHandler = (et: ElementTriggerInfo): string => {
2047
2039
  (preventDefault ? ' event.preventDefault();\n' : '') +
2048
2040
  (stopPropagation ? ' event.stopPropagation();\n' : '') +
2049
2041
  ` const __te = event.currentTarget || event.target;\n` +
2042
+ // The control that fired is `event.target`, which differs from the
2043
+ // bound element when the event bubbled: a radio group's wrapper
2044
+ // listens for `change` and the chosen radio is the target.
2050
2045
  // `formControlPropertyReads(...)` injects `value`, `checked`, `files`
2051
- // so dropdown/input/checkbox/file-picker `change` events surface the
2052
- // user's choice as `triggerContext.value` (etc.) — without this every
2046
+ // from THAT control so `change` events surface the user's choice as
2047
+ // `triggerContext.value` (etc.) — without this every
2053
2048
  // `state-update-local-state` node bound to `Trigger.value` writes
2054
- // `undefined` and the picker becomes a no-op.
2055
- ` const triggerContext = { elementId: '${elementId}', eventType: '${eventType}', triggerElement: __te, element: __te, 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(
2056
- '__te'
2049
+ // `undefined` and the picker becomes a no-op; and `target` carries
2050
+ // the control itself (its name, id, dataset) for delegated handlers.
2051
+ ` const __tt = event.target || __te;\n` +
2052
+ ` const triggerContext = { elementId: '${elementId}', eventType: '${eventType}', triggerElement: __te, element: __te, target: __tt, currentTarget: __te, 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(
2053
+ '__tt'
2057
2054
  )} };\n` +
2058
2055
  (isDropEvent
2059
2056
  ? ' if (event.dataTransfer && event.dataTransfer.files) { triggerContext.files = Array.from(event.dataTransfer.files); }\n'
@@ -2147,6 +2144,21 @@ const normalizeKeyName = (key: string): string => {
2147
2144
  return keyMap[key] || key
2148
2145
  }
2149
2146
 
2147
+ /**
2148
+ * The run of a page/component-loaded trigger. A delayed run is cancelled when
2149
+ * the triggers are torn down: by then its page has unmounted, or has remounted
2150
+ * for a new arrival that schedules a run of its own.
2151
+ */
2152
+ const generateLoadedTriggerRun = (
2153
+ execCall: string,
2154
+ delay: number | undefined,
2155
+ safeId: string
2156
+ ): string =>
2157
+ delay
2158
+ ? ` const __delay_${safeId} = setTimeout(function() { ${execCall}; }, ${delay});\n` +
2159
+ ` cleanups.push(function() { clearTimeout(__delay_${safeId}); });\n`
2160
+ : ` ${execCall};\n`
2161
+
2150
2162
  const generateLifecycleTrigger = (wf: UIDLWorkflow, safeId: string): string => {
2151
2163
  const trigger = wf.trigger
2152
2164
  const config = (trigger.config || {}) as Record<string, unknown>
@@ -2159,9 +2171,7 @@ const generateLifecycleTrigger = (wf: UIDLWorkflow, safeId: string): string => {
2159
2171
  ` // Page loaded (${wf.name || wf.id})\n` +
2160
2172
  ` {\n` +
2161
2173
  ` const triggerContext = { url: window.location.href, timestamp: Date.now(), referrer: document.referrer };\n` +
2162
- (delay
2163
- ? ` setTimeout(function() { ${execCall}; }, ${delay});\n`
2164
- : ` ${execCall};\n`) +
2174
+ generateLoadedTriggerRun(execCall, delay, safeId) +
2165
2175
  ` }`
2166
2176
  )
2167
2177
  }
@@ -2174,9 +2184,7 @@ const generateLifecycleTrigger = (wf: UIDLWorkflow, safeId: string): string => {
2174
2184
  ` // Component loaded (${wf.name || wf.id})\n` +
2175
2185
  ` {\n` +
2176
2186
  ` const triggerContext = { componentId: '${componentId}', componentName: '${componentName}', url: window.location.href, timestamp: Date.now() };\n` +
2177
- (delay
2178
- ? ` setTimeout(function() { ${execCall}; }, ${delay});\n`
2179
- : ` ${execCall};\n`) +
2187
+ generateLoadedTriggerRun(execCall, delay, safeId) +
2180
2188
  ` }`
2181
2189
  )
2182
2190
  }
@@ -2778,156 +2786,3 @@ const generateRealtimeUnsubscribeCode = (
2778
2786
  return ''
2779
2787
  }
2780
2788
  }
2781
-
2782
- // Component-scope triggers match by emitted component name. The GUI stamps the
2783
- // name at export time, but generators may re-case or strip spaces during
2784
- // resolution, so equality is checked case/separator-insensitively.
2785
- const normalizeComponentName = (name: string): string =>
2786
- (name || '').toLowerCase().replace(/[^a-z0-9]/g, '')
2787
-
2788
- const getRelevantWorkflows = (
2789
- workflows: UIDLWorkflows,
2790
- uidl: any,
2791
- isPage: boolean
2792
- ): UIDLWorkflow[] => {
2793
- const relevant: UIDLWorkflow[] = []
2794
- const pageId = uidl.outputOptions?.pageId || uidl.outputOptions?.fileName || uidl.name || ''
2795
-
2796
- const allWorkflows = Object.values(workflows.workflows) as UIDLWorkflow[]
2797
- allWorkflows.forEach((wf: UIDLWorkflow) => {
2798
- const trigger = wf.trigger
2799
-
2800
- if (trigger.type === 'event-cron-triggered') {
2801
- return
2802
- }
2803
-
2804
- if (trigger.type === 'event-webhook-received') {
2805
- return
2806
- }
2807
-
2808
- if (trigger.type === 'event-state-change') {
2809
- const cfg = trigger.config || {}
2810
- const stateDefs = cfg.stateDefinitions as
2811
- | Array<{
2812
- pageOrComponentId?: string
2813
- pageOrComponentName?: string
2814
- name?: string
2815
- stateDefinitionId?: string
2816
- }>
2817
- | undefined
2818
-
2819
- let boundContainerType = cfg.boundContainerType as string | undefined
2820
- if (!boundContainerType) {
2821
- if (stateDefs?.some((s) => s.pageOrComponentName)) {
2822
- boundContainerType = 'component'
2823
- } else if (!isPage && stateDefs && stateDefs.length > 0) {
2824
- boundContainerType = 'component'
2825
- } else {
2826
- boundContainerType = 'page'
2827
- }
2828
- }
2829
-
2830
- const matchesContainer = boundContainerType === 'page' ? isPage : !isPage
2831
- if (!matchesContainer) {
2832
- return
2833
- }
2834
-
2835
- const boundFromConfig = cfg.boundPageOrComponentId as string | undefined
2836
- const matchesByName =
2837
- !!stateDefs &&
2838
- stateDefs.some((sd) => sd.pageOrComponentName && sd.pageOrComponentName === uidl.name)
2839
- const matchesById =
2840
- boundFromConfig === pageId ||
2841
- (!!stateDefs && stateDefs.some((sd) => sd.pageOrComponentId === pageId))
2842
-
2843
- if (matchesByName || matchesById) {
2844
- relevant.push(wf)
2845
- }
2846
- return
2847
- }
2848
-
2849
- if (trigger.type === 'event-global-state-change') {
2850
- if (!isPage) {
2851
- return
2852
- }
2853
- const selectedPages = trigger.config.selectedPages as Array<{ id: string }> | undefined
2854
- if (
2855
- selectedPages &&
2856
- selectedPages.length > 0 &&
2857
- !selectedPages.some((p) => p.id === pageId)
2858
- ) {
2859
- return
2860
- }
2861
- relevant.push(wf)
2862
- return
2863
- }
2864
-
2865
- if (trigger.scope === 'global') {
2866
- return
2867
- }
2868
-
2869
- if (trigger.scope === 'component') {
2870
- if (isPage) {
2871
- return
2872
- }
2873
- const cfg = trigger.config || {}
2874
- const cfgName = (cfg.componentName as string) || ''
2875
- const cfgId = (cfg.componentId as string) || ''
2876
- const matches =
2877
- (cfgName &&
2878
- (cfgName === uidl.name ||
2879
- normalizeComponentName(cfgName) === normalizeComponentName(uidl.name))) ||
2880
- (cfgId && cfgId === pageId)
2881
- if (matches) {
2882
- relevant.push(wf)
2883
- }
2884
- return
2885
- }
2886
-
2887
- if (trigger.scope === 'page' && isPage) {
2888
- const triggerPageId = trigger.config.pageId as string
2889
- const selectedPages = trigger.config.selectedPages as Array<{ id: string }> | undefined
2890
-
2891
- if (trigger.type === 'event-page-loaded') {
2892
- const allPages = trigger.config.allPages === true
2893
- if (allPages) {
2894
- relevant.push(wf)
2895
- return
2896
- }
2897
- // Unscoped page-loaded workflows are SKIPPED, not injected everywhere.
2898
- // Silently fanning out to every page masks mis-scoped workflows whose
2899
- // state target lives in a component (the setter no-ops on pages).
2900
- if (!triggerPageId && (!selectedPages || selectedPages.length === 0)) {
2901
- return
2902
- }
2903
- }
2904
-
2905
- if (triggerPageId && triggerPageId !== pageId) {
2906
- return
2907
- }
2908
-
2909
- if (selectedPages && !selectedPages.some((p) => p.id === pageId)) {
2910
- return
2911
- }
2912
-
2913
- relevant.push(wf)
2914
- return
2915
- }
2916
-
2917
- if (trigger.scope === 'element') {
2918
- // If the element-scoped trigger specifies which pages it belongs to, filter by page.
2919
- // This prevents including ALL element-scoped workflows on every page (massive bloat).
2920
- const elementSelectedPages = trigger.config.selectedPages as Array<{ id: string }> | undefined
2921
- if (isPage && elementSelectedPages && elementSelectedPages.length > 0) {
2922
- if (!elementSelectedPages.some((p) => p.id === pageId)) {
2923
- return
2924
- }
2925
- }
2926
-
2927
- relevant.push(wf)
2928
- return
2929
- }
2930
- })
2931
-
2932
- return relevant
2933
- }
@@ -98,6 +98,7 @@ import {
98
98
  import { rewriteLowStockCustomHandlers } from './ecommerce-customhandler-rewriter'
99
99
  import { assertWorkflowsAreSecure } from './security-scanner'
100
100
  import { parameterizeAllWorkflowRawSql } from './raw-sql-param-binding'
101
+ import { generateBookingCalendarFeedFiles } from './booking-calendar-feed-route-generator'
101
102
 
102
103
  // Resolves the auth IDENTITY (users) table from the UIDL auth tables. Getting
103
104
  // this wrong is a security hole (the /api/data guard only protects this table),
@@ -351,6 +352,10 @@ export class NextWorkflowProjectPlugin implements ProjectPlugin {
351
352
  this.generateAuthFiles(uidl.authentication, structure)
352
353
  }
353
354
 
355
+ // The bookings' calendar feeds do not depend on any workflow, so they are
356
+ // emitted before the early return for a project without workflows.
357
+ generateBookingCalendarFeedFiles(structure)
358
+
354
359
  if (!uidl.workflows || !uidl.workflows.workflows) {
355
360
  if (uidl.invoiceSettings?.enabled) {
356
361
  const { dataSourceType, dataSourceConfig } = resolveInvoiceDataSource(structure)
@@ -1616,11 +1621,14 @@ async function customNode_${safeId}(outerContext, parameters, nodeHandlers) {
1616
1621
 
1617
1622
  // The custom node returns its last executed node's output. A fire-and-forget
1618
1623
  // node is excluded: it always stores \`null\`, so if it happens to be last it
1619
- // would turn the whole custom node's result into null for every caller.
1624
+ // would turn the whole custom node's result into null for every caller. So is
1625
+ // a node of a branch not taken: whatever a segment answered for it must never
1626
+ // stand in for the terminal of the branch that ran.
1620
1627
  var __returnableNodes = nodes
1621
1628
  .slice()
1622
1629
  .sort(function(a, b) { return a.stepNumber - b.stepNumber; })
1623
- .filter(function(n) { return !__utils.isFireAndForgetNode(n); });
1630
+ .filter(function(n) { return !__utils.isFireAndForgetNode(n); })
1631
+ .filter(function(n) { return !(context.__skippedNodes && context.__skippedNodes[n.id]); });
1624
1632
  var lastNodeId = null;
1625
1633
  for (var __ri = __returnableNodes.length - 1; __ri >= 0; __ri--) {
1626
1634
  if (context[__returnableNodes[__ri].id] !== undefined) { lastNodeId = __returnableNodes[__ri].id; break; }
@@ -1682,11 +1690,14 @@ async function customNode_${safeId}(outerContext, parameters, nodeHandlers) {
1682
1690
 
1683
1691
  // The custom node returns its last executed node's output. A fire-and-forget
1684
1692
  // node is excluded: it always stores \`null\`, so if it happens to be last it
1685
- // would turn the whole custom node's result into null for every caller.
1693
+ // would turn the whole custom node's result into null for every caller. So is
1694
+ // a node of a branch not taken: whatever a segment answered for it must never
1695
+ // stand in for the terminal of the branch that ran.
1686
1696
  var __returnableNodes = nodes
1687
1697
  .slice()
1688
1698
  .sort(function(a, b) { return a.stepNumber - b.stepNumber; })
1689
- .filter(function(n) { return !__utils.isFireAndForgetNode(n); });
1699
+ .filter(function(n) { return !__utils.isFireAndForgetNode(n); })
1700
+ .filter(function(n) { return !(context.__skippedNodes && context.__skippedNodes[n.id]); });
1690
1701
  var lastNodeId = null;
1691
1702
  for (var __ri = __returnableNodes.length - 1; __ri >= 0; __ri--) {
1692
1703
  if (context[__returnableNodes[__ri].id] !== undefined) { lastNodeId = __returnableNodes[__ri].id; break; }