Revert "Make onboarding optional, able to be ignored on desktop (#6564)"
This reverts commit 820082d7f2
.
195 lines
6.5 KiB
TypeScript
195 lines
6.5 KiB
TypeScript
import { useCallback, useEffect } from 'react'
|
|
import { useNavigate } from 'react-router-dom'
|
|
import { waitFor } from 'xstate'
|
|
|
|
import { ActionButton } from '@src/components/ActionButton'
|
|
import { CustomIcon } from '@src/components/CustomIcon'
|
|
import Tooltip from '@src/components/Tooltip'
|
|
import { useAbsoluteFilePath } from '@src/hooks/useAbsoluteFilePath'
|
|
import { useNetworkContext } from '@src/hooks/useNetworkContext'
|
|
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 { 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 { 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 { 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 onboardingPaths)[keyof typeof onboardingPaths]
|
|
) {
|
|
return slug
|
|
? slug === onboardingPaths.INDEX
|
|
? 1
|
|
: onboardingRoutes.findIndex(
|
|
(r) => r.path === makeUrlPathRelative(slug)
|
|
) + 1
|
|
: 1
|
|
}
|
|
|
|
export function useDemoCode() {
|
|
const { overallState, immediateState } = useNetworkContext()
|
|
|
|
useEffect(() => {
|
|
async function setCodeToDemoIfNeeded() {
|
|
// Don't run if the editor isn't loaded or the code is already the bracket
|
|
if (!editorManager.editorView || codeManager.code === bracket) {
|
|
return
|
|
}
|
|
// Don't run if the network isn't healthy or the connection isn't established
|
|
if (
|
|
overallState === NetworkHealthState.Disconnected ||
|
|
overallState === NetworkHealthState.Issue ||
|
|
immediateState.type !== EngineConnectionStateType.ConnectionEstablished
|
|
) {
|
|
return
|
|
}
|
|
const pResult = parse(bracket)
|
|
if (trap(pResult) || !resultIsOk(pResult)) {
|
|
return Promise.reject(pResult)
|
|
}
|
|
const ast = pResult.program
|
|
await updateModelingState(ast, EXECUTION_TYPE_REAL, {
|
|
kclManager: kclManager,
|
|
editorManager: editorManager,
|
|
codeManager: codeManager,
|
|
})
|
|
}
|
|
|
|
setCodeToDemoIfNeeded().catch(reportRejection)
|
|
}, [editorManager.editorView, immediateState.type, overallState])
|
|
}
|
|
|
|
export function useNextClick(newStatus: string) {
|
|
const filePath = useAbsoluteFilePath()
|
|
const navigate = useNavigate()
|
|
|
|
return useCallback(() => {
|
|
settingsActor.send({
|
|
type: 'set.app.onboardingStatus',
|
|
data: { level: 'user', value: newStatus },
|
|
})
|
|
navigate(filePath + PATHS.ONBOARDING.INDEX.slice(0, -1) + newStatus)
|
|
}, [filePath, newStatus, settingsActor.send, navigate])
|
|
}
|
|
|
|
export function useDismiss() {
|
|
const filePath = useAbsoluteFilePath()
|
|
const send = settingsActor.send
|
|
const navigate = useNavigate()
|
|
|
|
const settingsCallback = useCallback(() => {
|
|
send({
|
|
type: 'set.app.onboardingStatus',
|
|
data: { level: 'user', value: 'dismissed' },
|
|
})
|
|
waitFor(settingsActor, (state) => state.matches('idle'))
|
|
.then(() => navigate(filePath))
|
|
.catch(reportRejection)
|
|
}, [send])
|
|
|
|
return settingsCallback
|
|
}
|
|
export function OnboardingButtons({
|
|
currentSlug,
|
|
className,
|
|
dismissClassName,
|
|
onNextOverride,
|
|
...props
|
|
}: {
|
|
currentSlug?: (typeof onboardingPaths)[keyof typeof onboardingPaths]
|
|
className?: string
|
|
dismissClassName?: string
|
|
onNextOverride?: () => void
|
|
} & React.HTMLAttributes<HTMLDivElement>) {
|
|
const dismiss = useDismiss()
|
|
const stepNumber = useStepNumber(currentSlug)
|
|
const previousStep =
|
|
!stepNumber || stepNumber === 0 ? null : onboardingRoutes[stepNumber - 2]
|
|
const goToPrevious = useNextClick(
|
|
onboardingPaths.INDEX + (previousStep?.path ?? '')
|
|
)
|
|
const nextStep =
|
|
!stepNumber || stepNumber === onboardingRoutes.length
|
|
? null
|
|
: onboardingRoutes[stepNumber]
|
|
const goToNext = useNextClick(onboardingPaths.INDEX + (nextStep?.path ?? ''))
|
|
|
|
return (
|
|
<>
|
|
<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 ' +
|
|
dismissClassName
|
|
}
|
|
data-testid="onboarding-dismiss"
|
|
>
|
|
<CustomIcon
|
|
name="close"
|
|
className="w-5 h-5 rounded-sm bg-destroy-10 text-destroy-80 dark:bg-destroy-80 dark:text-destroy-10 group-hover:brightness-110"
|
|
/>
|
|
<Tooltip position="bottom">
|
|
Dismiss <kbd className="hotkey ml-4 dark:!bg-chalkboard-80">esc</kbd>
|
|
</Tooltip>
|
|
</button>
|
|
<div
|
|
className={'flex items-center justify-between ' + (className ?? '')}
|
|
{...props}
|
|
>
|
|
<ActionButton
|
|
Element="button"
|
|
onClick={() =>
|
|
previousStep?.path || previousStep?.index
|
|
? goToPrevious()
|
|
: dismiss()
|
|
}
|
|
iconStart={{
|
|
icon: previousStep ? 'arrowLeft' : 'close',
|
|
className: 'text-chalkboard-10',
|
|
bgClassName: 'bg-destroy-80 group-hover:bg-destroy-80',
|
|
}}
|
|
className="hover:border-destroy-40 hover:bg-destroy-10/50 dark:hover:bg-destroy-80/50"
|
|
data-testid="onboarding-prev"
|
|
>
|
|
{previousStep ? `Back` : 'Dismiss'}
|
|
</ActionButton>
|
|
{stepNumber !== undefined && (
|
|
<p className="font-mono text-xs text-center m-0">
|
|
{stepNumber} / {onboardingRoutes.length}
|
|
</p>
|
|
)}
|
|
<ActionButton
|
|
autoFocus
|
|
Element="button"
|
|
onClick={() => {
|
|
if (nextStep?.path) {
|
|
onNextOverride ? onNextOverride() : goToNext()
|
|
} else {
|
|
dismiss()
|
|
}
|
|
}}
|
|
iconStart={{
|
|
icon: nextStep ? 'arrowRight' : 'checkmark',
|
|
bgClassName: 'dark:bg-chalkboard-80',
|
|
}}
|
|
className="dark:hover:bg-chalkboard-80/50"
|
|
data-testid="onboarding-next"
|
|
>
|
|
{nextStep ? `Next` : 'Finish'}
|
|
</ActionButton>
|
|
</div>
|
|
</>
|
|
)
|
|
}
|