Skip to content

Commit 4deabfc

Browse files
committed
improvement(tour): fix tour auto-start logic and standardize selectors
1 parent cdea240 commit 4deabfc

File tree

10 files changed

+160
-111
lines changed

10 files changed

+160
-111
lines changed

apps/sim/app/workspace/[workspaceId]/components/product-tour/nav-tour-steps.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { Step } from 'react-joyride'
22

33
export const navTourSteps: Step[] = [
44
{
5-
target: '[data-item-id="home"]',
5+
target: '[data-tour="nav-home"]',
66
title: 'Home',
77
content:
88
'Your starting point. Describe what you want to build in plain language or pick a template to get started.',
@@ -11,62 +11,62 @@ export const navTourSteps: Step[] = [
1111
spotlightPadding: 0,
1212
},
1313
{
14-
target: '[data-item-id="search"]',
14+
target: '[data-tour="nav-search"]',
1515
title: 'Search',
1616
content: 'Quickly find workflows, blocks, and tools. Use Cmd+K to open it from anywhere.',
1717
placement: 'right',
1818
disableBeacon: true,
1919
spotlightPadding: 0,
2020
},
2121
{
22-
target: '[data-item-id="tables"]',
22+
target: '[data-tour="nav-tables"]',
2323
title: 'Tables',
2424
content:
2525
'Store and query structured data. Your workflows can read and write to tables directly.',
2626
placement: 'right',
2727
disableBeacon: true,
2828
},
2929
{
30-
target: '[data-item-id="files"]',
30+
target: '[data-tour="nav-files"]',
3131
title: 'Files',
3232
content: 'Upload and manage files that your workflows can process, transform, or reference.',
3333
placement: 'right',
3434
disableBeacon: true,
3535
},
3636
{
37-
target: '[data-item-id="knowledge-base"]',
37+
target: '[data-tour="nav-knowledge-base"]',
3838
title: 'Knowledge Base',
3939
content:
4040
'Build knowledge bases from your documents. Set up connectors to give your agents realtime access to your data sources from sources like Notion, Drive, Slack, Confluence, and more.',
4141
placement: 'right',
4242
disableBeacon: true,
4343
},
4444
{
45-
target: '[data-item-id="scheduled-tasks"]',
45+
target: '[data-tour="nav-scheduled-tasks"]',
4646
title: 'Scheduled Tasks',
4747
content:
4848
'View and manage background tasks. Set up new tasks, or view the tasks the Mothership is monitoring for upcoming or past executions.',
4949
placement: 'right',
5050
disableBeacon: true,
5151
},
5252
{
53-
target: '[data-item-id="logs"]',
53+
target: '[data-tour="nav-logs"]',
5454
title: 'Logs',
5555
content:
5656
'Monitor every workflow execution. See inputs, outputs, errors, and timing for each run. View analytics on performance and costs, filter previous runs, and view snapshots of the workflow at the time of execution.',
5757
placement: 'right',
5858
disableBeacon: true,
5959
},
6060
{
61-
target: '.tasks-section',
61+
target: '[data-tour="nav-tasks"]',
6262
title: 'Tasks',
6363
content:
6464
'Tasks that work for you. Mothership can create, edit, and delete resource throughout the platform. It can also perform actions on your behalf, like sending emails, creating tasks, and more.',
6565
placement: 'right',
6666
disableBeacon: true,
6767
},
6868
{
69-
target: '.workflows-section',
69+
target: '[data-tour="nav-workflows"]',
7070
title: 'Workflows',
7171
content:
7272
'All your workflows live here. Create new ones with the + button and organize them into folders. Deploy your workflows as API, webhook, schedule, or chat widget. Then hit Run to test it out.',

apps/sim/app/workspace/[workspaceId]/components/product-tour/tour-shared.tsx

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client'
22

3-
import { createContext, useCallback, useContext, useEffect, useState } from 'react'
3+
import { createContext, useContext, useEffect, useRef, useState } from 'react'
44
import type { TooltipRenderProps } from 'react-joyride'
55
import { TourTooltip } from '@/components/emcn'
66

@@ -60,6 +60,7 @@ export function TourTooltipAdapter({
6060
}: TooltipRenderProps) {
6161
const { isTooltipVisible, isEntrance, totalSteps } = useContext(TourStateContext)
6262
const [targetEl, setTargetEl] = useState<HTMLElement | null>(null)
63+
const joyrideRef = useRef<HTMLDivElement | null>(null)
6364

6465
useEffect(() => {
6566
const { target } = step
@@ -72,21 +73,27 @@ export function TourTooltipAdapter({
7273
}
7374
}, [step])
7475

75-
const refCallback = useCallback(
76-
(node: HTMLDivElement | null) => {
77-
if (tooltipProps.ref) {
78-
;(tooltipProps.ref as React.RefCallback<HTMLDivElement>)(node)
79-
}
80-
},
81-
[tooltipProps.ref]
82-
)
76+
/**
77+
* Forwards the Joyride tooltip ref safely, handling both
78+
* callback refs and RefObject refs from the library.
79+
*/
80+
const setJoyrideRef = (node: HTMLDivElement | null) => {
81+
joyrideRef.current = node
82+
const { ref } = tooltipProps
83+
if (!ref) return
84+
if (typeof ref === 'function') {
85+
ref(node)
86+
} else {
87+
;(ref as React.MutableRefObject<HTMLDivElement | null>).current = node
88+
}
89+
}
8390

8491
const placement = mapPlacement(step.placement)
8592

8693
return (
8794
<>
8895
<div
89-
ref={refCallback}
96+
ref={setJoyrideRef}
9097
role={tooltipProps.role}
9198
aria-modal={tooltipProps['aria-modal']}
9299
style={{ position: 'absolute', opacity: 0, pointerEvents: 'none', width: 0, height: 0 }}

apps/sim/app/workspace/[workspaceId]/components/product-tour/use-tour.ts

Lines changed: 68 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,13 @@ function clearTourCompletion(storageKey: string): void {
6565
}
6666
}
6767

68+
/**
69+
* Tracks which tours have already attempted auto-start in this page session.
70+
* Module-level so it survives component remounts (e.g. navigating between
71+
* workflows remounts WorkflowTour), while still resetting on full page reload.
72+
*/
73+
const autoStartAttempted = new Set<string>()
74+
6875
/**
6976
* Shared hook for managing product tour state with smooth transitions.
7077
*
@@ -87,16 +94,51 @@ export function useTour({
8794
const [isTooltipVisible, setIsTooltipVisible] = useState(true)
8895
const [isEntrance, setIsEntrance] = useState(true)
8996

90-
const hasAutoStarted = useRef(false)
97+
const disabledRef = useRef(disabled)
9198
const retriggerTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
9299
const transitionTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
100+
const rafRef = useRef<number | null>(null)
101+
102+
useEffect(() => {
103+
disabledRef.current = disabled
104+
}, [disabled])
105+
106+
/**
107+
* Schedules a two-frame rAF to reveal the tooltip after the browser
108+
* finishes repositioning. Stores the outer frame ID in `rafRef` so
109+
* it can be cancelled on unmount or when the tour is interrupted.
110+
*/
111+
const scheduleReveal = useCallback(() => {
112+
if (rafRef.current) {
113+
cancelAnimationFrame(rafRef.current)
114+
}
115+
rafRef.current = requestAnimationFrame(() => {
116+
rafRef.current = requestAnimationFrame(() => {
117+
rafRef.current = null
118+
setIsTooltipVisible(true)
119+
})
120+
})
121+
}, [])
122+
123+
/** Cancels any pending transition timer and rAF reveal */
124+
const cancelPendingTransitions = useCallback(() => {
125+
if (transitionTimerRef.current) {
126+
clearTimeout(transitionTimerRef.current)
127+
transitionTimerRef.current = null
128+
}
129+
if (rafRef.current) {
130+
cancelAnimationFrame(rafRef.current)
131+
rafRef.current = null
132+
}
133+
}, [])
93134

94135
const stopTour = useCallback(() => {
136+
cancelPendingTransitions()
95137
setRun(false)
96138
setIsTooltipVisible(true)
97139
setIsEntrance(true)
98140
markTourCompleted(storageKey)
99-
}, [storageKey])
141+
}, [storageKey, cancelPendingTransitions])
100142

101143
/** Transition to a new step with a coordinated fade-out/fade-in */
102144
const transitionToStep = useCallback(
@@ -106,65 +148,49 @@ export function useTour({
106148
return
107149
}
108150

109-
/** Hide tooltip during transition */
110151
setIsTooltipVisible(false)
111-
112-
if (transitionTimerRef.current) {
113-
clearTimeout(transitionTimerRef.current)
114-
}
152+
cancelPendingTransitions()
115153

116154
transitionTimerRef.current = setTimeout(() => {
117155
transitionTimerRef.current = null
118156
setStepIndex(newIndex)
119157
setIsEntrance(false)
120-
121-
/**
122-
* Wait for the browser to process the Radix Popover repositioning
123-
* before showing the tooltip at the new position.
124-
*/
125-
requestAnimationFrame(() => {
126-
requestAnimationFrame(() => {
127-
setIsTooltipVisible(true)
128-
})
129-
})
158+
scheduleReveal()
130159
}, FADE_OUT_MS)
131160
},
132-
[steps.length, stopTour]
161+
[steps.length, stopTour, cancelPendingTransitions, scheduleReveal]
133162
)
134163

135164
/** Stop the tour when disabled becomes true (e.g. navigating away from the relevant page) */
136165
useEffect(() => {
137166
if (disabled && run) {
167+
cancelPendingTransitions()
138168
setRun(false)
139169
setIsTooltipVisible(true)
140170
setIsEntrance(true)
141171
logger.info(`${tourName} paused — disabled became true`)
142172
}
143-
}, [disabled, run, tourName])
173+
}, [disabled, run, tourName, cancelPendingTransitions])
144174

145-
/** Auto-start on first visit */
175+
/** Auto-start on first visit (once per page session per tour) */
146176
useEffect(() => {
147-
if (disabled || hasAutoStarted.current) return
177+
if (autoStartAttempted.has(storageKey)) return
178+
autoStartAttempted.add(storageKey)
148179

149180
const timer = setTimeout(() => {
150-
hasAutoStarted.current = true
151-
if (!isTourCompleted(storageKey)) {
152-
setStepIndex(0)
153-
setIsEntrance(true)
154-
setIsTooltipVisible(false)
155-
setRun(true)
156-
logger.info(`Auto-starting ${tourName}`)
181+
if (disabledRef.current || isTourCompleted(storageKey)) return
157182

158-
requestAnimationFrame(() => {
159-
requestAnimationFrame(() => {
160-
setIsTooltipVisible(true)
161-
})
162-
})
163-
}
183+
setStepIndex(0)
184+
setIsEntrance(true)
185+
setIsTooltipVisible(false)
186+
setRun(true)
187+
logger.info(`Auto-starting ${tourName}`)
188+
scheduleReveal()
164189
}, autoStartDelay)
165190

166191
return () => clearTimeout(timer)
167-
}, [storageKey, autoStartDelay, tourName, disabled])
192+
// eslint-disable-next-line react-hooks/exhaustive-deps
193+
}, [])
168194

169195
/** Listen for manual trigger events */
170196
useEffect(() => {
@@ -179,24 +205,14 @@ export function useTour({
179205
clearTimeout(retriggerTimerRef.current)
180206
}
181207

182-
/**
183-
* Start with the tooltip hidden so Joyride can mount, find the
184-
* target element, and position its overlay/spotlight before the
185-
* tooltip card appears.
186-
*/
187208
retriggerTimerRef.current = setTimeout(() => {
188209
retriggerTimerRef.current = null
189210
setStepIndex(0)
190211
setIsEntrance(true)
191212
setIsTooltipVisible(false)
192213
setRun(true)
193214
logger.info(`${tourName} triggered via event`)
194-
195-
requestAnimationFrame(() => {
196-
requestAnimationFrame(() => {
197-
setIsTooltipVisible(true)
198-
})
199-
})
215+
scheduleReveal()
200216
}, 50)
201217
}
202218

@@ -207,15 +223,17 @@ export function useTour({
207223
clearTimeout(retriggerTimerRef.current)
208224
}
209225
}
210-
}, [triggerEvent, resettable, storageKey, tourName])
226+
}, [triggerEvent, resettable, storageKey, tourName, scheduleReveal])
211227

228+
/** Clean up all pending async work on unmount */
212229
useEffect(() => {
213230
return () => {
214-
if (transitionTimerRef.current) {
215-
clearTimeout(transitionTimerRef.current)
231+
cancelPendingTransitions()
232+
if (retriggerTimerRef.current) {
233+
clearTimeout(retriggerTimerRef.current)
216234
}
217235
}
218-
}, [])
236+
}, [cancelPendingTransitions])
219237

220238
const handleCallback = useCallback(
221239
(data: CallBackProps) => {

apps/sim/app/workspace/[workspaceId]/components/product-tour/workflow-tour-steps.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export const workflowTourSteps: Step[] = [
1010
disableBeacon: true,
1111
},
1212
{
13-
target: '[data-tab-button="copilot"]',
13+
target: '[data-tour="tab-copilot"]',
1414
title: 'AI Copilot',
1515
content:
1616
'Build and debug workflows using natural language. Describe what you want and Copilot creates the blocks for you.',
@@ -19,7 +19,7 @@ export const workflowTourSteps: Step[] = [
1919
spotlightPadding: 0,
2020
},
2121
{
22-
target: '[data-tab-button="toolbar"]',
22+
target: '[data-tour="tab-toolbar"]',
2323
title: 'Block Library',
2424
content:
2525
'Browse all available blocks and triggers. Drag them onto the canvas to build your workflow step by step.',
@@ -28,7 +28,7 @@ export const workflowTourSteps: Step[] = [
2828
spotlightPadding: 0,
2929
},
3030
{
31-
target: '[data-tab-button="editor"]',
31+
target: '[data-tour="tab-editor"]',
3232
title: 'Block Editor',
3333
content:
3434
'Click any block on the canvas to configure it here. Set inputs, credentials, and fine-tune behavior.',

0 commit comments

Comments
 (0)