diff --git a/e2e/playwright/fixtures/homePageFixture.ts b/e2e/playwright/fixtures/homePageFixture.ts
index 43765161682..4878e5fbb93 100644
--- a/e2e/playwright/fixtures/homePageFixture.ts
+++ b/e2e/playwright/fixtures/homePageFixture.ts
@@ -24,7 +24,6 @@ export class HomePageFixture {
projectTextName!: Locator
sortByDateBtn!: Locator
sortByNameBtn!: Locator
- tutorialBtn!: Locator
constructor(page: Page) {
this.page = page
@@ -44,7 +43,6 @@ export class HomePageFixture {
this.sortByDateBtn = this.page.getByTestId('home-sort-by-modified')
this.sortByNameBtn = this.page.getByTestId('home-sort-by-name')
- this.tutorialBtn = this.page.getByTestId('home-tutorial-button')
}
private _serialiseSortBy = async (): Promise<
diff --git a/e2e/playwright/fixtures/toolbarFixture.ts b/e2e/playwright/fixtures/toolbarFixture.ts
index ae51d659acd..ed7bffe79c9 100644
--- a/e2e/playwright/fixtures/toolbarFixture.ts
+++ b/e2e/playwright/fixtures/toolbarFixture.ts
@@ -17,8 +17,6 @@ type LengthUnitLabel = (typeof baseUnitLabels)[keyof typeof baseUnitLabels]
export class ToolbarFixture {
public page: Page
- projectName!: Locator
- fileName!: Locator
extrudeButton!: Locator
loftButton!: Locator
sweepButton!: Locator
@@ -55,8 +53,6 @@ export class ToolbarFixture {
constructor(page: Page) {
this.page = page
- this.projectName = page.getByTestId('app-header-project-name')
- this.fileName = page.getByTestId('app-header-file-name')
this.extrudeButton = page.getByTestId('extrude')
this.loftButton = page.getByTestId('loft')
this.sweepButton = page.getByTestId('sweep')
diff --git a/e2e/playwright/native-file-menu.spec.ts b/e2e/playwright/native-file-menu.spec.ts
index 5c6dc4d725d..d9967f06efa 100644
--- a/e2e/playwright/native-file-menu.spec.ts
+++ b/e2e/playwright/native-file-menu.spec.ts
@@ -450,7 +450,7 @@ test.describe(
)
await expect(actual).toBeVisible()
})
- test('Home.Help.Replay onboarding tutorial', async ({
+ test('Home.Help.Reset onboarding', async ({
tronApp,
cmdBar,
page,
@@ -464,7 +464,7 @@ test.describe(
await tronApp.electron.evaluate(async ({ app }) => {
if (!app || !app.applicationMenu) return false
const menu = app.applicationMenu.getMenuItemById(
- 'Help.Replay onboarding tutorial'
+ 'Help.Reset onboarding'
)
if (!menu) {
return false
@@ -2339,7 +2339,7 @@ test.describe(
await scene.connectionEstablished()
await expect(toolbar.startSketchBtn).toBeVisible()
})
- test('Modeling.Help.Replay onboarding tutorial', async ({
+ test('Modeling.Help.Reset onboarding', async ({
tronApp,
cmdBar,
page,
@@ -2358,7 +2358,7 @@ test.describe(
await tronApp.electron.evaluate(async ({ app }) => {
if (!app || !app.applicationMenu) fail()
const menu = app.applicationMenu.getMenuItemById(
- 'Help.Replay onboarding tutorial'
+ 'Help.Reset onboarding'
)
if (!menu) fail()
menu.click()
diff --git a/e2e/playwright/onboarding-tests.spec.ts b/e2e/playwright/onboarding-tests.spec.ts
index 6febaffc8e1..a2e776c5617 100644
--- a/e2e/playwright/onboarding-tests.spec.ts
+++ b/e2e/playwright/onboarding-tests.spec.ts
@@ -1,175 +1,560 @@
+import { join } from 'path'
+import { bracket } from '@e2e/playwright/fixtures/bracket'
+import { onboardingPaths } from '@src/routes/Onboarding/paths'
+import fsp from 'fs/promises'
+
+import { expectPixelColor } from '@e2e/playwright/fixtures/sceneFixture'
+import {
+ TEST_SETTINGS_KEY,
+ TEST_SETTINGS_ONBOARDING_EXPORT,
+ TEST_SETTINGS_ONBOARDING_START,
+ TEST_SETTINGS_ONBOARDING_USER_MENU,
+} from '@e2e/playwright/storageStates'
+import {
+ createProject,
+ executorInputPath,
+ getUtils,
+ settingsToToml,
+} from '@e2e/playwright/test-utils'
import { expect, test } from '@e2e/playwright/zoo-test'
+// Because our default test settings have the onboardingStatus set to 'dismissed',
+// we must set it to empty for the tests where we want to see the onboarding immediately.
+
test.describe('Onboarding tests', () => {
- test('Desktop onboarding flow works', async ({
+ test('Onboarding code is shown in the editor', async ({
page,
homePage,
- toolbar,
- editor,
- scene,
tronApp,
}) => {
if (!tronApp) {
fail()
}
-
- // Because our default test settings have the onboardingStatus set to 'dismissed',
- // we must set it to empty for the tests where we want to see the onboarding UI.
await tronApp.cleanProjectDir({
app: {
onboarding_status: '',
},
})
- const bracketComment = '// Shelf Bracket'
- const tutorialWelcomHeading = page.getByText(
- 'Welcome to Design Studio! This'
+ const u = await getUtils(page)
+ await page.setBodyDimensions({ width: 1200, height: 500 })
+ await homePage.goToModelingScene()
+
+ // Test that the onboarding pane loaded
+ await expect(page.getByText('Welcome to Design Studio! This')).toBeVisible()
+
+ // Test that the onboarding pane loaded
+ await expect(page.getByText('Welcome to Design Studio! This')).toBeVisible()
+
+ // *and* that the code is shown in the editor
+ await expect(page.locator('.cm-content')).toContainText('// Shelf Bracket')
+
+ // Make sure the model loaded
+ const XYPlanePoint = { x: 774, y: 116 } as const
+ const modelColor: [number, number, number] = [45, 45, 45]
+ await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y)
+ expect(await u.getGreatestPixDiff(XYPlanePoint, modelColor)).toBeLessThan(8)
+ })
+
+ test(
+ 'Desktop: fresh onboarding executes and loads',
+ {
+ tag: '@electron',
+ },
+ async ({ page, tronApp, scene }) => {
+ if (!tronApp) {
+ fail()
+ }
+ await tronApp.cleanProjectDir({
+ app: {
+ onboarding_status: '',
+ },
+ })
+
+ const viewportSize = { width: 1200, height: 500 }
+ await page.setBodyDimensions(viewportSize)
+
+ await test.step(`Create a project and open to the onboarding`, async () => {
+ await createProject({ name: 'project-link', page })
+ await test.step(`Ensure the engine connection works by testing the sketch button`, async () => {
+ await scene.connectionEstablished()
+ })
+ })
+
+ await test.step(`Ensure we see the onboarding stuff`, async () => {
+ // Test that the onboarding pane loaded
+ await expect(
+ page.getByText('Welcome to Design Studio! This')
+ ).toBeVisible()
+
+ // *and* that the code is shown in the editor
+ await expect(page.locator('.cm-content')).toContainText(
+ '// Shelf Bracket'
+ )
+
+ // TODO: jess make less shit
+ // Make sure the model loaded
+ //const XYPlanePoint = { x: 986, y: 522 } as const
+ //const modelColor: [number, number, number] = [76, 76, 76]
+ //await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y)
+
+ //await expectPixelColor(page, modelColor, XYPlanePoint, 8)
+ })
+ }
+ )
+
+ test('Code resets after confirmation', async ({
+ page,
+ homePage,
+ tronApp,
+ scene,
+ }) => {
+ if (!tronApp) {
+ fail()
+ }
+ await tronApp.cleanProjectDir()
+
+ const initialCode = `sketch001 = startSketchOn(XZ)`
+
+ // Load the page up with some code so we see the confirmation warning
+ // when we go to replay onboarding
+ await page.addInitScript((code) => {
+ localStorage.setItem('persistCode', code)
+ }, initialCode)
+
+ await page.setBodyDimensions({ width: 1200, height: 500 })
+ await homePage.goToModelingScene()
+ await scene.connectionEstablished()
+
+ // Replay the onboarding
+ await page.getByRole('link', { name: 'Settings' }).last().click()
+ const replayButton = page.getByRole('button', {
+ name: 'Replay onboarding',
+ })
+ await expect(replayButton).toBeVisible()
+ await replayButton.click()
+
+ // Ensure we see the warning, and that the code has not yet updated
+ await expect(page.getByText('Would you like to create')).toBeVisible()
+ await expect(page.locator('.cm-content')).toHaveText(initialCode)
+
+ const nextButton = page.getByTestId('onboarding-next')
+ await nextButton.hover()
+ await nextButton.click()
+
+ // Ensure we see the introduction and that the code has been reset
+ await expect(page.getByText('Welcome to Design Studio!')).toBeVisible()
+ await expect(page.locator('.cm-content')).toContainText('// Shelf Bracket')
+
+ // There used to be old code here that checked if we stored the reset
+ // code into localStorage but that isn't the case on desktop. It gets
+ // saved to the file system, which we have other tests for.
+ })
+
+ test('Click through each onboarding step and back', async ({
+ context,
+ page,
+ homePage,
+ tronApp,
+ }) => {
+ if (!tronApp) {
+ fail()
+ }
+ await tronApp.cleanProjectDir({
+ app: {
+ onboarding_status: '',
+ },
+ })
+ // Override beforeEach test setup
+ await context.addInitScript(
+ async ({ settingsKey, settings }) => {
+ // Give no initial code, so that the onboarding start is shown immediately
+ localStorage.setItem('persistCode', '')
+ localStorage.setItem(settingsKey, settings)
+ },
+ {
+ settingsKey: TEST_SETTINGS_KEY,
+ settings: settingsToToml({
+ settings: TEST_SETTINGS_ONBOARDING_START,
+ }),
+ }
)
+
+ await page.setBodyDimensions({ width: 1200, height: 1080 })
+ await homePage.goToModelingScene()
+
+ // Test that the onboarding pane loaded
+ await expect(page.getByText('Welcome to Design Studio! This')).toBeVisible()
+
const nextButton = page.getByTestId('onboarding-next')
const prevButton = page.getByTestId('onboarding-prev')
- const userMenuButton = toolbar.userSidebarButton
- const userMenuSettingsButton = page.getByRole('button', {
- name: 'User settings',
- })
- const settingsHeading = page.getByRole('heading', {
- name: 'Settings',
- exact: true,
- })
- const restartOnboardingSettingsButton = page.getByRole('button', {
- name: 'Replay onboarding',
+
+ while ((await nextButton.innerText()) !== 'Finish') {
+ await nextButton.hover()
+ await nextButton.click()
+ }
+
+ while ((await prevButton.innerText()) !== 'Dismiss') {
+ await prevButton.hover()
+ await prevButton.click()
+ }
+
+ // Dismiss the onboarding
+ await prevButton.hover()
+ await prevButton.click()
+
+ // Test that the onboarding pane is gone
+ await expect(page.getByTestId('onboarding-content')).not.toBeVisible()
+ await expect.poll(() => page.url()).not.toContain('/onboarding')
+ })
+
+ test('Onboarding redirects and code updating', async ({
+ context,
+ page,
+ homePage,
+ tronApp,
+ }) => {
+ if (!tronApp) {
+ fail()
+ }
+ await tronApp.cleanProjectDir({
+ app: {
+ onboarding_status: '/export',
+ },
})
- const helpMenuButton = page.getByRole('button', {
- name: 'Help and resources',
+
+ const originalCode = 'sigmaAllow = 15000'
+
+ // Override beforeEach test setup
+ await context.addInitScript(
+ async ({ settingsKey, settings, code }) => {
+ // Give some initial code, so we can test that it's cleared
+ localStorage.setItem('persistCode', code)
+ localStorage.setItem(settingsKey, settings)
+ },
+ {
+ settingsKey: TEST_SETTINGS_KEY,
+ settings: settingsToToml({
+ settings: TEST_SETTINGS_ONBOARDING_EXPORT,
+ }),
+ code: originalCode,
+ }
+ )
+
+ await page.setBodyDimensions({ width: 1200, height: 500 })
+ await homePage.goToModelingScene()
+
+ // Test that the redirect happened
+ await expect.poll(() => page.url()).toContain('/onboarding/export')
+
+ // Test that you come back to this page when you refresh
+ await page.reload()
+ await expect.poll(() => page.url()).toContain('/onboarding/export')
+
+ // Test that the code changes when you advance to the next step
+ await page.getByTestId('onboarding-next').hover()
+ await page.getByTestId('onboarding-next').click()
+
+ // Test that the onboarding pane loaded
+ const title = page.locator('[data-testid="onboarding-content"]')
+ await expect(title).toBeAttached()
+
+ await expect(page.locator('.cm-content')).not.toHaveText(originalCode)
+
+ // Test that the code is not empty when you click on the next step
+ await page.locator('[data-testid="onboarding-next"]').hover()
+ await page.locator('[data-testid="onboarding-next"]').click()
+ await expect(page.locator('.cm-content')).toHaveText(/.+/)
+ })
+
+ test('Onboarding code gets reset to demo on Interactive Numbers step', async ({
+ page,
+ homePage,
+ tronApp,
+ editor,
+ toolbar,
+ }) => {
+ if (!tronApp) {
+ fail()
+ }
+ await tronApp.cleanProjectDir({
+ app: {
+ onboarding_status: '/parametric-modeling',
+ },
})
- const helpMenuRestartOnboardingButton = page.getByRole('button', {
- name: 'Replay onboarding tutorial',
+
+ const badCode = `// This is bad code we shouldn't see`
+
+ await page.setBodyDimensions({ width: 1200, height: 1080 })
+ await homePage.goToModelingScene()
+
+ await expect
+ .poll(() => page.url())
+ .toContain(onboardingPaths.PARAMETRIC_MODELING)
+
+ // Check the code got reset on load
+ await toolbar.openPane('code')
+ await editor.expectEditor.toContain(bracket, {
+ shouldNormalise: true,
+ timeout: 10_000,
})
- const postDismissToast = page.getByText(
- 'Click the question mark in the lower-right corner if you ever want to redo the tutorial!'
- )
- await test.step('Test initial home page view, showing a tutorial button', async () => {
- await expect(homePage.tutorialBtn).toBeVisible()
- await homePage.expectState({
- projectCards: [],
- sortBy: 'last-modified-desc',
- })
+ // Mess with the code again
+ await editor.replaceCode('', badCode)
+ await editor.expectEditor.toContain(badCode, {
+ shouldNormalise: true,
+ timeout: 10_000,
})
- await test.step('Create a blank project and verify no onboarding chrome is shown', async () => {
- await homePage.goToModelingScene()
- await expect(toolbar.projectName).toContainText('testDefault')
- await expect(tutorialWelcomHeading).not.toBeVisible()
- await editor.expectEditor.toContain('@settings(defaultLengthUnit = in)', {
- shouldNormalise: true,
- })
- await scene.connectionEstablished()
- await expect(toolbar.startSketchBtn).toBeEnabled({ timeout: 15_000 })
+ // Click to the next step
+ await page.locator('[data-testid="onboarding-next"]').hover()
+ await page.locator('[data-testid="onboarding-next"]').click()
+ await page.waitForURL('**' + onboardingPaths.INTERACTIVE_NUMBERS, {
+ waitUntil: 'domcontentloaded',
})
- await test.step('Go home and verify we still see the tutorial button, then begin it.', async () => {
- await toolbar.logoLink.click()
- await expect(homePage.tutorialBtn).toBeVisible()
- await homePage.expectState({
- projectCards: [
- {
- title: 'testDefault',
- fileCount: 1,
- },
- ],
- sortBy: 'last-modified-desc',
- })
- await homePage.tutorialBtn.click()
+ // Check that the code has been reset
+ await editor.expectEditor.toContain(bracket, {
+ shouldNormalise: true,
+ timeout: 10_000,
})
+ })
+
+ // (lee) The two avatar tests are weird because even on main, we don't have
+ // anything to do with the avatar inside the onboarding test. Due to the
+ // low impact of an avatar not showing I'm changing this to fixme.
+ test('Avatar text updates depending on image load success', async ({
+ context,
+ page,
+ toolbar,
+ homePage,
+ tronApp,
+ }) => {
+ if (!tronApp) {
+ fail()
+ }
- // This is web-only.
- // TODO: write a new test just for the onboarding in browser
- // await test.step('Ensure the onboarding request toast appears', async () => {
- // await expect(page.getByTestId('onboarding-toast')).toBeVisible()
- // await page.getByTestId('onboarding-next').click()
- // })
-
- await test.step('Ensure we see the welcome screen in a new project', async () => {
- await expect(toolbar.projectName).toContainText('Tutorial Project 00')
- await expect(tutorialWelcomHeading).toBeVisible()
- await editor.expectEditor.toContain(bracketComment)
- await scene.connectionEstablished()
- await expect(toolbar.startSketchBtn).toBeEnabled({ timeout: 15_000 })
+ await tronApp.cleanProjectDir({
+ app: {
+ onboarding_status: '',
+ },
})
- await test.step('Test the clicking through the onboarding flow', async () => {
- await test.step('Going forward', async () => {
- while ((await nextButton.innerText()) !== 'Finish') {
- await nextButton.hover()
- await nextButton.click()
- }
- })
+ // Override beforeEach test setup
+ await context.addInitScript(
+ async ({ settingsKey, settings }) => {
+ localStorage.setItem(settingsKey, settings)
+ },
+ {
+ settingsKey: TEST_SETTINGS_KEY,
+ settings: settingsToToml({
+ settings: TEST_SETTINGS_ONBOARDING_USER_MENU,
+ }),
+ }
+ )
- await test.step('Going backward', async () => {
- while ((await prevButton.innerText()) !== 'Dismiss') {
- await prevButton.hover()
- await prevButton.click()
- }
- })
+ await page.setBodyDimensions({ width: 1200, height: 500 })
+ await homePage.goToModelingScene()
- // Dismiss the onboarding
- await test.step('Dismiss the onboarding', async () => {
- await prevButton.hover()
- await prevButton.click()
- await expect(page.getByTestId('onboarding-content')).not.toBeVisible()
- await expect(postDismissToast).toBeVisible()
- await expect.poll(() => page.url()).not.toContain('/onboarding')
+ // Test that the text in this step is correct
+ const avatarLocator = toolbar.userSidebarButton.locator('img')
+ const onboardingOverlayLocator = page
+ .getByTestId('onboarding-content')
+ .locator('div')
+ .nth(1)
+
+ // Expect the avatar to be visible and for the text to reference it
+ await expect(avatarLocator).toBeVisible()
+ await expect(onboardingOverlayLocator).toBeVisible()
+ await expect(onboardingOverlayLocator).toContainText('your avatar')
+
+ // This is to force the avatar to 404.
+ // For our test image (only triggers locally. on CI, it's Kurt's /
+ // gravatar image )
+ await page.route('/cat.jpg', async (route) => {
+ await route.fulfill({
+ status: 404,
+ contentType: 'text/plain',
+ body: 'Not Found!',
})
})
- await test.step('Resetting onboarding from inside project should always make a new one', async () => {
- await test.step('Reset onboarding from settings', async () => {
- await userMenuButton.click()
- await userMenuSettingsButton.click()
- await expect(settingsHeading).toBeVisible()
- await expect(restartOnboardingSettingsButton).toBeVisible()
- await restartOnboardingSettingsButton.click()
+ // 404 the CI avatar image
+ await page.route('https://lh3.googleusercontent.com/**', async (route) => {
+ await route.fulfill({
+ status: 404,
+ contentType: 'text/plain',
+ body: 'Not Found!',
})
+ })
- await test.step('Makes a new project', async () => {
- await expect(toolbar.projectName).toContainText('Tutorial Project 01')
- await expect(tutorialWelcomHeading).toBeVisible()
- await editor.expectEditor.toContain(bracketComment)
- await scene.connectionEstablished()
- await expect(toolbar.startSketchBtn).toBeEnabled({ timeout: 15_000 })
- })
+ await page.reload({ waitUntil: 'domcontentloaded' })
- await test.step('Dismiss the onboarding', async () => {
- await postDismissToast.waitFor({ state: 'detached' })
- await page.keyboard.press('Escape')
- await expect(postDismissToast).toBeVisible()
- await expect(page.getByTestId('onboarding-content')).not.toBeVisible()
- await expect.poll(() => page.url()).not.toContain('/onboarding')
- })
+ // Now expect the text to be different
+ await expect(avatarLocator).not.toBeVisible()
+ await expect(onboardingOverlayLocator).toBeVisible()
+ await expect(onboardingOverlayLocator).toContainText('the menu button')
+ })
+
+ test("Avatar text doesn't mention avatar when no avatar", async ({
+ context,
+ page,
+ toolbar,
+ homePage,
+ tronApp,
+ }) => {
+ if (!tronApp) {
+ fail()
+ }
+
+ await tronApp.cleanProjectDir({
+ app: {
+ onboarding_status: '',
+ },
})
+ // Override beforeEach test setup
+ await context.addInitScript(
+ async ({ settingsKey, settings }) => {
+ localStorage.setItem(settingsKey, settings)
+ localStorage.setItem('FORCE_NO_IMAGE', 'FORCE_NO_IMAGE')
+ },
+ {
+ settingsKey: TEST_SETTINGS_KEY,
+ settings: settingsToToml({
+ settings: TEST_SETTINGS_ONBOARDING_USER_MENU,
+ }),
+ }
+ )
- await test.step('Resetting onboarding from home help menu makes a new project', async () => {
- await test.step('Go home and reset onboarding from lower-right help menu', async () => {
- await toolbar.logoLink.click()
- await expect(homePage.tutorialBtn).not.toBeVisible()
- await expect(
- homePage.projectCard.getByText('Tutorial Project 00')
- ).toBeVisible()
- await expect(
- homePage.projectCard.getByText('Tutorial Project 01')
- ).toBeVisible()
+ await page.setBodyDimensions({ width: 1200, height: 500 })
+ await homePage.goToModelingScene()
- await helpMenuButton.click()
- await helpMenuRestartOnboardingButton.click()
- })
+ // Test that the text in this step is correct
+ const sidebar = toolbar.userSidebarButton
+ const avatar = sidebar.locator('img')
+ const onboardingOverlayLocator = page
+ .getByTestId('onboarding-content')
+ .locator('div')
+ .nth(1)
- await test.step('Makes a new project', async () => {
- await expect(toolbar.projectName).toContainText('Tutorial Project 02')
- await expect(tutorialWelcomHeading).toBeVisible()
- await editor.expectEditor.toContain(bracketComment)
- await scene.connectionEstablished()
- await expect(toolbar.startSketchBtn).toBeEnabled({ timeout: 15_000 })
- })
+ // Expect the avatar to be visible and for the text to reference it
+ await expect(avatar).not.toBeVisible()
+ await expect(onboardingOverlayLocator).toBeVisible()
+ await expect(onboardingOverlayLocator).toContainText('the menu button')
+
+ // Test we mention what else is in this menu for https://github.com/KittyCAD/modeling-app/issues/2939
+ // which doesn't deserver its own full test spun up
+ const userMenuFeatures = [
+ 'manage your account',
+ 'report a bug',
+ 'request a feature',
+ 'sign out',
+ ]
+ for (const feature of userMenuFeatures) {
+ await expect(onboardingOverlayLocator).toContainText(feature)
+ }
+ })
+})
+
+test('Restarting onboarding on desktop takes one attempt', async ({
+ context,
+ page,
+ toolbar,
+ tronApp,
+}) => {
+ if (!tronApp) {
+ fail()
+ }
+
+ await tronApp.cleanProjectDir({
+ app: {
+ onboarding_status: 'dismissed',
+ },
+ })
+
+ await context.folderSetupFn(async (dir) => {
+ const routerTemplateDir = join(dir, 'router-template-slate')
+ await fsp.mkdir(routerTemplateDir, { recursive: true })
+ await fsp.copyFile(
+ executorInputPath('router-template-slate.kcl'),
+ join(routerTemplateDir, 'main.kcl')
+ )
+ })
+
+ // Our constants
+ const u = await getUtils(page)
+ const projectCard = page.getByText('router-template-slate')
+ const helpMenuButton = page.getByRole('button', {
+ name: 'Help and resources',
+ })
+ const restartOnboardingButton = page.getByRole('button', {
+ name: 'Reset onboarding',
+ })
+ const nextButton = page.getByTestId('onboarding-next')
+
+ const tutorialProjectIndicator = page
+ .getByTestId('project-sidebar-toggle')
+ .filter({ hasText: 'Tutorial Project 00' })
+ const tutorialModalText = page.getByText('Welcome to Design Studio!')
+ const tutorialDismissButton = page.getByRole('button', { name: 'Dismiss' })
+ const userMenuButton = toolbar.userSidebarButton
+ const userMenuSettingsButton = page.getByRole('button', {
+ name: 'User settings',
+ })
+ const settingsHeading = page.getByRole('heading', {
+ name: 'Settings',
+ exact: true,
+ })
+ const restartOnboardingSettingsButton = page.getByRole('button', {
+ name: 'Replay onboarding',
+ })
+
+ await test.step('Navigate into project', async () => {
+ await expect(page.getByRole('heading', { name: 'Projects' })).toBeVisible()
+ await expect(projectCard).toBeVisible()
+ await projectCard.click()
+ await u.waitForPageLoad()
+ })
+
+ await test.step('Restart the onboarding from help menu', async () => {
+ await helpMenuButton.click()
+ await restartOnboardingButton.click()
+
+ await nextButton.hover()
+ await nextButton.click()
+ })
+
+ await test.step('Confirm that the onboarding has restarted', async () => {
+ await expect(tutorialProjectIndicator).toBeVisible()
+ await expect(tutorialModalText).toBeVisible()
+ // Make sure the model loaded
+ const XYPlanePoint = { x: 988, y: 523 } as const
+ const modelColor: [number, number, number] = [76, 76, 76]
+
+ await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y)
+ await expectPixelColor(page, modelColor, XYPlanePoint, 8)
+ await tutorialDismissButton.click()
+ // Make sure model still there.
+ await expectPixelColor(page, modelColor, XYPlanePoint, 8)
+ })
+
+ await test.step('Clear code and restart onboarding from settings', async () => {
+ await u.openKclCodePanel()
+ await expect(u.codeLocator).toContainText('// Shelf Bracket')
+ await u.codeLocator.selectText()
+ await u.codeLocator.fill('')
+
+ await test.step('Navigate to settings', async () => {
+ await userMenuButton.click()
+ await userMenuSettingsButton.click()
+ await expect(settingsHeading).toBeVisible()
+ await expect(restartOnboardingSettingsButton).toBeVisible()
})
+
+ await restartOnboardingSettingsButton.click()
+ // Since the code is empty, we should not see the confirmation dialog
+ await expect(nextButton).not.toBeVisible()
+ await expect(tutorialProjectIndicator).toBeVisible()
+ await expect(tutorialModalText).toBeVisible()
})
})
diff --git a/e2e/playwright/storageStates.ts b/e2e/playwright/storageStates.ts
index 48d9fc5e55d..79d7f4af1ad 100644
--- a/e2e/playwright/storageStates.ts
+++ b/e2e/playwright/storageStates.ts
@@ -1,10 +1,12 @@
import type { SaveSettingsPayload } from '@src/lib/settings/settingsTypes'
import { Themes } from '@src/lib/theme'
import type { DeepPartial } from '@src/lib/types'
-import { ONBOARDING_SUBPATHS } from '@src/lib/onboardingPaths'
+import { onboardingPaths } from '@src/routes/Onboarding/paths'
import type { Settings } from '@rust/kcl-lib/bindings/Settings'
+export const IS_PLAYWRIGHT_KEY = 'playwright'
+
export const TEST_SETTINGS_KEY = '/settings.toml'
export const TEST_SETTINGS: DeepPartial = {
app: {
@@ -31,15 +33,12 @@ export const TEST_SETTINGS: DeepPartial = {
export const TEST_SETTINGS_ONBOARDING_USER_MENU: DeepPartial = {
...TEST_SETTINGS,
- app: {
- ...TEST_SETTINGS.app,
- onboarding_status: ONBOARDING_SUBPATHS.USER_MENU,
- },
+ app: { ...TEST_SETTINGS.app, onboarding_status: onboardingPaths.USER_MENU },
}
export const TEST_SETTINGS_ONBOARDING_EXPORT: DeepPartial = {
...TEST_SETTINGS,
- app: { ...TEST_SETTINGS.app, onboarding_status: ONBOARDING_SUBPATHS.EXPORT },
+ app: { ...TEST_SETTINGS.app, onboarding_status: onboardingPaths.EXPORT },
}
export const TEST_SETTINGS_ONBOARDING_PARAMETRIC_MODELING: DeepPartial =
@@ -47,7 +46,7 @@ export const TEST_SETTINGS_ONBOARDING_PARAMETRIC_MODELING: DeepPartial
...TEST_SETTINGS,
app: {
...TEST_SETTINGS.app,
- onboarding_status: ONBOARDING_SUBPATHS.PARAMETRIC_MODELING,
+ onboarding_status: onboardingPaths.PARAMETRIC_MODELING,
},
}
diff --git a/e2e/playwright/test-utils.ts b/e2e/playwright/test-utils.ts
index 9f4201e09b0..55dcf9eff43 100644
--- a/e2e/playwright/test-utils.ts
+++ b/e2e/playwright/test-utils.ts
@@ -5,7 +5,7 @@ import type { BrowserContext, Locator, Page, TestInfo } from '@playwright/test'
import { expect } from '@playwright/test'
import type { EngineCommand } from '@src/lang/std/artifactGraph'
import type { Configuration } from '@src/lang/wasm'
-import { COOKIE_NAME, IS_PLAYWRIGHT_KEY } from '@src/lib/constants'
+import { COOKIE_NAME } from '@src/lib/constants'
import { reportRejection } from '@src/lib/trap'
import type { DeepPartial } from '@src/lib/types'
import { isArray } from '@src/lib/utils'
@@ -18,7 +18,11 @@ import type { ProjectConfiguration } from '@rust/kcl-lib/bindings/ProjectConfigu
import { isErrorWhitelisted } from '@e2e/playwright/lib/console-error-whitelist'
import { secrets } from '@e2e/playwright/secrets'
-import { TEST_SETTINGS, TEST_SETTINGS_KEY } from '@e2e/playwright/storageStates'
+import {
+ IS_PLAYWRIGHT_KEY,
+ TEST_SETTINGS,
+ TEST_SETTINGS_KEY,
+} from '@e2e/playwright/storageStates'
import { test } from '@e2e/playwright/zoo-test'
const toNormalizedCode = (text: string) => {
diff --git a/known-circular.txt b/known-circular.txt
index fbdd980df08..a36dee1ef7f 100644
--- a/known-circular.txt
+++ b/known-circular.txt
@@ -11,3 +11,4 @@
6) src/lib/singletons.ts -> src/clientSideScene/sceneEntities.ts -> src/clientSideScene/segments.ts -> src/components/Toolbar/angleLengthInfo.ts
7) src/lib/singletons.ts -> src/clientSideScene/sceneEntities.ts -> src/clientSideScene/segments.ts
8) src/hooks/useModelingContext.ts -> src/components/ModelingMachineProvider.tsx -> src/components/Toolbar/Intersect.tsx -> src/components/SetHorVertDistanceModal.tsx -> src/lib/useCalculateKclExpression.ts
+ 9) src/routes/Onboarding/index.tsx -> src/routes/Onboarding/Camera.tsx -> src/routes/Onboarding/utils.tsx
diff --git a/rust/kcl-lib/src/settings/types/mod.rs b/rust/kcl-lib/src/settings/types/mod.rs
index babf539965f..0437d4b0241 100644
--- a/rust/kcl-lib/src/settings/types/mod.rs
+++ b/rust/kcl-lib/src/settings/types/mod.rs
@@ -527,6 +527,9 @@ pub enum OnboardingStatus {
#[serde(rename = "/export")]
#[display("/export")]
Export,
+ #[serde(rename = "/move")]
+ #[display("/move")]
+ Move,
#[serde(rename = "/sketching")]
#[display("/sketching")]
Sketching,
diff --git a/src/App.tsx b/src/App.tsx
index e087d074e60..4e501a20e1b 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -4,7 +4,6 @@ import { useHotkeys } from 'react-hotkeys-hook'
import ModalContainer from 'react-modal-promise'
import {
useLoaderData,
- useLocation,
useNavigate,
useRouteLoaderData,
useSearchParams,
@@ -27,20 +26,15 @@ import useHotkeyWrapper from '@src/lib/hotkeyWrapper'
import { isDesktop } from '@src/lib/isDesktop'
import { PATHS } from '@src/lib/paths'
import { takeScreenshotOfVideoStreamCanvas } from '@src/lib/screenshot'
-import { sceneInfra, codeManager, kclManager } from '@src/lib/singletons'
+import { sceneInfra } from '@src/lib/singletons'
import { maybeWriteToDisk } from '@src/lib/telemetry'
-import type { IndexLoaderData } from '@src/lib/types'
+import { type IndexLoaderData } from '@src/lib/types'
import { engineStreamActor, useSettings, useToken } from '@src/lib/singletons'
import { commandBarActor } from '@src/lib/singletons'
import { EngineStreamTransition } from '@src/machines/engineStreamMachine'
+import { onboardingPaths } from '@src/routes/Onboarding/paths'
import { CommandBarOpenButton } from '@src/components/CommandBarOpenButton'
import { ShareButton } from '@src/components/ShareButton'
-import {
- needsToOnboard,
- ONBOARDING_TOAST_ID,
- TutorialRequestToast,
-} from '@src/routes/Onboarding/utils'
-import { ONBOARDING_SUBPATHS } from '@src/lib/onboardingPaths'
// CYCLIC REF
sceneInfra.camControls.engineStreamActor = engineStreamActor
@@ -64,7 +58,6 @@ export function App() {
})
})
- const location = useLocation()
const navigate = useNavigate()
const filePath = useAbsoluteFilePath()
const { onProjectOpen } = useLspContext()
@@ -73,7 +66,7 @@ export function App() {
const ref = useRef(null)
// Stream related refs and data
- const [searchParams] = useSearchParams()
+ let [searchParams] = useSearchParams()
const pool = searchParams.get('pool')
const projectName = project?.name || null
@@ -83,10 +76,9 @@ export function App() {
const loaderData = useRouteLoaderData(PATHS.FILE) as IndexLoaderData
const lastCommandType = commands[commands.length - 1]?.type
- // Run LSP file open hook when navigating between projects or files
useEffect(() => {
onProjectOpen({ name: projectName, path: projectPath }, file || null)
- }, [onProjectOpen, projectName, projectPath, file])
+ }, [projectName, projectPath])
useHotKeyListener()
@@ -112,10 +104,9 @@ export function App() {
toast.success('Your work is auto-saved in real-time')
})
- const paneOpacity = [
- ONBOARDING_SUBPATHS.CAMERA,
- ONBOARDING_SUBPATHS.STREAMING,
- ].some((p) => p === onboardingStatus.current)
+ const paneOpacity = [onboardingPaths.CAMERA, onboardingPaths.STREAMING].some(
+ (p) => p === onboardingStatus.current
+ )
? 'opacity-20'
: ''
@@ -141,7 +132,7 @@ export function App() {
})
}, 500)
}
- }, [lastCommandType, loaderData?.project?.path])
+ }, [lastCommandType])
useEffect(() => {
// When leaving the modeling scene, cut the engine stream.
@@ -150,32 +141,6 @@ export function App() {
}
}, [])
- // Show a custom toast to users if they haven't done the onboarding
- // and they're on the web
- useEffect(() => {
- const onboardingStatus =
- settings.app.onboardingStatus.current ||
- settings.app.onboardingStatus.default
- const needsOnboarded = needsToOnboard(location, onboardingStatus)
-
- if (!isDesktop() && needsOnboarded) {
- toast.success(
- () =>
- TutorialRequestToast({
- onboardingStatus: settings.app.onboardingStatus.current,
- navigate,
- codeManager,
- kclManager,
- }),
- {
- id: ONBOARDING_TOAST_ID,
- duration: Number.POSITIVE_INFINITY,
- icon: null,
- }
- )
- }
- }, [location, settings.app.onboardingStatus, navigate])
-
return (
{/*
*/}
-
+
diff --git a/src/Router.tsx b/src/Router.tsx
index e457870f2a8..f592783deff 100644
--- a/src/Router.tsx
+++ b/src/Router.tsx
@@ -28,7 +28,7 @@ import useHotkeyWrapper from '@src/lib/hotkeyWrapper'
import { isDesktop } from '@src/lib/isDesktop'
import makeUrlPathRelative from '@src/lib/makeUrlPathRelative'
import { PATHS } from '@src/lib/paths'
-import { fileLoader, homeLoader } from '@src/lib/routeLoaders'
+import { fileLoader, homeLoader, telemetryLoader } from '@src/lib/routeLoaders'
import {
codeManager,
engineCommandManager,
@@ -110,6 +110,7 @@ const router = createRouter([
},
{
id: PATHS.FILE + 'TELEMETRY',
+ loader: telemetryLoader,
children: [
{
path: makeUrlPathRelative(PATHS.TELEMETRY),
@@ -143,6 +144,7 @@ const router = createRouter([
},
{
path: makeUrlPathRelative(PATHS.TELEMETRY),
+ loader: telemetryLoader,
element: ,
},
],
diff --git a/src/components/CustomIcon.tsx b/src/components/CustomIcon.tsx
index 0d918aef306..c69f7fe4c4a 100644
--- a/src/components/CustomIcon.tsx
+++ b/src/components/CustomIcon.tsx
@@ -854,14 +854,6 @@ const CustomIconMap = {
/>
),
- play: (
-
-
-
- ),
rotate: (
(
)
-export function HelpMenu({
- navigate = () => {},
-}: {
- navigate?: NavigateFunction
-}) {
+export function HelpMenu(props: React.PropsWithChildren) {
const location = useLocation()
+ const { onProjectOpen } = useLspContext()
const filePath = useAbsoluteFilePath()
+ const isInProject = location.pathname.includes(PATHS.FILE)
+ const navigate = useNavigate()
const resetOnboardingWorkflow = () => {
- const props = {
- onboardingStatus: ONBOARDING_SUBPATHS.INDEX,
- navigate,
- codeManager,
- kclManager,
+ settingsActor.send({
+ type: 'set.app.onboardingStatus',
+ data: {
+ value: '',
+ level: 'user',
+ },
+ })
+ if (isInProject) {
+ navigate(filePath + PATHS.ONBOARDING.INDEX)
+ } else {
+ createAndOpenNewTutorialProject({
+ onProjectOpen,
+ navigate,
+ }).catch(reportRejection)
}
- acceptOnboarding(props).catch((reason) =>
- catchOnboardingWarnError(reason, props)
- )
}
const cb = (data: WebContentSendPayload) => {
- if (data.menuLabel === 'Help.Replay onboarding tutorial') {
+ if (data.menuLabel === 'Help.Reset onboarding') {
resetOnboardingWorkflow()
}
}
@@ -65,81 +68,71 @@ export function HelpMenu({
as="ul"
className="absolute right-0 left-auto flex flex-col w-64 gap-1 p-0 py-2 m-0 mb-1 text-sm border border-solid rounded shadow-lg bottom-full align-stretch text-chalkboard-10 dark:text-inherit bg-chalkboard-110 dark:bg-chalkboard-100 border-chalkboard-110 dark:border-chalkboard-80"
>
- {({ close }) => (
- <>
-
- Report a bug
-
-
- Request a feature
-
-
- Ask the community
-
-
-
- KCL code samples
-
-
- KCL docs
-
-
-
- Release notes
-
- {
- const targetPath = location.pathname.includes(PATHS.FILE)
- ? filePath + PATHS.SETTINGS_KEYBINDINGS
- : PATHS.HOME + PATHS.SETTINGS_KEYBINDINGS
- navigate(targetPath)
- }}
- data-testid="keybindings-button"
- >
- Keyboard shortcuts
-
- {
- close()
- resetOnboardingWorkflow()
- }}
- >
- Replay onboarding tutorial
-
- >
- )}
+
+ Report a bug
+
+
+ Request a feature
+
+
+ Ask the community
+
+
+
+ KCL code samples
+
+
+ KCL docs
+
+
+
+ Release notes
+
+ {
+ const targetPath = location.pathname.includes(PATHS.FILE)
+ ? filePath + PATHS.SETTINGS_KEYBINDINGS
+ : PATHS.HOME + PATHS.SETTINGS_KEYBINDINGS
+ navigate(targetPath)
+ }}
+ data-testid="keybindings-button"
+ >
+ Keyboard shortcuts
+
+
+ Reset onboarding
+
)
diff --git a/src/components/LowerRightControls.tsx b/src/components/LowerRightControls.tsx
index 219ae2334f8..42d539e8ddc 100644
--- a/src/components/LowerRightControls.tsx
+++ b/src/components/LowerRightControls.tsx
@@ -1,4 +1,5 @@
-import { Link, type NavigateFunction, useLocation } from 'react-router-dom'
+import { Link, useLocation } from 'react-router-dom'
+
import { CustomIcon } from '@src/components/CustomIcon'
import { HelpMenu } from '@src/components/HelpMenu'
import { NetworkHealthIndicator } from '@src/components/NetworkHealthIndicator'
@@ -11,10 +12,8 @@ import { APP_VERSION, getReleaseUrl } from '@src/routes/utils'
export function LowerRightControls({
children,
- navigate = () => {},
}: {
children?: React.ReactNode
- navigate?: NavigateFunction
}) {
const location = useLocation()
const filePath = useAbsoluteFilePath()
@@ -73,7 +72,7 @@ export function LowerRightControls({
{!location.pathname.startsWith(PATHS.HOME) && (
)}
-
+
)
diff --git a/src/components/ModelingSidebar/ModelingPane.tsx b/src/components/ModelingSidebar/ModelingPane.tsx
index 264da072a0f..b919b8ff079 100644
--- a/src/components/ModelingSidebar/ModelingPane.tsx
+++ b/src/components/ModelingSidebar/ModelingPane.tsx
@@ -6,7 +6,7 @@ import { ActionIcon } from '@src/components/ActionIcon'
import type { CustomIconName } from '@src/components/CustomIcon'
import Tooltip from '@src/components/Tooltip'
import { useSettings } from '@src/lib/singletons'
-import { ONBOARDING_SUBPATHS } from '@src/lib/onboardingPaths'
+import { onboardingPaths } from '@src/routes/Onboarding/paths'
import styles from './ModelingPane.module.css'
@@ -71,7 +71,7 @@ export const ModelingPane = ({
const settings = useSettings()
const onboardingStatus = settings.app.onboardingStatus
const pointerEventsCssClass =
- onboardingStatus.current === ONBOARDING_SUBPATHS.CAMERA
+ onboardingStatus.current === onboardingPaths.CAMERA
? 'pointer-events-none '
: 'pointer-events-auto '
return (
diff --git a/src/components/ModelingSidebar/ModelingSidebar.tsx b/src/components/ModelingSidebar/ModelingSidebar.tsx
index 7e4d18450cf..6e5eb2f6861 100644
--- a/src/components/ModelingSidebar/ModelingSidebar.tsx
+++ b/src/components/ModelingSidebar/ModelingSidebar.tsx
@@ -24,7 +24,7 @@ import { SIDEBAR_BUTTON_SUFFIX } from '@src/lib/constants'
import { isDesktop } from '@src/lib/isDesktop'
import { useSettings } from '@src/lib/singletons'
import { commandBarActor } from '@src/lib/singletons'
-import { ONBOARDING_SUBPATHS } from '@src/lib/onboardingPaths'
+import { onboardingPaths } from '@src/routes/Onboarding/paths'
import { reportRejection } from '@src/lib/trap'
import { refreshPage } from '@src/lib/utils'
import { hotkeyDisplay } from '@src/lib/hotkeyWrapper'
@@ -53,7 +53,7 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
const onboardingStatus = settings.app.onboardingStatus
const { send, context } = useModelingContext()
const pointerEventsCssClass =
- onboardingStatus.current === ONBOARDING_SUBPATHS.CAMERA ||
+ onboardingStatus.current === onboardingPaths.CAMERA ||
context.store?.openPanes.length === 0
? 'pointer-events-none '
: 'pointer-events-auto '
diff --git a/src/components/Providers/SystemIOProviderDesktop.tsx b/src/components/Providers/SystemIOProviderDesktop.tsx
index 5dc4a8261c0..a20b6a25ef9 100644
--- a/src/components/Providers/SystemIOProviderDesktop.tsx
+++ b/src/components/Providers/SystemIOProviderDesktop.tsx
@@ -1,10 +1,5 @@
import { useFileSystemWatcher } from '@src/hooks/useFileSystemWatcher'
-import {
- PATHS,
- joinRouterPaths,
- joinOSPaths,
- safeEncodeForRouterPaths,
-} from '@src/lib/paths'
+import { PATHS } from '@src/lib/paths'
import { systemIOActor, useSettings, useToken } from '@src/lib/singletons'
import {
useHasListedProjects,
@@ -40,14 +35,14 @@ export function SystemIOMachineLogicListenerDesktop() {
if (!requestedProjectName.name) {
return
}
- const projectPathWithoutSpecificKCLFile = joinOSPaths(
- projectDirectoryPath,
+ let projectPathWithoutSpecificKCLFile =
+ projectDirectoryPath +
+ window.electron.path.sep +
requestedProjectName.name
- )
- const requestedPath = joinRouterPaths(
- PATHS.FILE,
- safeEncodeForRouterPaths(projectPathWithoutSpecificKCLFile)
- )
+
+ const requestedPath = `${PATHS.FILE}/${encodeURIComponent(
+ projectPathWithoutSpecificKCLFile
+ )}`
navigate(requestedPath)
}, [requestedProjectName])
}
@@ -57,16 +52,12 @@ export function SystemIOMachineLogicListenerDesktop() {
if (!requestedFileName.file || !requestedFileName.project) {
return
}
- const filePath = joinOSPaths(
+ const projectPath = window.electron.join(
projectDirectoryPath,
- requestedFileName.project,
- requestedFileName.file
- )
- const requestedPath = joinRouterPaths(
- PATHS.FILE,
- safeEncodeForRouterPaths(filePath),
- requestedFileName.subRoute || ''
+ requestedFileName.project
)
+ const filePath = window.electron.join(projectPath, requestedFileName.file)
+ const requestedPath = `${PATHS.FILE}/${encodeURIComponent(filePath)}`
navigate(requestedPath)
}, [requestedFileName])
}
diff --git a/src/components/RouteProvider.tsx b/src/components/RouteProvider.tsx
index 3cb61cfc420..b18f55743a5 100644
--- a/src/components/RouteProvider.tsx
+++ b/src/components/RouteProvider.tsx
@@ -7,6 +7,8 @@ import {
useRouteLoaderData,
} from 'react-router-dom'
+import type { OnboardingStatus } from '@rust/kcl-lib/bindings/OnboardingStatus'
+
import { useAuthNavigation } from '@src/hooks/useAuthNavigation'
import { useFileSystemWatcher } from '@src/hooks/useFileSystemWatcher'
import { getAppSettingsFilePath } from '@src/lib/desktop'
@@ -16,7 +18,7 @@ import { markOnce } from '@src/lib/performance'
import { loadAndValidateSettings } from '@src/lib/settings/settingsUtils'
import { trap } from '@src/lib/trap'
import type { IndexLoaderData } from '@src/lib/types'
-import { settingsActor } from '@src/lib/singletons'
+import { settingsActor, useSettings } from '@src/lib/singletons'
export const RouteProviderContext = createContext({})
@@ -30,6 +32,7 @@ export function RouteProvider({ children }: { children: ReactNode }) {
const navigation = useNavigation()
const navigate = useNavigate()
const location = useLocation()
+ const settings = useSettings()
useEffect(() => {
// On initialization, the react-router-dom does not send a 'loading' state event.
@@ -43,9 +46,35 @@ export function RouteProvider({ children }: { children: ReactNode }) {
markOnce('code/willLoadHome')
} else if (isFile) {
markOnce('code/willLoadFile')
+
+ /**
+ * TODO: Move to XState. This block has been moved from routerLoaders
+ * and is borrowing the `isFile` logic from the rest of this
+ * telemetry-focused `useEffect`. Once `appMachine` knows about
+ * the current route and navigation, this can be moved into settingsMachine
+ * to fire as soon as the user settings have been read.
+ */
+ const onboardingStatus: OnboardingStatus =
+ settings.app.onboardingStatus.current || ''
+ // '' is the initial state, 'completed' and 'dismissed' are the final states
+ const needsToOnboard =
+ onboardingStatus.length === 0 ||
+ !(onboardingStatus === 'completed' || onboardingStatus === 'dismissed')
+ const shouldRedirectToOnboarding = isFile && needsToOnboard
+
+ if (
+ shouldRedirectToOnboarding &&
+ settingsActor.getSnapshot().matches('idle')
+ ) {
+ navigate(
+ (first ? location.pathname : navigation.location?.pathname) +
+ PATHS.ONBOARDING.INDEX +
+ onboardingStatus.slice(1)
+ )
+ }
}
setFirstState(false)
- }, [first, navigation, location.pathname])
+ }, [navigation])
useEffect(() => {
if (!isDesktop()) return
diff --git a/src/components/Settings/AllSettingsFields.tsx b/src/components/Settings/AllSettingsFields.tsx
index ec39dc2204a..9e50f2e8e2d 100644
--- a/src/components/Settings/AllSettingsFields.tsx
+++ b/src/components/Settings/AllSettingsFields.tsx
@@ -6,12 +6,16 @@ import { useLocation, useNavigate } from 'react-router-dom'
import { Fragment } from 'react/jsx-runtime'
import { ActionButton } from '@src/components/ActionButton'
+import { useLspContext } from '@src/components/LspProvider'
import { SettingsFieldInput } from '@src/components/Settings/SettingsFieldInput'
import { SettingsSection } from '@src/components/Settings/SettingsSection'
-import { getSettingsFolderPaths } from '@src/lib/desktopFS'
+import { useDotDotSlash } from '@src/hooks/useDotDotSlash'
+import {
+ createAndOpenNewTutorialProject,
+ getSettingsFolderPaths,
+} from '@src/lib/desktopFS'
import { isDesktop } from '@src/lib/isDesktop'
import { openExternalBrowserIfDesktop } from '@src/lib/openWindow'
-import { ONBOARDING_SUBPATHS } from '@src/lib/onboardingPaths'
import { PATHS } from '@src/lib/paths'
import type { Setting } from '@src/lib/settings/initialSettings'
import type {
@@ -24,17 +28,9 @@ import {
} from '@src/lib/settings/settingsUtils'
import { reportRejection } from '@src/lib/trap'
import { toSync } from '@src/lib/utils'
-import {
- codeManager,
- kclManager,
- settingsActor,
- useSettings,
-} from '@src/lib/singletons'
+import { settingsActor, useSettings } from '@src/lib/singletons'
import { APP_VERSION, IS_NIGHTLY, getReleaseUrl } from '@src/routes/utils'
-import {
- acceptOnboarding,
- catchOnboardingWarnError,
-} from '@src/routes/Onboarding/utils'
+import { waitFor } from 'xstate'
interface AllSettingsFieldsProps {
searchParamTab: SettingsLevel
@@ -48,6 +44,8 @@ export const AllSettingsFields = forwardRef(
) => {
const location = useLocation()
const navigate = useNavigate()
+ const { onProjectOpen } = useLspContext()
+ const dotDotSlash = useDotDotSlash()
const context = useSettings()
const projectPath = useMemo(() => {
@@ -65,18 +63,26 @@ export const AllSettingsFields = forwardRef(
: undefined
return projectPath
- }, [location.pathname, isFileSettings])
+ }, [location.pathname])
async function restartOnboarding() {
- const props = {
- onboardingStatus: ONBOARDING_SUBPATHS.INDEX,
- navigate,
- codeManager,
- kclManager,
+ settingsActor.send({
+ type: `set.app.onboardingStatus`,
+ data: { level: 'user', value: '' },
+ })
+ await waitFor(settingsActor, (s) => s.matches('idle'), {
+ timeout: 10_000,
+ }).catch(reportRejection)
+
+ if (isFileSettings) {
+ // If we're in a project, first navigate to the onboarding start here
+ // so we can trigger the warning screen if necessary
+ navigate(dotDotSlash(1) + PATHS.ONBOARDING.INDEX)
+ } else {
+ // If we're in the global settings, create a new project and navigate
+ // to the onboarding start in that project
+ await createAndOpenNewTutorialProject({ onProjectOpen, navigate })
}
- acceptOnboarding(props).catch((reason) =>
- catchOnboardingWarnError(reason, props)
- )
}
return (
diff --git a/src/lib/constants.ts b/src/lib/constants.ts
index adbdb695090..4a527381d4c 100644
--- a/src/lib/constants.ts
+++ b/src/lib/constants.ts
@@ -2,7 +2,6 @@ import type { Models } from '@kittycad/lib/dist/types/src'
import type { UnitAngle, UnitLength } from '@rust/kcl-lib/bindings/ModelingCmd'
-export const IS_PLAYWRIGHT_KEY = 'playwright'
export const APP_NAME = 'Design Studio'
/** Search string in new project names to increment as an index */
export const INDEX_IDENTIFIER = '$n'
diff --git a/src/lib/desktopFS.ts b/src/lib/desktopFS.ts
index e12225129ca..29bb9d8ef3b 100644
--- a/src/lib/desktopFS.ts
+++ b/src/lib/desktopFS.ts
@@ -1,6 +1,18 @@
import { relevantFileExtensions } from '@src/lang/wasmUtils'
-import { FILE_EXT, INDEX_IDENTIFIER, MAX_PADDING } from '@src/lib/constants'
+import {
+ FILE_EXT,
+ INDEX_IDENTIFIER,
+ MAX_PADDING,
+ ONBOARDING_PROJECT_NAME,
+} from '@src/lib/constants'
+import {
+ createNewProjectDirectory,
+ listProjects,
+ readAppSettingsFile,
+} from '@src/lib/desktop'
+import { bracket } from '@src/lib/exampleKcl'
import { isDesktop } from '@src/lib/isDesktop'
+import { PATHS } from '@src/lib/paths'
import type { FileEntry } from '@src/lib/project'
export const isHidden = (fileOrDir: FileEntry) =>
@@ -120,6 +132,65 @@ export async function getSettingsFolderPaths(projectPath?: string) {
}
}
+export async function createAndOpenNewTutorialProject({
+ onProjectOpen,
+ navigate,
+}: {
+ onProjectOpen: (
+ project: {
+ name: string | null
+ path: string | null
+ } | null,
+ file: FileEntry | null
+ ) => void
+ navigate: (path: string) => void
+}) {
+ // Create a new project with the onboarding project name
+ const configuration = await readAppSettingsFile()
+ const projects = await listProjects(configuration)
+ const nextIndex = getNextProjectIndex(ONBOARDING_PROJECT_NAME, projects)
+ const name = interpolateProjectNameWithIndex(
+ ONBOARDING_PROJECT_NAME,
+ nextIndex
+ )
+
+ // Delete the tutorial project if it already exists.
+ if (isDesktop()) {
+ if (configuration.settings?.project?.directory === undefined) {
+ return Promise.reject(new Error('configuration settings are undefined'))
+ }
+
+ const fullPath = window.electron.join(
+ configuration.settings.project.directory,
+ name
+ )
+ if (window.electron.exists(fullPath)) {
+ await window.electron.rm(fullPath)
+ }
+ }
+
+ const newProject = await createNewProjectDirectory(
+ name,
+ bracket,
+ configuration
+ )
+
+ // Prep the LSP and navigate to the onboarding start
+ onProjectOpen(
+ {
+ name: newProject.name,
+ path: newProject.path,
+ },
+ null
+ )
+ navigate(
+ `${PATHS.FILE}/${encodeURIComponent(newProject.default_file)}${
+ PATHS.ONBOARDING.INDEX
+ }`
+ )
+ return newProject
+}
+
/**
* Get the next available file name by appending a hyphen and number to the end of the name
*/
diff --git a/src/lib/paths.ts b/src/lib/paths.ts
index 7d921d6a4db..4ad514f197e 100644
--- a/src/lib/paths.ts
+++ b/src/lib/paths.ts
@@ -2,7 +2,7 @@ import type { PlatformPath } from 'path'
import type { Configuration } from '@rust/kcl-lib/bindings/Configuration'
-import { IS_PLAYWRIGHT_KEY } from '@src/lib/constants'
+import { IS_PLAYWRIGHT_KEY } from '@e2e/playwright/storageStates'
import {
BROWSER_FILE_NAME,
@@ -14,7 +14,7 @@ import { isDesktop } from '@src/lib/isDesktop'
import { readLocalStorageAppSettingsFile } from '@src/lib/settings/settingsUtils'
import { err } from '@src/lib/trap'
import type { DeepPartial } from '@src/lib/types'
-import { ONBOARDING_SUBPATHS } from '@src/lib/onboardingPaths'
+import { onboardingPaths } from '@src/routes/Onboarding/paths'
const prependRoutes =
(routesObject: Record) => (prepend: string) => {
@@ -27,7 +27,7 @@ const prependRoutes =
}
type OnboardingPaths = {
- [K in keyof typeof ONBOARDING_SUBPATHS]: `/onboarding${(typeof ONBOARDING_SUBPATHS)[K]}`
+ [K in keyof typeof onboardingPaths]: `/onboarding${(typeof onboardingPaths)[K]}`
}
const SETTINGS = '/settings'
@@ -48,9 +48,7 @@ export const PATHS = {
SETTINGS_PROJECT: `${SETTINGS}?tab=project` as const,
SETTINGS_KEYBINDINGS: `${SETTINGS}?tab=keybindings` as const,
SIGN_IN: '/signin',
- ONBOARDING: prependRoutes(ONBOARDING_SUBPATHS)(
- '/onboarding'
- ) as OnboardingPaths,
+ ONBOARDING: prependRoutes(onboardingPaths)('/onboarding') as OnboardingPaths,
TELEMETRY: '/telemetry',
} as const
export const BROWSER_PATH = `%2F${BROWSER_PROJECT_NAME}%2F${BROWSER_FILE_NAME}${FILE_EXT}`
@@ -138,56 +136,3 @@ export function parseProjectRoute(
currentFilePath: currentFilePath,
}
}
-
-/**
- * Joins any number of arguments of strings to create a Router level path that is safe
- * A path will be created of the format /value/value1/value2
- * Filters out '/', ''
- * Removes all leading and ending slashes, this allows you to pass '//dog//','//cat//' it will resolve to
- * /dog/cat
- */
-export function joinRouterPaths(...parts: string[]): string {
- return (
- '/' +
- parts
- .map((part) => part.replace(/^\/+|\/+$/g, '')) // Remove leading/trailing slashes
- .filter((part) => part.length > 0) // Remove empty segments
- .join('/')
- )
-}
-
-/**
- * Joins any number of arguments of strings to create a OS level path that is safe
- * A path will be created of the format /value/value1/value2
- * or \value\value1\value2 for POSIX OSes like Windows
- * Filters out the separator slashes
- * Removes all leading and ending slashes, this allows you to pass '//dog//','//cat//' it will resolve to
- * /dog/cat
- * or \dog\cat on POSIX
- */
-export function joinOSPaths(...parts: string[]): string {
- const sep = window.electron?.sep || '/'
- const regexSep = sep === '/' ? '/' : '\\'
- return (
- (sep === '\\' ? '' : sep) + // Windows absolute paths should not be prepended with a separator, they start with the drive name
- parts
- .map((part) =>
- part.replace(new RegExp(`^${regexSep}+|${regexSep}+$`, 'g'), '')
- ) // Remove leading/trailing slashes
- .filter((part) => part.length > 0) // Remove empty segments
- .join(sep)
- )
-}
-
-export function safeEncodeForRouterPaths(dynamicValue: string): string {
- return `${encodeURIComponent(dynamicValue)}`
-}
-
-/**
- * /dog/cat/house.kcl gives you house.kcl
- * \dog\cat\house.kcl gives you house.kcl
- * Works on all OS!
- */
-export function getStringAfterLastSeparator(path: string): string {
- return path.split(window.electron.sep).pop() || ''
-}
diff --git a/src/lib/routeLoaders.ts b/src/lib/routeLoaders.ts
index 020efedfbfd..de46b4c4a5c 100644
--- a/src/lib/routeLoaders.ts
+++ b/src/lib/routeLoaders.ts
@@ -22,6 +22,12 @@ import type {
} from '@src/lib/types'
import { settingsActor } from '@src/lib/singletons'
+export const telemetryLoader: LoaderFunction = async ({
+ params,
+}): Promise => {
+ return null
+}
+
export const fileLoader: LoaderFunction = async (
routerData
): Promise => {
diff --git a/src/machines/systemIO/systemIOMachine.ts b/src/machines/systemIO/systemIOMachine.ts
index 93984de0331..57e1750827b 100644
--- a/src/machines/systemIO/systemIOMachine.ts
+++ b/src/machines/systemIO/systemIOMachine.ts
@@ -43,11 +43,7 @@ export const systemIOMachine = setup({
}
| {
type: SystemIOMachineEvents.navigateToFile
- data: {
- requestedProjectName: string
- requestedFileName: string
- requestedSubRoute?: string
- }
+ data: { requestedProjectName: string; requestedFileName: string }
}
| {
type: SystemIOMachineEvents.createProject
@@ -79,7 +75,6 @@ export const systemIOMachine = setup({
requestedProjectName: string
requestedFileName: string
requestedCode: string
- requestedSubRoute?: string
}
}
| {
@@ -122,9 +117,7 @@ export const systemIOMachine = setup({
[SystemIOMachineActions.setRequestedProjectName]: assign({
requestedProjectName: ({ event }) => {
assertEvent(event, SystemIOMachineEvents.navigateToProject)
- return {
- name: event.data.requestedProjectName,
- }
+ return { name: event.data.requestedProjectName }
},
}),
[SystemIOMachineActions.setRequestedFileName]: assign({
@@ -133,7 +126,6 @@ export const systemIOMachine = setup({
return {
project: event.data.requestedProjectName,
file: event.data.requestedFileName,
- subRoute: event.data.requestedSubRoute,
}
},
}),
@@ -232,15 +224,13 @@ export const systemIOMachine = setup({
requestedFileName: string
requestedCode: string
rootContext: AppMachineContext
- requestedSubRoute?: string
}
}): Promise<{
message: string
fileName: string
projectName: string
- subRoute: string
}> => {
- return { message: '', fileName: '', projectName: '', subRoute: '' }
+ return { message: '', fileName: '', projectName: '' }
}
),
[SystemIOMachineActors.checkReadWrite]: fromPromise(
@@ -468,7 +458,6 @@ export const systemIOMachine = setup({
context,
requestedProjectName: event.data.requestedProjectName,
requestedFileName: event.data.requestedFileName,
- requestedSubRoute: event.data.requestedSubRoute,
requestedCode: event.data.requestedCode,
rootContext: self.system.get('root').getSnapshot().context,
}
@@ -487,7 +476,6 @@ export const systemIOMachine = setup({
return {
project: event.output.projectName,
file,
- subRoute: event.output.subRoute,
}
},
}),
diff --git a/src/machines/systemIO/systemIOMachineDesktop.ts b/src/machines/systemIO/systemIOMachineDesktop.ts
index 4a382477b62..bc42af2e55c 100644
--- a/src/machines/systemIO/systemIOMachineDesktop.ts
+++ b/src/machines/systemIO/systemIOMachineDesktop.ts
@@ -158,7 +158,6 @@ export const systemIOMachineDesktop = systemIOMachine.provide({
requestedFileName: string
requestedCode: string
rootContext: AppMachineContext
- requestedSubRoute?: string
}
}) => {
const requestedProjectName = input.requestedProjectName
@@ -207,7 +206,6 @@ export const systemIOMachineDesktop = systemIOMachine.provide({
message: 'File created successfully',
fileName: newFileName,
projectName: newProjectName,
- subRoute: input.requestedSubRoute || '',
}
}
),
diff --git a/src/machines/systemIO/systemIOMachineWeb.ts b/src/machines/systemIO/systemIOMachineWeb.ts
index 67034695eb9..cac7ef9adcf 100644
--- a/src/machines/systemIO/systemIOMachineWeb.ts
+++ b/src/machines/systemIO/systemIOMachineWeb.ts
@@ -20,7 +20,6 @@ export const systemIOMachineWeb = systemIOMachine.provide({
requestedFileName: string
requestedCode: string
rootContext: AppMachineContext
- requestedSubRoute?: string
}
}) => {
// Browser version doesn't navigate, just overwrites the current file
@@ -44,7 +43,6 @@ export const systemIOMachineWeb = systemIOMachine.provide({
message: 'File overwritten successfully',
fileName: input.requestedFileName,
projectName: '',
- subRoute: input.requestedSubRoute || '',
}
}
),
diff --git a/src/machines/systemIO/utils.ts b/src/machines/systemIO/utils.ts
index a07605d6e6b..5691fe2e005 100644
--- a/src/machines/systemIO/utils.ts
+++ b/src/machines/systemIO/utils.ts
@@ -8,7 +8,6 @@ export enum SystemIOMachineActors {
deleteProject = 'delete project',
createKCLFile = 'create kcl file',
checkReadWrite = 'check read write',
- /** TODO: rename this event to be more generic, like `createKCLFileAndNavigate` */
importFileFromURL = 'import file from URL',
deleteKCLFile = 'delete kcl delete',
}
@@ -22,7 +21,6 @@ export enum SystemIOMachineStates {
deletingProject = 'deletingProject',
creatingKCLFile = 'creatingKCLFile',
checkingReadWrite = 'checkingReadWrite',
- /** TODO: rename this event to be more generic, like `createKCLFileAndNavigate` */
importFileFromURL = 'importFileFromURL',
deletingKCLFile = 'deletingKCLFile',
}
@@ -43,7 +41,6 @@ export enum SystemIOMachineEvents {
createKCLFile = 'create kcl file',
setDefaultProjectFolderName = 'set default project folder name',
done_checkReadWrite = donePrefix + 'check read write',
- /** TODO: rename this event to be more generic, like `createKCLFileAndNavigate` */
importFileFromURL = 'import file from URL',
done_importFileFromURL = donePrefix + 'import file from URL',
generateTextToCAD = 'generate text to CAD',
@@ -77,7 +74,7 @@ export type SystemIOContext = {
* this is required to prevent chokidar from spamming invalid events during initialization. */
hasListedProjects: boolean
requestedProjectName: { name: string }
- requestedFileName: { project: string; file: string; subRoute?: string }
+ requestedFileName: { project: string; file: string }
canReadWriteProjectDirectory: { value: boolean; error: unknown }
clearURLParams: { value: boolean }
requestedTextToCadGeneration: {
diff --git a/src/menu/channels.ts b/src/menu/channels.ts
index 6312fa8db8b..f9ba9f5d8a4 100644
--- a/src/menu/channels.ts
+++ b/src/menu/channels.ts
@@ -6,7 +6,7 @@ import type { Channel } from '@src/channels'
export type MenuLabels =
| 'Help.Command Palette...'
| 'Help.Report a bug'
- | 'Help.Replay onboarding tutorial'
+ | 'Help.Reset onboarding'
| 'Edit.Rename project'
| 'Edit.Delete project'
| 'Edit.Change project directory'
diff --git a/src/menu/helpRole.ts b/src/menu/helpRole.ts
index b6941994a77..de7a1c1af87 100644
--- a/src/menu/helpRole.ts
+++ b/src/menu/helpRole.ts
@@ -84,11 +84,11 @@ export const helpRole = (
},
{ type: 'separator' },
{
- id: 'Help.Replay onboarding tutorial',
- label: 'Replay onboarding tutorial',
+ id: 'Help.Reset onboarding',
+ label: 'Reset onboarding',
click: () => {
typeSafeWebContentsSend(mainWindow, 'menu-action-clicked', {
- menuLabel: 'Help.Replay onboarding tutorial',
+ menuLabel: 'Help.Reset onboarding',
})
},
},
diff --git a/src/menu/roles.ts b/src/menu/roles.ts
index 5ead00a0f14..8b51ad1dafa 100644
--- a/src/menu/roles.ts
+++ b/src/menu/roles.ts
@@ -45,7 +45,7 @@ type HelpRoleLabel =
| 'Ask the community discourse'
| 'KCL code samples'
| 'KCL docs'
- | 'Replay onboarding tutorial'
+ | 'Reset onboarding'
| 'Show release notes'
| 'Manage account'
| 'Get started with Text-to-CAD'
diff --git a/src/routes/Home.tsx b/src/routes/Home.tsx
index aed35b0b846..5e85e597fc4 100644
--- a/src/routes/Home.tsx
+++ b/src/routes/Home.tsx
@@ -2,12 +2,7 @@ import type { FormEvent, HTMLProps } from 'react'
import { useEffect, useRef } from 'react'
import { toast } from 'react-hot-toast'
import { useHotkeys } from 'react-hotkeys-hook'
-import {
- Link,
- useLocation,
- useNavigate,
- useSearchParams,
-} from 'react-router-dom'
+import { Link, useNavigate, useSearchParams } from 'react-router-dom'
import { ActionButton } from '@src/components/ActionButton'
import { AppHeader } from '@src/components/AppHeader'
@@ -24,7 +19,7 @@ import { isDesktop } from '@src/lib/isDesktop'
import { PATHS } from '@src/lib/paths'
import { markOnce } from '@src/lib/performance'
import type { Project } from '@src/lib/project'
-import { codeManager, kclManager } from '@src/lib/singletons'
+import { kclManager } from '@src/lib/singletons'
import {
getNextSearchParams,
getSortFunction,
@@ -44,12 +39,6 @@ import {
} from '@src/machines/systemIO/utils'
import type { WebContentSendPayload } from '@src/menu/channels'
import { openExternalBrowserIfDesktop } from '@src/lib/openWindow'
-import {
- acceptOnboarding,
- needsToOnboard,
- onDismissOnboardingInvite,
-} from '@src/routes/Onboarding/utils'
-import Tooltip from '@src/components/Tooltip'
type ReadWriteProjectState = {
value: boolean
@@ -80,10 +69,8 @@ const Home = () => {
})
})
- const location = useLocation()
const navigate = useNavigate()
const settings = useSettings()
- const onboardingStatus = settings.app.onboardingStatus.current
// Menu listeners
const cb = (data: WebContentSendPayload) => {
@@ -206,7 +193,7 @@ const Home = () => {
return (
-
+
{
readWriteProjectDir={readWriteProjectDir}
className="col-start-2 -col-end-1"
/>
-
+
- {needsToOnboard(location, onboardingStatus) && (
-
- {
- acceptOnboarding({
- onboardingStatus,
- navigate,
- codeManager,
- kclManager,
- }).catch(reportRejection)
- }}
- className={`${sidebarButtonClasses} !text-primary flex-1`}
- iconStart={{
- icon: 'play',
- bgClassName: '!bg-primary rounded-sm',
- iconClassName: '!text-white',
- }}
- data-testid="home-tutorial-button"
- >
- {onboardingStatus === '' ? 'Start' : 'Continue'} tutorial
-
-
- Dismiss tutorial
-
-
- )}
{
sort={sort}
className="flex-1 col-start-2 -col-end-1 overflow-y-auto pr-2 pb-24"
/>
-
+
)
diff --git a/src/routes/Onboarding/Camera.tsx b/src/routes/Onboarding/Camera.tsx
index 199e26bed5d..211cd8e7bed 100644
--- a/src/routes/Onboarding/Camera.tsx
+++ b/src/routes/Onboarding/Camera.tsx
@@ -1,11 +1,18 @@
import { SettingsSection } from '@src/components/Settings/SettingsSection'
import type { CameraSystem } from '@src/lib/cameraControls'
import { cameraMouseDragGuards, cameraSystems } from '@src/lib/cameraControls'
-import { ONBOARDING_SUBPATHS } from '@src/lib/onboardingPaths'
import { settingsActor, useSettings } from '@src/lib/singletons'
-import { OnboardingButtons } from '@src/routes/Onboarding/utils'
+import { onboardingPaths } from '@src/routes/Onboarding/paths'
+
+import {
+ OnboardingButtons,
+ useDismiss,
+ useNextClick,
+} from '@src/routes/Onboarding/utils'
export default function Units() {
+ useDismiss()
+ useNextClick(onboardingPaths.STREAMING)
const {
modeling: { mouseControls },
} = useSettings()
@@ -59,7 +66,7 @@ export default function Units() {
diff --git a/src/routes/Onboarding/CmdK.tsx b/src/routes/Onboarding/CmdK.tsx
index 161f2974283..7dc2e4e2ca6 100644
--- a/src/routes/Onboarding/CmdK.tsx
+++ b/src/routes/Onboarding/CmdK.tsx
@@ -1,7 +1,8 @@
import { COMMAND_PALETTE_HOTKEY } from '@src/components/CommandBar/CommandBar'
import usePlatform from '@src/hooks/usePlatform'
import { hotkeyDisplay } from '@src/lib/hotkeyWrapper'
-import { ONBOARDING_SUBPATHS } from '@src/lib/onboardingPaths'
+import { onboardingPaths } from '@src/routes/Onboarding/paths'
+
import { OnboardingButtons, kbdClasses } from '@src/routes/Onboarding/utils'
export default function CmdK() {
@@ -36,7 +37,7 @@ export default function CmdK() {
. You can control settings, authentication, and file management from
the command bar, as well as a growing number of modeling commands.
-
+
)
diff --git a/src/routes/Onboarding/CodeEditor.tsx b/src/routes/Onboarding/CodeEditor.tsx
index 597885016d7..e0e4e47fba5 100644
--- a/src/routes/Onboarding/CodeEditor.tsx
+++ b/src/routes/Onboarding/CodeEditor.tsx
@@ -1,4 +1,5 @@
-import { ONBOARDING_SUBPATHS } from '@src/lib/onboardingPaths'
+import { onboardingPaths } from '@src/routes/Onboarding/paths'
+
import {
OnboardingButtons,
kbdClasses,
@@ -69,7 +70,7 @@ export default function OnboardingCodeEditor() {
pressing Shift + C .
-
+
)
diff --git a/src/routes/Onboarding/Export.tsx b/src/routes/Onboarding/Export.tsx
index a7274c06f74..5d14b4d876e 100644
--- a/src/routes/Onboarding/Export.tsx
+++ b/src/routes/Onboarding/Export.tsx
@@ -1,5 +1,6 @@
import { APP_NAME } from '@src/lib/constants'
-import { ONBOARDING_SUBPATHS } from '@src/lib/onboardingPaths'
+import { onboardingPaths } from '@src/routes/Onboarding/paths'
+
import { OnboardingButtons } from '@src/routes/Onboarding/utils'
export default function Export() {
@@ -49,7 +50,7 @@ export default function Export() {
!
-
+
)
diff --git a/src/routes/Onboarding/FutureWork.tsx b/src/routes/Onboarding/FutureWork.tsx
index 0fbd7585584..fa2756adfee 100644
--- a/src/routes/Onboarding/FutureWork.tsx
+++ b/src/routes/Onboarding/FutureWork.tsx
@@ -1,9 +1,11 @@
import { useEffect } from 'react'
+
import { useModelingContext } from '@src/hooks/useModelingContext'
import { APP_NAME } from '@src/lib/constants'
import { sceneInfra } from '@src/lib/singletons'
+import { onboardingPaths } from '@src/routes/Onboarding/paths'
+
import { OnboardingButtons, useDemoCode } from '@src/routes/Onboarding/utils'
-import { ONBOARDING_SUBPATHS } from '@src/lib/onboardingPaths'
export default function FutureWork() {
const { send } = useModelingContext()
@@ -56,7 +58,7 @@ export default function FutureWork() {
💚 The Zoo Team
diff --git a/src/routes/Onboarding/InteractiveNumbers.tsx b/src/routes/Onboarding/InteractiveNumbers.tsx
index a6b336ac2c8..d5217f08576 100644
--- a/src/routes/Onboarding/InteractiveNumbers.tsx
+++ b/src/routes/Onboarding/InteractiveNumbers.tsx
@@ -1,5 +1,6 @@
import { bracketWidthConstantLine } from '@src/lib/exampleKcl'
-import { ONBOARDING_SUBPATHS } from '@src/lib/onboardingPaths'
+import { onboardingPaths } from '@src/routes/Onboarding/paths'
+
import {
OnboardingButtons,
kbdClasses,
@@ -84,9 +85,7 @@ export default function OnboardingInteractiveNumbers() {
your ideas for how to make it better.
-
+
)
diff --git a/src/routes/Onboarding/Introduction.tsx b/src/routes/Onboarding/Introduction.tsx
index e89d7bf4360..ceb0eeb1730 100644
--- a/src/routes/Onboarding/Introduction.tsx
+++ b/src/routes/Onboarding/Introduction.tsx
@@ -1,11 +1,124 @@
+import { useEffect, useState } from 'react'
+import { useNavigate, useRouteLoaderData } from 'react-router-dom'
+
+import { useLspContext } from '@src/components/LspProvider'
+import { useFileContext } from '@src/hooks/useFileContext'
+import { isKclEmptyOrOnlySettings } from '@src/lang/wasm'
import { APP_NAME } from '@src/lib/constants'
+import { createAndOpenNewTutorialProject } from '@src/lib/desktopFS'
+import { bracket } from '@src/lib/exampleKcl'
import { isDesktop } from '@src/lib/isDesktop'
+import { PATHS } from '@src/lib/paths'
+import { codeManager, kclManager } from '@src/lib/singletons'
import { Themes, getSystemTheme } from '@src/lib/theme'
+import { reportRejection } from '@src/lib/trap'
+import type { IndexLoaderData } from '@src/lib/types'
import { useSettings } from '@src/lib/singletons'
+import { onboardingPaths } from '@src/routes/Onboarding/paths'
+
import { OnboardingButtons, useDemoCode } from '@src/routes/Onboarding/utils'
-import { ONBOARDING_SUBPATHS } from '@src/lib/onboardingPaths'
-export default function Introduction() {
+/**
+ * Show either a welcome screen or a warning screen
+ * depending on if the user has code in the editor.
+ */
+export default function OnboardingIntroduction() {
+ const [shouldShowWarning, setShouldShowWarning] = useState(
+ !isKclEmptyOrOnlySettings(codeManager.code) && codeManager.code !== bracket
+ )
+
+ return shouldShowWarning ? (
+
+ ) : (
+
+ )
+}
+
+interface OnboardingResetWarningProps {
+ setShouldShowWarning: (arg: boolean) => void
+}
+
+function OnboardingResetWarning(props: OnboardingResetWarningProps) {
+ return (
+
+
+ {!isDesktop() ? (
+
+ ) : (
+
+ )}
+
+
+ )
+}
+
+function OnboardingWarningDesktop(props: OnboardingResetWarningProps) {
+ const navigate = useNavigate()
+ const loaderData = useRouteLoaderData(PATHS.FILE) as IndexLoaderData
+ const { context: fileContext } = useFileContext()
+ const { onProjectClose, onProjectOpen } = useLspContext()
+
+ async function onAccept() {
+ onProjectClose(
+ loaderData.file || null,
+ fileContext.project.path || null,
+ false
+ )
+ await createAndOpenNewTutorialProject({ onProjectOpen, navigate })
+ props.setShouldShowWarning(false)
+ }
+
+ return (
+ <>
+
+ Would you like to create a new project?
+
+
+
+ You have some content in this project that we don't want to overwrite.
+ If you would like to create a new project, please click the button
+ below.
+
+
+ {
+ onAccept().catch(reportRejection)
+ }}
+ />
+ >
+ )
+}
+
+function OnboardingWarningWeb(props: OnboardingResetWarningProps) {
+ useEffect(() => {
+ async function beforeNavigate() {
+ // We do want to update both the state and editor here.
+ codeManager.updateCodeStateEditor(bracket)
+ await codeManager.writeToFile()
+
+ await kclManager.executeCode()
+ props.setShouldShowWarning(false)
+ }
+ return () => {
+ beforeNavigate().catch(reportRejection)
+ }
+ }, [])
+ return (
+ <>
+
+ Replaying onboarding resets your code
+
+
+ We see you have some of your own code written in this project. Please
+ save it somewhere else before continuing the onboarding.
+
+
+ >
+ )
+}
+
+function OnboardingIntroductionInner() {
// Reset the code to the bracket code
useDemoCode()
@@ -69,7 +182,7 @@ export default function Introduction() {
diff --git a/src/routes/Onboarding/ParametricModeling.tsx b/src/routes/Onboarding/ParametricModeling.tsx
index 189d0044930..a3fb6fa2468 100644
--- a/src/routes/Onboarding/ParametricModeling.tsx
+++ b/src/routes/Onboarding/ParametricModeling.tsx
@@ -2,8 +2,9 @@ import { bracketThicknessCalculationLine } from '@src/lib/exampleKcl'
import { isDesktop } from '@src/lib/isDesktop'
import { Themes, getSystemTheme } from '@src/lib/theme'
import { useSettings } from '@src/lib/singletons'
+import { onboardingPaths } from '@src/routes/Onboarding/paths'
+
import { OnboardingButtons, useDemoCode } from '@src/routes/Onboarding/utils'
-import { ONBOARDING_SUBPATHS } from '@src/lib/onboardingPaths'
export default function OnboardingParametricModeling() {
useDemoCode()
@@ -71,9 +72,7 @@ export default function OnboardingParametricModeling() {
-
+
)
diff --git a/src/routes/Onboarding/ProjectMenu.tsx b/src/routes/Onboarding/ProjectMenu.tsx
index 6e73d9a791d..b8f3ee3d488 100644
--- a/src/routes/Onboarding/ProjectMenu.tsx
+++ b/src/routes/Onboarding/ProjectMenu.tsx
@@ -1,5 +1,6 @@
import { isDesktop } from '@src/lib/isDesktop'
-import { ONBOARDING_SUBPATHS } from '@src/lib/onboardingPaths'
+import { onboardingPaths } from '@src/routes/Onboarding/paths'
+
import { OnboardingButtons } from '@src/routes/Onboarding/utils'
export default function ProjectMenu() {
@@ -55,7 +56,7 @@ export default function ProjectMenu() {
>
)}
-
+
)
diff --git a/src/routes/Onboarding/Sketching.tsx b/src/routes/Onboarding/Sketching.tsx
index 652557048f5..e24c76c1a10 100644
--- a/src/routes/Onboarding/Sketching.tsx
+++ b/src/routes/Onboarding/Sketching.tsx
@@ -1,7 +1,9 @@
import { useEffect } from 'react'
+
import { codeManager, kclManager } from '@src/lib/singletons'
+import { onboardingPaths } from '@src/routes/Onboarding/paths'
+
import { OnboardingButtons } from '@src/routes/Onboarding/utils'
-import { ONBOARDING_SUBPATHS } from '@src/lib/onboardingPaths'
export default function Sketching() {
useEffect(() => {
@@ -40,7 +42,7 @@ export default function Sketching() {
always just modifying and generating code in Zoo Design Studio.
diff --git a/src/routes/Onboarding/Streaming.tsx b/src/routes/Onboarding/Streaming.tsx
index 1583ad92247..ef052be5968 100644
--- a/src/routes/Onboarding/Streaming.tsx
+++ b/src/routes/Onboarding/Streaming.tsx
@@ -1,4 +1,5 @@
-import { ONBOARDING_SUBPATHS } from '@src/lib/onboardingPaths'
+import { onboardingPaths } from '@src/routes/Onboarding/paths'
+
import { OnboardingButtons } from '@src/routes/Onboarding/utils'
export default function Streaming() {
@@ -40,7 +41,7 @@ export default function Streaming() {
diff --git a/src/routes/Onboarding/Units.tsx b/src/routes/Onboarding/Units.tsx
index b56a1cde69c..cbccf8e17f8 100644
--- a/src/routes/Onboarding/Units.tsx
+++ b/src/routes/Onboarding/Units.tsx
@@ -4,12 +4,13 @@ import { ActionButton } from '@src/components/ActionButton'
import { SettingsSection } from '@src/components/Settings/SettingsSection'
import { type BaseUnit, baseUnitsUnion } from '@src/lib/settings/settingsTypes'
import { settingsActor, useSettings } from '@src/lib/singletons'
-import { ONBOARDING_SUBPATHS } from '@src/lib/onboardingPaths'
+import { onboardingPaths } from '@src/routes/Onboarding/paths'
+
import { useDismiss, useNextClick } from '@src/routes/Onboarding/utils'
export default function Units() {
const dismiss = useDismiss()
- const next = useNextClick(ONBOARDING_SUBPATHS.CAMERA)
+ const next = useNextClick(onboardingPaths.CAMERA)
const {
modeling: { defaultUnit },
} = useSettings()
diff --git a/src/routes/Onboarding/UserMenu.tsx b/src/routes/Onboarding/UserMenu.tsx
index 53347934937..e3e03fee924 100644
--- a/src/routes/Onboarding/UserMenu.tsx
+++ b/src/routes/Onboarding/UserMenu.tsx
@@ -1,7 +1,9 @@
import { useEffect, useState } from 'react'
+
import { useUser } from '@src/lib/singletons'
+import { onboardingPaths } from '@src/routes/Onboarding/paths'
+
import { OnboardingButtons } from '@src/routes/Onboarding/utils'
-import { ONBOARDING_SUBPATHS } from '@src/lib/onboardingPaths'
export default function UserMenu() {
const user = useUser()
@@ -46,7 +48,7 @@ export default function UserMenu() {
only apply to the current project.
-
+
)
diff --git a/src/routes/Onboarding/index.tsx b/src/routes/Onboarding/index.tsx
index dbf7be27824..1652d70ac96 100644
--- a/src/routes/Onboarding/index.tsx
+++ b/src/routes/Onboarding/index.tsx
@@ -14,8 +14,8 @@ import ProjectMenu from '@src/routes/Onboarding/ProjectMenu'
import Sketching from '@src/routes/Onboarding/Sketching'
import Streaming from '@src/routes/Onboarding/Streaming'
import UserMenu from '@src/routes/Onboarding/UserMenu'
+import { onboardingPaths } from '@src/routes/Onboarding/paths'
import { useDismiss } from '@src/routes/Onboarding/utils'
-import { ONBOARDING_SUBPATHS } from '@src/lib/onboardingPaths'
export const onboardingRoutes = [
{
@@ -23,48 +23,48 @@ export const onboardingRoutes = [
element: ,
},
{
- path: makeUrlPathRelative(ONBOARDING_SUBPATHS.CAMERA),
+ path: makeUrlPathRelative(onboardingPaths.CAMERA),
element: ,
},
{
- path: makeUrlPathRelative(ONBOARDING_SUBPATHS.STREAMING),
+ path: makeUrlPathRelative(onboardingPaths.STREAMING),
element: ,
},
{
- path: makeUrlPathRelative(ONBOARDING_SUBPATHS.EDITOR),
+ path: makeUrlPathRelative(onboardingPaths.EDITOR),
element: ,
},
{
- path: makeUrlPathRelative(ONBOARDING_SUBPATHS.PARAMETRIC_MODELING),
+ path: makeUrlPathRelative(onboardingPaths.PARAMETRIC_MODELING),
element: ,
},
{
- path: makeUrlPathRelative(ONBOARDING_SUBPATHS.INTERACTIVE_NUMBERS),
+ path: makeUrlPathRelative(onboardingPaths.INTERACTIVE_NUMBERS),
element: ,
},
{
- path: makeUrlPathRelative(ONBOARDING_SUBPATHS.COMMAND_K),
+ path: makeUrlPathRelative(onboardingPaths.COMMAND_K),
element: ,
},
{
- path: makeUrlPathRelative(ONBOARDING_SUBPATHS.USER_MENU),
+ path: makeUrlPathRelative(onboardingPaths.USER_MENU),
element: ,
},
{
- path: makeUrlPathRelative(ONBOARDING_SUBPATHS.PROJECT_MENU),
+ path: makeUrlPathRelative(onboardingPaths.PROJECT_MENU),
element: ,
},
{
- path: makeUrlPathRelative(ONBOARDING_SUBPATHS.EXPORT),
+ path: makeUrlPathRelative(onboardingPaths.EXPORT),
element: ,
},
// Export / conversion API
{
- path: makeUrlPathRelative(ONBOARDING_SUBPATHS.SKETCHING),
+ path: makeUrlPathRelative(onboardingPaths.SKETCHING),
element: ,
},
{
- path: makeUrlPathRelative(ONBOARDING_SUBPATHS.FUTURE_WORK),
+ path: makeUrlPathRelative(onboardingPaths.FUTURE_WORK),
element: ,
},
]
diff --git a/src/lib/onboardingPaths.ts b/src/routes/Onboarding/paths.ts
similarity index 83%
rename from src/lib/onboardingPaths.ts
rename to src/routes/Onboarding/paths.ts
index 95abdb1cae9..f8a069331ac 100644
--- a/src/lib/onboardingPaths.ts
+++ b/src/routes/Onboarding/paths.ts
@@ -1,6 +1,6 @@
import type { OnboardingStatus } from '@rust/kcl-lib/bindings/OnboardingStatus'
-export const ONBOARDING_SUBPATHS: Record = {
+export const onboardingPaths: Record = {
INDEX: '/',
CAMERA: '/camera',
STREAMING: '/streaming',
@@ -11,6 +11,7 @@ export const ONBOARDING_SUBPATHS: Record = {
USER_MENU: '/user-menu',
PROJECT_MENU: '/project-menu',
EXPORT: '/export',
+ MOVE: '/move',
SKETCHING: '/sketching',
FUTURE_WORK: '/future-work',
} as const
diff --git a/src/routes/Onboarding/utils.tsx b/src/routes/Onboarding/utils.tsx
index 5a72b4716d9..30309ad87a8 100644
--- a/src/routes/Onboarding/utils.tsx
+++ b/src/routes/Onboarding/utils.tsx
@@ -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) {
- 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 (
<>
(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'}
{stepNumber !== undefined && (
- {stepNumber} / {onboardingPathsArray.length}
+ {stepNumber} / {onboardingRoutes.length}
)}
{
- 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'}
>
)
}
-
-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,
- 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 (
-
-
-
-
- Welcome to Zoo Design Studio
-
- Would you like a tutorial to show you around the app?
-
-
-
-
- Not right now
-
-
- Get started
-
-
-
-
- )
-}
-
-/**
- * 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 (
-
-
-
-
- The welcome tutorial resets your code in the browser
-
- We see you have some of your own code written in this project.
- Please save it somewhere else before continuing the onboarding.
-
-
-
-
- I'll save it
-
-
- Overwrite and begin
-
-
-
-
- )
-}
diff --git a/src/routes/SignIn.tsx b/src/routes/SignIn.tsx
index fd14429ef31..67e48f30f98 100644
--- a/src/routes/SignIn.tsx
+++ b/src/routes/SignIn.tsx
@@ -26,9 +26,7 @@ const SignIn = () => {
if (isDesktop()) {
window.electron.createFallbackMenu().catch(reportRejection)
// Disable these since they cannot be accessed within the sign in page.
- window.electron
- .disableMenu('Help.Replay onboarding tutorial')
- .catch(reportRejection)
+ window.electron.disableMenu('Help.Reset onboarding').catch(reportRejection)
window.electron.disableMenu('Help.Show all commands').catch(reportRejection)
}
diff --git a/src/routes/utils.ts b/src/routes/utils.ts
index bd8bcbd9aa4..5cadccea19e 100644
--- a/src/routes/utils.ts
+++ b/src/routes/utils.ts
@@ -1,7 +1,7 @@
import { NODE_ENV } from '@src/env'
import { isDesktop } from '@src/lib/isDesktop'
-import { IS_PLAYWRIGHT_KEY } from '@src/lib/constants'
+import { IS_PLAYWRIGHT_KEY } from '@e2e/playwright/storageStates'
const isTestEnv = window?.localStorage.getItem(IS_PLAYWRIGHT_KEY) === 'true'