Revert "Make onboarding optional, able to be ignored on desktop" (#6610)
Revert "Make onboarding optional, able to be ignored on desktop (#6564)"
This reverts commit 820082d7f2.
This commit is contained in:
@ -1,9 +1,5 @@
|
||||
import { useCallback, useEffect } from 'react'
|
||||
import {
|
||||
type NavigateFunction,
|
||||
type useLocation,
|
||||
useNavigate,
|
||||
} from 'react-router-dom'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { waitFor } from 'xstate'
|
||||
|
||||
import { ActionButton } from '@src/components/ActionButton'
|
||||
@ -15,39 +11,30 @@ import { NetworkHealthState } from '@src/hooks/useNetworkStatus'
|
||||
import { EngineConnectionStateType } from '@src/lang/std/engineConnection'
|
||||
import { bracket } from '@src/lib/exampleKcl'
|
||||
import makeUrlPathRelative from '@src/lib/makeUrlPathRelative'
|
||||
import { joinRouterPaths, PATHS } from '@src/lib/paths'
|
||||
import {
|
||||
codeManager,
|
||||
editorManager,
|
||||
kclManager,
|
||||
systemIOActor,
|
||||
} from '@src/lib/singletons'
|
||||
import { PATHS } from '@src/lib/paths'
|
||||
import { codeManager, editorManager, kclManager } from '@src/lib/singletons'
|
||||
import { reportRejection, trap } from '@src/lib/trap'
|
||||
import { settingsActor } from '@src/lib/singletons'
|
||||
import { isKclEmptyOrOnlySettings, parse, resultIsOk } from '@src/lang/wasm'
|
||||
import { onboardingRoutes } from '@src/routes/Onboarding'
|
||||
import { onboardingPaths } from '@src/routes/Onboarding/paths'
|
||||
import { parse, resultIsOk } from '@src/lang/wasm'
|
||||
import { updateModelingState } from '@src/lang/modelingWorkflows'
|
||||
import {
|
||||
DEFAULT_PROJECT_KCL_FILE,
|
||||
EXECUTION_TYPE_REAL,
|
||||
ONBOARDING_PROJECT_NAME,
|
||||
} from '@src/lib/constants'
|
||||
import toast from 'react-hot-toast'
|
||||
import type CodeManager from '@src/lang/codeManager'
|
||||
import type { OnboardingStatus } from '@rust/kcl-lib/bindings/OnboardingStatus'
|
||||
import { isDesktop } from '@src/lib/isDesktop'
|
||||
import type { KclManager } from '@src/lang/KclSingleton'
|
||||
import { Logo } from '@src/components/Logo'
|
||||
import { SystemIOMachineEvents } from '@src/machines/systemIO/utils'
|
||||
import { ONBOARDING_SUBPATHS } from '@src/lib/onboardingPaths'
|
||||
import { EXECUTION_TYPE_REAL } from '@src/lib/constants'
|
||||
|
||||
export const kbdClasses =
|
||||
'py-0.5 px-1 text-sm rounded bg-chalkboard-10 dark:bg-chalkboard-100 border border-chalkboard-50 border-b-2'
|
||||
|
||||
// Get the 1-indexed step number of the current onboarding step
|
||||
function useStepNumber(
|
||||
slug?: (typeof ONBOARDING_SUBPATHS)[keyof typeof ONBOARDING_SUBPATHS]
|
||||
slug?: (typeof onboardingPaths)[keyof typeof onboardingPaths]
|
||||
) {
|
||||
return slug ? Object.values(ONBOARDING_SUBPATHS).indexOf(slug) + 1 : -1
|
||||
return slug
|
||||
? slug === onboardingPaths.INDEX
|
||||
? 1
|
||||
: onboardingRoutes.findIndex(
|
||||
(r) => r.path === makeUrlPathRelative(slug)
|
||||
) + 1
|
||||
: 1
|
||||
}
|
||||
|
||||
export function useDemoCode() {
|
||||
@ -93,7 +80,7 @@ export function useNextClick(newStatus: string) {
|
||||
data: { level: 'user', value: newStatus },
|
||||
})
|
||||
navigate(filePath + PATHS.ONBOARDING.INDEX.slice(0, -1) + newStatus)
|
||||
}, [filePath, newStatus, navigate])
|
||||
}, [filePath, newStatus, settingsActor.send, navigate])
|
||||
}
|
||||
|
||||
export function useDismiss() {
|
||||
@ -107,17 +94,9 @@ export function useDismiss() {
|
||||
data: { level: 'user', value: 'dismissed' },
|
||||
})
|
||||
waitFor(settingsActor, (state) => state.matches('idle'))
|
||||
.then(() => {
|
||||
navigate(filePath)
|
||||
toast.success(
|
||||
'Click the question mark in the lower-right corner if you ever want to redo the tutorial!',
|
||||
{
|
||||
duration: 5_000,
|
||||
}
|
||||
)
|
||||
})
|
||||
.then(() => navigate(filePath))
|
||||
.catch(reportRejection)
|
||||
}, [send, filePath, navigate])
|
||||
}, [send])
|
||||
|
||||
return settingsCallback
|
||||
}
|
||||
@ -128,31 +107,32 @@ export function OnboardingButtons({
|
||||
onNextOverride,
|
||||
...props
|
||||
}: {
|
||||
currentSlug?: (typeof ONBOARDING_SUBPATHS)[keyof typeof ONBOARDING_SUBPATHS]
|
||||
currentSlug?: (typeof onboardingPaths)[keyof typeof onboardingPaths]
|
||||
className?: string
|
||||
dismissClassName?: string
|
||||
onNextOverride?: () => void
|
||||
} & React.HTMLAttributes<HTMLDivElement>) {
|
||||
const onboardingPathsArray = Object.values(ONBOARDING_SUBPATHS)
|
||||
const dismiss = useDismiss()
|
||||
const stepNumber = useStepNumber(currentSlug)
|
||||
const previousStep =
|
||||
!stepNumber || stepNumber <= 1 ? null : onboardingPathsArray[stepNumber]
|
||||
const goToPrevious = useNextClick(previousStep ?? ONBOARDING_SUBPATHS.INDEX)
|
||||
!stepNumber || stepNumber === 0 ? null : onboardingRoutes[stepNumber - 2]
|
||||
const goToPrevious = useNextClick(
|
||||
onboardingPaths.INDEX + (previousStep?.path ?? '')
|
||||
)
|
||||
const nextStep =
|
||||
!stepNumber || stepNumber === onboardingPathsArray.length
|
||||
!stepNumber || stepNumber === onboardingRoutes.length
|
||||
? null
|
||||
: onboardingPathsArray[stepNumber]
|
||||
const goToNext = useNextClick(nextStep + ONBOARDING_SUBPATHS.INDEX)
|
||||
: onboardingRoutes[stepNumber]
|
||||
const goToNext = useNextClick(onboardingPaths.INDEX + (nextStep?.path ?? ''))
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
onClick={dismiss}
|
||||
className={`group block !absolute left-auto right-full top-[-3px] m-2.5 p-0 border-none bg-transparent hover:bg-transparent ${
|
||||
className={
|
||||
'group block !absolute left-auto right-full top-[-3px] m-2.5 p-0 border-none bg-transparent hover:bg-transparent ' +
|
||||
dismissClassName
|
||||
}`}
|
||||
}
|
||||
data-testid="onboarding-dismiss"
|
||||
>
|
||||
<CustomIcon
|
||||
@ -164,12 +144,16 @@ export function OnboardingButtons({
|
||||
</Tooltip>
|
||||
</button>
|
||||
<div
|
||||
className={`flex items-center justify-between ${className ?? ''}`}
|
||||
className={'flex items-center justify-between ' + (className ?? '')}
|
||||
{...props}
|
||||
>
|
||||
<ActionButton
|
||||
Element="button"
|
||||
onClick={() => (previousStep ? goToPrevious() : dismiss())}
|
||||
onClick={() =>
|
||||
previousStep?.path || previousStep?.index
|
||||
? goToPrevious()
|
||||
: dismiss()
|
||||
}
|
||||
iconStart={{
|
||||
icon: previousStep ? 'arrowLeft' : 'close',
|
||||
className: 'text-chalkboard-10',
|
||||
@ -178,18 +162,18 @@ export function OnboardingButtons({
|
||||
className="hover:border-destroy-40 hover:bg-destroy-10/50 dark:hover:bg-destroy-80/50"
|
||||
data-testid="onboarding-prev"
|
||||
>
|
||||
{previousStep ? 'Back' : 'Dismiss'}
|
||||
{previousStep ? `Back` : 'Dismiss'}
|
||||
</ActionButton>
|
||||
{stepNumber !== undefined && (
|
||||
<p className="font-mono text-xs text-center m-0">
|
||||
{stepNumber} / {onboardingPathsArray.length}
|
||||
{stepNumber} / {onboardingRoutes.length}
|
||||
</p>
|
||||
)}
|
||||
<ActionButton
|
||||
autoFocus
|
||||
Element="button"
|
||||
onClick={() => {
|
||||
if (nextStep) {
|
||||
if (nextStep?.path) {
|
||||
onNextOverride ? onNextOverride() : goToNext()
|
||||
} else {
|
||||
dismiss()
|
||||
@ -202,221 +186,9 @@ export function OnboardingButtons({
|
||||
className="dark:hover:bg-chalkboard-80/50"
|
||||
data-testid="onboarding-next"
|
||||
>
|
||||
{nextStep ? 'Next' : 'Finish'}
|
||||
{nextStep ? `Next` : 'Finish'}
|
||||
</ActionButton>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export interface OnboardingUtilDeps {
|
||||
onboardingStatus: OnboardingStatus
|
||||
codeManager: CodeManager
|
||||
kclManager: KclManager
|
||||
navigate: NavigateFunction
|
||||
}
|
||||
|
||||
export const ERROR_MUST_WARN = 'Must warn user before overwrite'
|
||||
|
||||
/**
|
||||
* Accept to begin the onboarding tutorial,
|
||||
* depending on the platform and the state of the user's code.
|
||||
*/
|
||||
export async function acceptOnboarding(deps: OnboardingUtilDeps) {
|
||||
if (isDesktop()) {
|
||||
/** TODO: rename this event to be more generic, like `createKCLFileAndNavigate` */
|
||||
systemIOActor.send({
|
||||
type: SystemIOMachineEvents.importFileFromURL,
|
||||
data: {
|
||||
requestedProjectName: ONBOARDING_PROJECT_NAME,
|
||||
requestedFileName: DEFAULT_PROJECT_KCL_FILE,
|
||||
requestedCode: bracket,
|
||||
requestedSubRoute: joinRouterPaths(
|
||||
PATHS.ONBOARDING.INDEX,
|
||||
deps.onboardingStatus
|
||||
),
|
||||
},
|
||||
})
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
const isCodeResettable = hasResetReadyCode(deps.codeManager)
|
||||
if (isCodeResettable) {
|
||||
return resetCodeAndAdvanceOnboarding(deps)
|
||||
}
|
||||
|
||||
return Promise.reject(new Error(ERROR_MUST_WARN))
|
||||
}
|
||||
|
||||
/**
|
||||
* Given that the user has accepted overwriting their web editor,
|
||||
* advance to the next step and clear their editor.
|
||||
*/
|
||||
export async function resetCodeAndAdvanceOnboarding({
|
||||
onboardingStatus,
|
||||
codeManager,
|
||||
kclManager,
|
||||
navigate,
|
||||
}: OnboardingUtilDeps) {
|
||||
// We do want to update both the state and editor here.
|
||||
codeManager.updateCodeStateEditor(bracket)
|
||||
codeManager.writeToFile().catch(reportRejection)
|
||||
kclManager.executeCode().catch(reportRejection)
|
||||
// TODO: this is not navigating to the correct `/onboarding/blah` path yet
|
||||
navigate(
|
||||
makeUrlPathRelative(
|
||||
`${PATHS.ONBOARDING.INDEX}${makeUrlPathRelative(onboardingStatus)}`
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
function hasResetReadyCode(codeManager: CodeManager) {
|
||||
return (
|
||||
isKclEmptyOrOnlySettings(codeManager.code) || codeManager.code === bracket
|
||||
)
|
||||
}
|
||||
|
||||
export function needsToOnboard(
|
||||
location: ReturnType<typeof useLocation>,
|
||||
onboardingStatus: OnboardingStatus
|
||||
) {
|
||||
return (
|
||||
!location.pathname.includes(PATHS.ONBOARDING.INDEX) &&
|
||||
(onboardingStatus.length === 0 ||
|
||||
!(onboardingStatus === 'completed' || onboardingStatus === 'dismissed'))
|
||||
)
|
||||
}
|
||||
|
||||
export const ONBOARDING_TOAST_ID = 'onboarding-toast'
|
||||
|
||||
export function onDismissOnboardingInvite() {
|
||||
settingsActor.send({
|
||||
type: 'set.app.onboardingStatus',
|
||||
data: { level: 'user', value: 'dismissed' },
|
||||
})
|
||||
toast.dismiss(ONBOARDING_TOAST_ID)
|
||||
toast.success(
|
||||
'Click the question mark in the lower-right corner if you ever want to do the tutorial!',
|
||||
{
|
||||
duration: 5_000,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export function TutorialRequestToast(props: OnboardingUtilDeps) {
|
||||
function onAccept() {
|
||||
acceptOnboarding(props)
|
||||
.then(() => {
|
||||
toast.dismiss(ONBOARDING_TOAST_ID)
|
||||
})
|
||||
.catch((reason) => catchOnboardingWarnError(reason, props))
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
data-testid="onboarding-toast"
|
||||
className="flex items-center gap-6 min-w-80"
|
||||
>
|
||||
<Logo className="w-auto h-8 flex-none" />
|
||||
<div className="flex flex-col justify-between gap-6">
|
||||
<section>
|
||||
<h2>Welcome to Zoo Design Studio</h2>
|
||||
<p className="text-sm text-chalkboard-70 dark:text-chalkboard-30">
|
||||
Would you like a tutorial to show you around the app?
|
||||
</p>
|
||||
</section>
|
||||
<div className="flex justify-between gap-8">
|
||||
<ActionButton
|
||||
Element="button"
|
||||
iconStart={{
|
||||
icon: 'close',
|
||||
}}
|
||||
data-negative-button="dismiss"
|
||||
name="dismiss"
|
||||
onClick={onDismissOnboardingInvite}
|
||||
>
|
||||
Not right now
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
Element="button"
|
||||
iconStart={{
|
||||
icon: 'checkmark',
|
||||
}}
|
||||
name="accept"
|
||||
onClick={onAccept}
|
||||
>
|
||||
Get started
|
||||
</ActionButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to catch the `ERROR_MUST_WARN` error from
|
||||
* `acceptOnboarding` and show a warning toast.
|
||||
*/
|
||||
export async function catchOnboardingWarnError(
|
||||
err: Error,
|
||||
props: OnboardingUtilDeps
|
||||
) {
|
||||
if (err instanceof Error && err.message === ERROR_MUST_WARN) {
|
||||
toast.success(TutorialWebConfirmationToast(props), {
|
||||
id: ONBOARDING_TOAST_ID,
|
||||
duration: Number.POSITIVE_INFINITY,
|
||||
icon: null,
|
||||
})
|
||||
} else {
|
||||
toast.dismiss(ONBOARDING_TOAST_ID)
|
||||
return reportRejection(err)
|
||||
}
|
||||
}
|
||||
|
||||
export function TutorialWebConfirmationToast(props: OnboardingUtilDeps) {
|
||||
function onAccept() {
|
||||
toast.dismiss(ONBOARDING_TOAST_ID)
|
||||
resetCodeAndAdvanceOnboarding(props).catch(reportRejection)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
data-testid="onboarding-toast-confirmation"
|
||||
className="flex items-center gap-6 min-w-80"
|
||||
>
|
||||
<Logo className="w-auto h-8 flex-none" />
|
||||
<div className="flex flex-col justify-between gap-6">
|
||||
<section>
|
||||
<h2>The welcome tutorial resets your code in the browser</h2>
|
||||
<p className="text-sm text-chalkboard-70 dark:text-chalkboard-30">
|
||||
We see you have some of your own code written in this project.
|
||||
Please save it somewhere else before continuing the onboarding.
|
||||
</p>
|
||||
</section>
|
||||
<div className="flex justify-between gap-8">
|
||||
<ActionButton
|
||||
Element="button"
|
||||
iconStart={{
|
||||
icon: 'close',
|
||||
}}
|
||||
data-negative-button="dismiss"
|
||||
name="dismiss"
|
||||
onClick={onDismissOnboardingInvite}
|
||||
>
|
||||
I'll save it
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
Element="button"
|
||||
iconStart={{
|
||||
icon: 'checkmark',
|
||||
}}
|
||||
name="accept"
|
||||
onClick={onAccept}
|
||||
>
|
||||
Overwrite and begin
|
||||
</ActionButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user