Revert "Make onboarding optional, able to be ignored on desktop" (#6610)
Revert "Make onboarding optional, able to be ignored on desktop (#6564)"
This reverts commit 820082d7f2
.
This commit is contained in:
@ -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<
|
||||
|
@ -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')
|
||||
|
@ -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()
|
||||
|
@ -1,32 +1,502 @@
|
||||
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')
|
||||
|
||||
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 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 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,
|
||||
})
|
||||
|
||||
// Mess with the code again
|
||||
await editor.replaceCode('', badCode)
|
||||
await editor.expectEditor.toContain(badCode, {
|
||||
shouldNormalise: true,
|
||||
timeout: 10_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',
|
||||
})
|
||||
|
||||
// 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()
|
||||
}
|
||||
|
||||
await tronApp.cleanProjectDir({
|
||||
app: {
|
||||
onboarding_status: '',
|
||||
},
|
||||
})
|
||||
|
||||
// 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 page.setBodyDimensions({ width: 1200, height: 500 })
|
||||
await homePage.goToModelingScene()
|
||||
|
||||
// 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!',
|
||||
})
|
||||
})
|
||||
|
||||
// 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 page.reload({ waitUntil: 'domcontentloaded' })
|
||||
|
||||
// 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 page.setBodyDimensions({ width: 1200, height: 500 })
|
||||
await homePage.goToModelingScene()
|
||||
|
||||
// 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)
|
||||
|
||||
// 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',
|
||||
@ -38,138 +508,53 @@ test.describe('Onboarding tests', () => {
|
||||
const restartOnboardingSettingsButton = page.getByRole('button', {
|
||||
name: 'Replay onboarding',
|
||||
})
|
||||
const helpMenuButton = page.getByRole('button', {
|
||||
name: 'Help and resources',
|
||||
})
|
||||
const helpMenuRestartOnboardingButton = page.getByRole('button', {
|
||||
name: 'Replay onboarding tutorial',
|
||||
})
|
||||
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',
|
||||
})
|
||||
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('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 })
|
||||
})
|
||||
await test.step('Restart the onboarding from help menu', async () => {
|
||||
await helpMenuButton.click()
|
||||
await restartOnboardingButton.click()
|
||||
|
||||
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()
|
||||
})
|
||||
|
||||
// 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 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()
|
||||
}
|
||||
})
|
||||
|
||||
await test.step('Going backward', async () => {
|
||||
while ((await prevButton.innerText()) !== 'Dismiss') {
|
||||
await prevButton.hover()
|
||||
await prevButton.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)
|
||||
})
|
||||
|
||||
// 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')
|
||||
})
|
||||
})
|
||||
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('Resetting onboarding from inside project should always make a new one', async () => {
|
||||
await test.step('Reset onboarding from settings', async () => {
|
||||
await test.step('Navigate to settings', async () => {
|
||||
await userMenuButton.click()
|
||||
await userMenuSettingsButton.click()
|
||||
await expect(settingsHeading).toBeVisible()
|
||||
await expect(restartOnboardingSettingsButton).toBeVisible()
|
||||
})
|
||||
|
||||
await restartOnboardingSettingsButton.click()
|
||||
})
|
||||
|
||||
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 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')
|
||||
})
|
||||
})
|
||||
|
||||
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 helpMenuButton.click()
|
||||
await helpMenuRestartOnboardingButton.click()
|
||||
})
|
||||
|
||||
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 })
|
||||
})
|
||||
})
|
||||
// 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()
|
||||
})
|
||||
})
|
||||
|
@ -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<Settings> = {
|
||||
app: {
|
||||
@ -31,15 +33,12 @@ export const TEST_SETTINGS: DeepPartial<Settings> = {
|
||||
|
||||
export const TEST_SETTINGS_ONBOARDING_USER_MENU: DeepPartial<Settings> = {
|
||||
...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<Settings> = {
|
||||
...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<Settings> =
|
||||
@ -47,7 +46,7 @@ export const TEST_SETTINGS_ONBOARDING_PARAMETRIC_MODELING: DeepPartial<Settings>
|
||||
...TEST_SETTINGS,
|
||||
app: {
|
||||
...TEST_SETTINGS.app,
|
||||
onboarding_status: ONBOARDING_SUBPATHS.PARAMETRIC_MODELING,
|
||||
onboarding_status: onboardingPaths.PARAMETRIC_MODELING,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -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) => {
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
55
src/App.tsx
55
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<HTMLDivElement>(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 (
|
||||
<div className="relative h-full flex flex-col" ref={ref}>
|
||||
<AppHeader
|
||||
@ -190,7 +155,7 @@ export function App() {
|
||||
<ModelingSidebar paneOpacity={paneOpacity} />
|
||||
<EngineStream pool={pool} authToken={authToken} />
|
||||
{/* <CamToggle /> */}
|
||||
<LowerRightControls navigate={navigate}>
|
||||
<LowerRightControls>
|
||||
<UnitsMenu />
|
||||
<Gizmo />
|
||||
</LowerRightControls>
|
||||
|
@ -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: <Telemetry />,
|
||||
},
|
||||
],
|
||||
|
@ -854,14 +854,6 @@ const CustomIconMap = {
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
play: (
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M14.2842 9.83887L14.2617 10.6758L7.76172 14.6758L7 14.25V5.75L7.78418 5.33887L14.2842 9.83887ZM8 13.3555L13.0859 10.2246L8 6.70312V13.3555Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
rotate: (
|
||||
<svg
|
||||
viewBox="0 0 20 20"
|
||||
|
@ -1,46 +1,49 @@
|
||||
import { Popover } from '@headlessui/react'
|
||||
import { type NavigateFunction, useLocation } from 'react-router-dom'
|
||||
import { useLocation, useNavigate } from 'react-router-dom'
|
||||
|
||||
import { CustomIcon } from '@src/components/CustomIcon'
|
||||
import { useLspContext } from '@src/components/LspProvider'
|
||||
import Tooltip from '@src/components/Tooltip'
|
||||
import { useAbsoluteFilePath } from '@src/hooks/useAbsoluteFilePath'
|
||||
import { useMenuListener } from '@src/hooks/useMenu'
|
||||
import { createAndOpenNewTutorialProject } from '@src/lib/desktopFS'
|
||||
import { openExternalBrowserIfDesktop } from '@src/lib/openWindow'
|
||||
import { PATHS } from '@src/lib/paths'
|
||||
import { codeManager, kclManager } from '@src/lib/singletons'
|
||||
import { reportRejection } from '@src/lib/trap'
|
||||
import { settingsActor } from '@src/lib/singletons'
|
||||
import type { WebContentSendPayload } from '@src/menu/channels'
|
||||
import {
|
||||
acceptOnboarding,
|
||||
catchOnboardingWarnError,
|
||||
} from '@src/routes/Onboarding/utils'
|
||||
import { ONBOARDING_SUBPATHS } from '@src/lib/onboardingPaths'
|
||||
|
||||
const HelpMenuDivider = () => (
|
||||
<div className="h-[1px] bg-chalkboard-110 dark:bg-chalkboard-80" />
|
||||
)
|
||||
|
||||
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,
|
||||
settingsActor.send({
|
||||
type: 'set.app.onboardingStatus',
|
||||
data: {
|
||||
value: '',
|
||||
level: 'user',
|
||||
},
|
||||
})
|
||||
if (isInProject) {
|
||||
navigate(filePath + PATHS.ONBOARDING.INDEX)
|
||||
} else {
|
||||
createAndOpenNewTutorialProject({
|
||||
onProjectOpen,
|
||||
navigate,
|
||||
codeManager,
|
||||
kclManager,
|
||||
}).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,8 +68,6 @@ 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 }) => (
|
||||
<>
|
||||
<HelpMenuItem
|
||||
as="a"
|
||||
href="https://github.com/KittyCAD/modeling-app/issues/new/choose"
|
||||
@ -129,17 +130,9 @@ export function HelpMenu({
|
||||
>
|
||||
Keyboard shortcuts
|
||||
</HelpMenuItem>
|
||||
<HelpMenuItem
|
||||
as="button"
|
||||
onClick={() => {
|
||||
close()
|
||||
resetOnboardingWorkflow()
|
||||
}}
|
||||
>
|
||||
Replay onboarding tutorial
|
||||
<HelpMenuItem as="button" onClick={resetOnboardingWorkflow}>
|
||||
Reset onboarding
|
||||
</HelpMenuItem>
|
||||
</>
|
||||
)}
|
||||
</Popover.Panel>
|
||||
</Popover>
|
||||
)
|
||||
|
@ -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) && (
|
||||
<NetworkHealthIndicator />
|
||||
)}
|
||||
<HelpMenu navigate={navigate} />
|
||||
<HelpMenu />
|
||||
</menu>
|
||||
</section>
|
||||
)
|
||||
|
@ -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 (
|
||||
|
@ -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 '
|
||||
|
@ -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])
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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 (
|
||||
|
@ -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'
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -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<string, string>) => (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() || ''
|
||||
}
|
||||
|
@ -22,6 +22,12 @@ import type {
|
||||
} from '@src/lib/types'
|
||||
import { settingsActor } from '@src/lib/singletons'
|
||||
|
||||
export const telemetryLoader: LoaderFunction = async ({
|
||||
params,
|
||||
}): Promise<null> => {
|
||||
return null
|
||||
}
|
||||
|
||||
export const fileLoader: LoaderFunction = async (
|
||||
routerData
|
||||
): Promise<FileLoaderData | Response> => {
|
||||
|
@ -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,
|
||||
}
|
||||
},
|
||||
}),
|
||||
|
@ -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 || '',
|
||||
}
|
||||
}
|
||||
),
|
||||
|
@ -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 || '',
|
||||
}
|
||||
}
|
||||
),
|
||||
|
@ -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: {
|
||||
|
@ -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'
|
||||
|
@ -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',
|
||||
})
|
||||
},
|
||||
},
|
||||
|
@ -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'
|
||||
|
@ -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 (
|
||||
<div className="relative flex flex-col h-screen overflow-hidden" ref={ref}>
|
||||
<AppHeader showToolbar={false} />
|
||||
<div className="overflow-hidden flex-1 home-layout max-w-4xl xl:max-w-7xl mb-12 px-4 mx-auto mt-24 lg:px-0">
|
||||
<div className="overflow-hidden home-layout max-w-4xl xl:max-w-7xl mb-12 px-4 mx-auto mt-24 lg:px-0">
|
||||
<HomeHeader
|
||||
setQuery={setQuery}
|
||||
sort={sort}
|
||||
@ -215,44 +202,8 @@ const Home = () => {
|
||||
readWriteProjectDir={readWriteProjectDir}
|
||||
className="col-start-2 -col-end-1"
|
||||
/>
|
||||
<aside className="lg:row-start-1 -row-end-1 flex flex-col justify-between">
|
||||
<aside className="row-start-1 -row-end-1 flex flex-col justify-between">
|
||||
<ul className="flex flex-col">
|
||||
{needsToOnboard(location, onboardingStatus) && (
|
||||
<li className="flex group">
|
||||
<ActionButton
|
||||
Element="button"
|
||||
onClick={() => {
|
||||
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
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
Element="button"
|
||||
onClick={onDismissOnboardingInvite}
|
||||
className={`${sidebarButtonClasses} hidden group-hover:flex flex-none ml-auto`}
|
||||
iconStart={{
|
||||
icon: 'close',
|
||||
bgClassName: '!bg-transparent rounded-sm',
|
||||
}}
|
||||
data-testid="onboarding-dismiss"
|
||||
>
|
||||
<Tooltip>Dismiss tutorial</Tooltip>
|
||||
</ActionButton>
|
||||
</li>
|
||||
)}
|
||||
<li className="contents">
|
||||
<ActionButton
|
||||
Element="button"
|
||||
@ -373,7 +324,7 @@ const Home = () => {
|
||||
sort={sort}
|
||||
className="flex-1 col-start-2 -col-end-1 overflow-y-auto pr-2 pb-24"
|
||||
/>
|
||||
<LowerRightControls navigate={navigate} />
|
||||
<LowerRightControls />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
@ -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() {
|
||||
</ul>
|
||||
</SettingsSection>
|
||||
<OnboardingButtons
|
||||
currentSlug={ONBOARDING_SUBPATHS.CAMERA}
|
||||
currentSlug={onboardingPaths.CAMERA}
|
||||
dismissClassName="right-auto left-full"
|
||||
/>
|
||||
</div>
|
||||
|
@ -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.
|
||||
</p>
|
||||
<OnboardingButtons currentSlug={ONBOARDING_SUBPATHS.COMMAND_K} />
|
||||
<OnboardingButtons currentSlug={onboardingPaths.COMMAND_K} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
@ -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 <kbd className={kbdClasses}>Shift + C</kbd>.
|
||||
</p>
|
||||
</section>
|
||||
<OnboardingButtons currentSlug={ONBOARDING_SUBPATHS.EDITOR} />
|
||||
<OnboardingButtons currentSlug={onboardingPaths.EDITOR} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
@ -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() {
|
||||
!
|
||||
</p>
|
||||
</section>
|
||||
<OnboardingButtons currentSlug={ONBOARDING_SUBPATHS.EXPORT} />
|
||||
<OnboardingButtons currentSlug={onboardingPaths.EXPORT} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
@ -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() {
|
||||
</p>
|
||||
<p className="my-4">💚 The Zoo Team</p>
|
||||
<OnboardingButtons
|
||||
currentSlug={ONBOARDING_SUBPATHS.FUTURE_WORK}
|
||||
currentSlug={onboardingPaths.FUTURE_WORK}
|
||||
className="mt-6"
|
||||
/>
|
||||
</div>
|
||||
|
@ -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.
|
||||
</p>
|
||||
</section>
|
||||
<OnboardingButtons
|
||||
currentSlug={ONBOARDING_SUBPATHS.INTERACTIVE_NUMBERS}
|
||||
/>
|
||||
<OnboardingButtons currentSlug={onboardingPaths.INTERACTIVE_NUMBERS} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
@ -1,11 +1,124 @@
|
||||
import { APP_NAME } from '@src/lib/constants'
|
||||
import { isDesktop } from '@src/lib/isDesktop'
|
||||
import { Themes, getSystemTheme } from '@src/lib/theme'
|
||||
import { useSettings } from '@src/lib/singletons'
|
||||
import { OnboardingButtons, useDemoCode } from '@src/routes/Onboarding/utils'
|
||||
import { ONBOARDING_SUBPATHS } from '@src/lib/onboardingPaths'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useNavigate, useRouteLoaderData } from 'react-router-dom'
|
||||
|
||||
export default function Introduction() {
|
||||
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'
|
||||
|
||||
/**
|
||||
* 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 ? (
|
||||
<OnboardingResetWarning setShouldShowWarning={setShouldShowWarning} />
|
||||
) : (
|
||||
<OnboardingIntroductionInner />
|
||||
)
|
||||
}
|
||||
|
||||
interface OnboardingResetWarningProps {
|
||||
setShouldShowWarning: (arg: boolean) => void
|
||||
}
|
||||
|
||||
function OnboardingResetWarning(props: OnboardingResetWarningProps) {
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 grid place-content-center bg-chalkboard-110/50">
|
||||
<div className="relative max-w-3xl p-8 rounded bg-chalkboard-10 dark:bg-chalkboard-90">
|
||||
{!isDesktop() ? (
|
||||
<OnboardingWarningWeb {...props} />
|
||||
) : (
|
||||
<OnboardingWarningDesktop {...props} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<>
|
||||
<h1 className="flex flex-wrap items-center gap-4 text-3xl font-bold">
|
||||
Would you like to create a new project?
|
||||
</h1>
|
||||
<section className="my-12">
|
||||
<p className="my-4">
|
||||
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.
|
||||
</p>
|
||||
</section>
|
||||
<OnboardingButtons
|
||||
className="mt-6"
|
||||
onNextOverride={() => {
|
||||
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 (
|
||||
<>
|
||||
<h1 className="text-3xl font-bold text-warn-80 dark:text-warn-10">
|
||||
Replaying onboarding resets your code
|
||||
</h1>
|
||||
<p className="my-4">
|
||||
We see you have some of your own code written in this project. Please
|
||||
save it somewhere else before continuing the onboarding.
|
||||
</p>
|
||||
<OnboardingButtons className="mt-6" />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function OnboardingIntroductionInner() {
|
||||
// Reset the code to the bracket code
|
||||
useDemoCode()
|
||||
|
||||
@ -69,7 +182,7 @@ export default function Introduction() {
|
||||
</p>
|
||||
</section>
|
||||
<OnboardingButtons
|
||||
currentSlug={ONBOARDING_SUBPATHS.INDEX}
|
||||
currentSlug={onboardingPaths.INDEX}
|
||||
className="mt-6"
|
||||
/>
|
||||
</div>
|
||||
|
@ -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() {
|
||||
</figcaption>
|
||||
</figure>
|
||||
</section>
|
||||
<OnboardingButtons
|
||||
currentSlug={ONBOARDING_SUBPATHS.PARAMETRIC_MODELING}
|
||||
/>
|
||||
<OnboardingButtons currentSlug={onboardingPaths.PARAMETRIC_MODELING} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
@ -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() {
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
<OnboardingButtons currentSlug={ONBOARDING_SUBPATHS.PROJECT_MENU} />
|
||||
<OnboardingButtons currentSlug={onboardingPaths.PROJECT_MENU} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
@ -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.
|
||||
</p>
|
||||
<OnboardingButtons
|
||||
currentSlug={ONBOARDING_SUBPATHS.SKETCHING}
|
||||
currentSlug={onboardingPaths.SKETCHING}
|
||||
className="mt-6"
|
||||
/>
|
||||
</div>
|
||||
|
@ -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() {
|
||||
</p>
|
||||
</section>
|
||||
<OnboardingButtons
|
||||
currentSlug={ONBOARDING_SUBPATHS.STREAMING}
|
||||
currentSlug={onboardingPaths.STREAMING}
|
||||
dismissClassName="right-auto left-full"
|
||||
/>
|
||||
</div>
|
||||
|
@ -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()
|
||||
|
@ -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.
|
||||
</p>
|
||||
</section>
|
||||
<OnboardingButtons currentSlug={ONBOARDING_SUBPATHS.USER_MENU} />
|
||||
<OnboardingButtons currentSlug={onboardingPaths.USER_MENU} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
@ -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: <Introduction />,
|
||||
},
|
||||
{
|
||||
path: makeUrlPathRelative(ONBOARDING_SUBPATHS.CAMERA),
|
||||
path: makeUrlPathRelative(onboardingPaths.CAMERA),
|
||||
element: <Camera />,
|
||||
},
|
||||
{
|
||||
path: makeUrlPathRelative(ONBOARDING_SUBPATHS.STREAMING),
|
||||
path: makeUrlPathRelative(onboardingPaths.STREAMING),
|
||||
element: <Streaming />,
|
||||
},
|
||||
{
|
||||
path: makeUrlPathRelative(ONBOARDING_SUBPATHS.EDITOR),
|
||||
path: makeUrlPathRelative(onboardingPaths.EDITOR),
|
||||
element: <CodeEditor />,
|
||||
},
|
||||
{
|
||||
path: makeUrlPathRelative(ONBOARDING_SUBPATHS.PARAMETRIC_MODELING),
|
||||
path: makeUrlPathRelative(onboardingPaths.PARAMETRIC_MODELING),
|
||||
element: <ParametricModeling />,
|
||||
},
|
||||
{
|
||||
path: makeUrlPathRelative(ONBOARDING_SUBPATHS.INTERACTIVE_NUMBERS),
|
||||
path: makeUrlPathRelative(onboardingPaths.INTERACTIVE_NUMBERS),
|
||||
element: <InteractiveNumbers />,
|
||||
},
|
||||
{
|
||||
path: makeUrlPathRelative(ONBOARDING_SUBPATHS.COMMAND_K),
|
||||
path: makeUrlPathRelative(onboardingPaths.COMMAND_K),
|
||||
element: <CmdK />,
|
||||
},
|
||||
{
|
||||
path: makeUrlPathRelative(ONBOARDING_SUBPATHS.USER_MENU),
|
||||
path: makeUrlPathRelative(onboardingPaths.USER_MENU),
|
||||
element: <UserMenu />,
|
||||
},
|
||||
{
|
||||
path: makeUrlPathRelative(ONBOARDING_SUBPATHS.PROJECT_MENU),
|
||||
path: makeUrlPathRelative(onboardingPaths.PROJECT_MENU),
|
||||
element: <ProjectMenu />,
|
||||
},
|
||||
{
|
||||
path: makeUrlPathRelative(ONBOARDING_SUBPATHS.EXPORT),
|
||||
path: makeUrlPathRelative(onboardingPaths.EXPORT),
|
||||
element: <Export />,
|
||||
},
|
||||
// Export / conversion API
|
||||
{
|
||||
path: makeUrlPathRelative(ONBOARDING_SUBPATHS.SKETCHING),
|
||||
path: makeUrlPathRelative(onboardingPaths.SKETCHING),
|
||||
element: <Sketching />,
|
||||
},
|
||||
{
|
||||
path: makeUrlPathRelative(ONBOARDING_SUBPATHS.FUTURE_WORK),
|
||||
path: makeUrlPathRelative(onboardingPaths.FUTURE_WORK),
|
||||
element: <FutureWork />,
|
||||
},
|
||||
]
|
||||
|
@ -1,6 +1,6 @@
|
||||
import type { OnboardingStatus } from '@rust/kcl-lib/bindings/OnboardingStatus'
|
||||
|
||||
export const ONBOARDING_SUBPATHS: Record<string, OnboardingStatus> = {
|
||||
export const onboardingPaths: Record<string, OnboardingStatus> = {
|
||||
INDEX: '/',
|
||||
CAMERA: '/camera',
|
||||
STREAMING: '/streaming',
|
||||
@ -11,6 +11,7 @@ export const ONBOARDING_SUBPATHS: Record<string, OnboardingStatus> = {
|
||||
USER_MENU: '/user-menu',
|
||||
PROJECT_MENU: '/project-menu',
|
||||
EXPORT: '/export',
|
||||
MOVE: '/move',
|
||||
SKETCHING: '/sketching',
|
||||
FUTURE_WORK: '/future-work',
|
||||
} as const
|
@ -1,9 +1,5 @@
|
||||
import { useCallback, useEffect } from 'react'
|
||||
import {
|
||||
type NavigateFunction,
|
||||
type useLocation,
|
||||
useNavigate,
|
||||
} from 'react-router-dom'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { waitFor } from 'xstate'
|
||||
|
||||
import { ActionButton } from '@src/components/ActionButton'
|
||||
@ -15,39 +11,30 @@ import { NetworkHealthState } from '@src/hooks/useNetworkStatus'
|
||||
import { EngineConnectionStateType } from '@src/lang/std/engineConnection'
|
||||
import { bracket } from '@src/lib/exampleKcl'
|
||||
import makeUrlPathRelative from '@src/lib/makeUrlPathRelative'
|
||||
import { joinRouterPaths, PATHS } from '@src/lib/paths'
|
||||
import {
|
||||
codeManager,
|
||||
editorManager,
|
||||
kclManager,
|
||||
systemIOActor,
|
||||
} from '@src/lib/singletons'
|
||||
import { PATHS } from '@src/lib/paths'
|
||||
import { codeManager, editorManager, kclManager } from '@src/lib/singletons'
|
||||
import { reportRejection, trap } from '@src/lib/trap'
|
||||
import { settingsActor } from '@src/lib/singletons'
|
||||
import { isKclEmptyOrOnlySettings, parse, resultIsOk } from '@src/lang/wasm'
|
||||
import { onboardingRoutes } from '@src/routes/Onboarding'
|
||||
import { onboardingPaths } from '@src/routes/Onboarding/paths'
|
||||
import { parse, resultIsOk } from '@src/lang/wasm'
|
||||
import { updateModelingState } from '@src/lang/modelingWorkflows'
|
||||
import {
|
||||
DEFAULT_PROJECT_KCL_FILE,
|
||||
EXECUTION_TYPE_REAL,
|
||||
ONBOARDING_PROJECT_NAME,
|
||||
} from '@src/lib/constants'
|
||||
import toast from 'react-hot-toast'
|
||||
import type CodeManager from '@src/lang/codeManager'
|
||||
import type { OnboardingStatus } from '@rust/kcl-lib/bindings/OnboardingStatus'
|
||||
import { isDesktop } from '@src/lib/isDesktop'
|
||||
import type { KclManager } from '@src/lang/KclSingleton'
|
||||
import { Logo } from '@src/components/Logo'
|
||||
import { SystemIOMachineEvents } from '@src/machines/systemIO/utils'
|
||||
import { ONBOARDING_SUBPATHS } from '@src/lib/onboardingPaths'
|
||||
import { EXECUTION_TYPE_REAL } from '@src/lib/constants'
|
||||
|
||||
export const kbdClasses =
|
||||
'py-0.5 px-1 text-sm rounded bg-chalkboard-10 dark:bg-chalkboard-100 border border-chalkboard-50 border-b-2'
|
||||
|
||||
// Get the 1-indexed step number of the current onboarding step
|
||||
function useStepNumber(
|
||||
slug?: (typeof ONBOARDING_SUBPATHS)[keyof typeof ONBOARDING_SUBPATHS]
|
||||
slug?: (typeof onboardingPaths)[keyof typeof onboardingPaths]
|
||||
) {
|
||||
return slug ? Object.values(ONBOARDING_SUBPATHS).indexOf(slug) + 1 : -1
|
||||
return slug
|
||||
? slug === onboardingPaths.INDEX
|
||||
? 1
|
||||
: onboardingRoutes.findIndex(
|
||||
(r) => r.path === makeUrlPathRelative(slug)
|
||||
) + 1
|
||||
: 1
|
||||
}
|
||||
|
||||
export function useDemoCode() {
|
||||
@ -93,7 +80,7 @@ export function useNextClick(newStatus: string) {
|
||||
data: { level: 'user', value: newStatus },
|
||||
})
|
||||
navigate(filePath + PATHS.ONBOARDING.INDEX.slice(0, -1) + newStatus)
|
||||
}, [filePath, newStatus, navigate])
|
||||
}, [filePath, newStatus, settingsActor.send, navigate])
|
||||
}
|
||||
|
||||
export function useDismiss() {
|
||||
@ -107,17 +94,9 @@ export function useDismiss() {
|
||||
data: { level: 'user', value: 'dismissed' },
|
||||
})
|
||||
waitFor(settingsActor, (state) => state.matches('idle'))
|
||||
.then(() => {
|
||||
navigate(filePath)
|
||||
toast.success(
|
||||
'Click the question mark in the lower-right corner if you ever want to redo the tutorial!',
|
||||
{
|
||||
duration: 5_000,
|
||||
}
|
||||
)
|
||||
})
|
||||
.then(() => navigate(filePath))
|
||||
.catch(reportRejection)
|
||||
}, [send, filePath, navigate])
|
||||
}, [send])
|
||||
|
||||
return settingsCallback
|
||||
}
|
||||
@ -128,31 +107,32 @@ export function OnboardingButtons({
|
||||
onNextOverride,
|
||||
...props
|
||||
}: {
|
||||
currentSlug?: (typeof ONBOARDING_SUBPATHS)[keyof typeof ONBOARDING_SUBPATHS]
|
||||
currentSlug?: (typeof onboardingPaths)[keyof typeof onboardingPaths]
|
||||
className?: string
|
||||
dismissClassName?: string
|
||||
onNextOverride?: () => void
|
||||
} & React.HTMLAttributes<HTMLDivElement>) {
|
||||
const onboardingPathsArray = Object.values(ONBOARDING_SUBPATHS)
|
||||
const dismiss = useDismiss()
|
||||
const stepNumber = useStepNumber(currentSlug)
|
||||
const previousStep =
|
||||
!stepNumber || stepNumber <= 1 ? null : onboardingPathsArray[stepNumber]
|
||||
const goToPrevious = useNextClick(previousStep ?? ONBOARDING_SUBPATHS.INDEX)
|
||||
!stepNumber || stepNumber === 0 ? null : onboardingRoutes[stepNumber - 2]
|
||||
const goToPrevious = useNextClick(
|
||||
onboardingPaths.INDEX + (previousStep?.path ?? '')
|
||||
)
|
||||
const nextStep =
|
||||
!stepNumber || stepNumber === onboardingPathsArray.length
|
||||
!stepNumber || stepNumber === onboardingRoutes.length
|
||||
? null
|
||||
: onboardingPathsArray[stepNumber]
|
||||
const goToNext = useNextClick(nextStep + ONBOARDING_SUBPATHS.INDEX)
|
||||
: onboardingRoutes[stepNumber]
|
||||
const goToNext = useNextClick(onboardingPaths.INDEX + (nextStep?.path ?? ''))
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
onClick={dismiss}
|
||||
className={`group block !absolute left-auto right-full top-[-3px] m-2.5 p-0 border-none bg-transparent hover:bg-transparent ${
|
||||
className={
|
||||
'group block !absolute left-auto right-full top-[-3px] m-2.5 p-0 border-none bg-transparent hover:bg-transparent ' +
|
||||
dismissClassName
|
||||
}`}
|
||||
}
|
||||
data-testid="onboarding-dismiss"
|
||||
>
|
||||
<CustomIcon
|
||||
@ -164,12 +144,16 @@ export function OnboardingButtons({
|
||||
</Tooltip>
|
||||
</button>
|
||||
<div
|
||||
className={`flex items-center justify-between ${className ?? ''}`}
|
||||
className={'flex items-center justify-between ' + (className ?? '')}
|
||||
{...props}
|
||||
>
|
||||
<ActionButton
|
||||
Element="button"
|
||||
onClick={() => (previousStep ? goToPrevious() : dismiss())}
|
||||
onClick={() =>
|
||||
previousStep?.path || previousStep?.index
|
||||
? goToPrevious()
|
||||
: dismiss()
|
||||
}
|
||||
iconStart={{
|
||||
icon: previousStep ? 'arrowLeft' : 'close',
|
||||
className: 'text-chalkboard-10',
|
||||
@ -178,18 +162,18 @@ export function OnboardingButtons({
|
||||
className="hover:border-destroy-40 hover:bg-destroy-10/50 dark:hover:bg-destroy-80/50"
|
||||
data-testid="onboarding-prev"
|
||||
>
|
||||
{previousStep ? 'Back' : 'Dismiss'}
|
||||
{previousStep ? `Back` : 'Dismiss'}
|
||||
</ActionButton>
|
||||
{stepNumber !== undefined && (
|
||||
<p className="font-mono text-xs text-center m-0">
|
||||
{stepNumber} / {onboardingPathsArray.length}
|
||||
{stepNumber} / {onboardingRoutes.length}
|
||||
</p>
|
||||
)}
|
||||
<ActionButton
|
||||
autoFocus
|
||||
Element="button"
|
||||
onClick={() => {
|
||||
if (nextStep) {
|
||||
if (nextStep?.path) {
|
||||
onNextOverride ? onNextOverride() : goToNext()
|
||||
} else {
|
||||
dismiss()
|
||||
@ -202,221 +186,9 @@ export function OnboardingButtons({
|
||||
className="dark:hover:bg-chalkboard-80/50"
|
||||
data-testid="onboarding-next"
|
||||
>
|
||||
{nextStep ? 'Next' : 'Finish'}
|
||||
{nextStep ? `Next` : 'Finish'}
|
||||
</ActionButton>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export interface OnboardingUtilDeps {
|
||||
onboardingStatus: OnboardingStatus
|
||||
codeManager: CodeManager
|
||||
kclManager: KclManager
|
||||
navigate: NavigateFunction
|
||||
}
|
||||
|
||||
export const ERROR_MUST_WARN = 'Must warn user before overwrite'
|
||||
|
||||
/**
|
||||
* Accept to begin the onboarding tutorial,
|
||||
* depending on the platform and the state of the user's code.
|
||||
*/
|
||||
export async function acceptOnboarding(deps: OnboardingUtilDeps) {
|
||||
if (isDesktop()) {
|
||||
/** TODO: rename this event to be more generic, like `createKCLFileAndNavigate` */
|
||||
systemIOActor.send({
|
||||
type: SystemIOMachineEvents.importFileFromURL,
|
||||
data: {
|
||||
requestedProjectName: ONBOARDING_PROJECT_NAME,
|
||||
requestedFileName: DEFAULT_PROJECT_KCL_FILE,
|
||||
requestedCode: bracket,
|
||||
requestedSubRoute: joinRouterPaths(
|
||||
PATHS.ONBOARDING.INDEX,
|
||||
deps.onboardingStatus
|
||||
),
|
||||
},
|
||||
})
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
const isCodeResettable = hasResetReadyCode(deps.codeManager)
|
||||
if (isCodeResettable) {
|
||||
return resetCodeAndAdvanceOnboarding(deps)
|
||||
}
|
||||
|
||||
return Promise.reject(new Error(ERROR_MUST_WARN))
|
||||
}
|
||||
|
||||
/**
|
||||
* Given that the user has accepted overwriting their web editor,
|
||||
* advance to the next step and clear their editor.
|
||||
*/
|
||||
export async function resetCodeAndAdvanceOnboarding({
|
||||
onboardingStatus,
|
||||
codeManager,
|
||||
kclManager,
|
||||
navigate,
|
||||
}: OnboardingUtilDeps) {
|
||||
// We do want to update both the state and editor here.
|
||||
codeManager.updateCodeStateEditor(bracket)
|
||||
codeManager.writeToFile().catch(reportRejection)
|
||||
kclManager.executeCode().catch(reportRejection)
|
||||
// TODO: this is not navigating to the correct `/onboarding/blah` path yet
|
||||
navigate(
|
||||
makeUrlPathRelative(
|
||||
`${PATHS.ONBOARDING.INDEX}${makeUrlPathRelative(onboardingStatus)}`
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
function hasResetReadyCode(codeManager: CodeManager) {
|
||||
return (
|
||||
isKclEmptyOrOnlySettings(codeManager.code) || codeManager.code === bracket
|
||||
)
|
||||
}
|
||||
|
||||
export function needsToOnboard(
|
||||
location: ReturnType<typeof useLocation>,
|
||||
onboardingStatus: OnboardingStatus
|
||||
) {
|
||||
return (
|
||||
!location.pathname.includes(PATHS.ONBOARDING.INDEX) &&
|
||||
(onboardingStatus.length === 0 ||
|
||||
!(onboardingStatus === 'completed' || onboardingStatus === 'dismissed'))
|
||||
)
|
||||
}
|
||||
|
||||
export const ONBOARDING_TOAST_ID = 'onboarding-toast'
|
||||
|
||||
export function onDismissOnboardingInvite() {
|
||||
settingsActor.send({
|
||||
type: 'set.app.onboardingStatus',
|
||||
data: { level: 'user', value: 'dismissed' },
|
||||
})
|
||||
toast.dismiss(ONBOARDING_TOAST_ID)
|
||||
toast.success(
|
||||
'Click the question mark in the lower-right corner if you ever want to do the tutorial!',
|
||||
{
|
||||
duration: 5_000,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export function TutorialRequestToast(props: OnboardingUtilDeps) {
|
||||
function onAccept() {
|
||||
acceptOnboarding(props)
|
||||
.then(() => {
|
||||
toast.dismiss(ONBOARDING_TOAST_ID)
|
||||
})
|
||||
.catch((reason) => catchOnboardingWarnError(reason, props))
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
data-testid="onboarding-toast"
|
||||
className="flex items-center gap-6 min-w-80"
|
||||
>
|
||||
<Logo className="w-auto h-8 flex-none" />
|
||||
<div className="flex flex-col justify-between gap-6">
|
||||
<section>
|
||||
<h2>Welcome to Zoo Design Studio</h2>
|
||||
<p className="text-sm text-chalkboard-70 dark:text-chalkboard-30">
|
||||
Would you like a tutorial to show you around the app?
|
||||
</p>
|
||||
</section>
|
||||
<div className="flex justify-between gap-8">
|
||||
<ActionButton
|
||||
Element="button"
|
||||
iconStart={{
|
||||
icon: 'close',
|
||||
}}
|
||||
data-negative-button="dismiss"
|
||||
name="dismiss"
|
||||
onClick={onDismissOnboardingInvite}
|
||||
>
|
||||
Not right now
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
Element="button"
|
||||
iconStart={{
|
||||
icon: 'checkmark',
|
||||
}}
|
||||
name="accept"
|
||||
onClick={onAccept}
|
||||
>
|
||||
Get started
|
||||
</ActionButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to catch the `ERROR_MUST_WARN` error from
|
||||
* `acceptOnboarding` and show a warning toast.
|
||||
*/
|
||||
export async function catchOnboardingWarnError(
|
||||
err: Error,
|
||||
props: OnboardingUtilDeps
|
||||
) {
|
||||
if (err instanceof Error && err.message === ERROR_MUST_WARN) {
|
||||
toast.success(TutorialWebConfirmationToast(props), {
|
||||
id: ONBOARDING_TOAST_ID,
|
||||
duration: Number.POSITIVE_INFINITY,
|
||||
icon: null,
|
||||
})
|
||||
} else {
|
||||
toast.dismiss(ONBOARDING_TOAST_ID)
|
||||
return reportRejection(err)
|
||||
}
|
||||
}
|
||||
|
||||
export function TutorialWebConfirmationToast(props: OnboardingUtilDeps) {
|
||||
function onAccept() {
|
||||
toast.dismiss(ONBOARDING_TOAST_ID)
|
||||
resetCodeAndAdvanceOnboarding(props).catch(reportRejection)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
data-testid="onboarding-toast-confirmation"
|
||||
className="flex items-center gap-6 min-w-80"
|
||||
>
|
||||
<Logo className="w-auto h-8 flex-none" />
|
||||
<div className="flex flex-col justify-between gap-6">
|
||||
<section>
|
||||
<h2>The welcome tutorial resets your code in the browser</h2>
|
||||
<p className="text-sm text-chalkboard-70 dark:text-chalkboard-30">
|
||||
We see you have some of your own code written in this project.
|
||||
Please save it somewhere else before continuing the onboarding.
|
||||
</p>
|
||||
</section>
|
||||
<div className="flex justify-between gap-8">
|
||||
<ActionButton
|
||||
Element="button"
|
||||
iconStart={{
|
||||
icon: 'close',
|
||||
}}
|
||||
data-negative-button="dismiss"
|
||||
name="dismiss"
|
||||
onClick={onDismissOnboardingInvite}
|
||||
>
|
||||
I'll save it
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
Element="button"
|
||||
iconStart={{
|
||||
icon: 'checkmark',
|
||||
}}
|
||||
name="accept"
|
||||
onClick={onAccept}
|
||||
>
|
||||
Overwrite and begin
|
||||
</ActionButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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'
|
||||
|
||||
|
Reference in New Issue
Block a user