diff --git a/e2e/playwright/file-tree.spec.ts b/e2e/playwright/file-tree.spec.ts
index b9b75756a9..8fbdb0a156 100644
--- a/e2e/playwright/file-tree.spec.ts
+++ b/e2e/playwright/file-tree.spec.ts
@@ -3,6 +3,7 @@ import { test, expect } from './fixtures/fixtureSetup'
import * as fsp from 'fs/promises'
import * as fs from 'fs'
import {
+ createProject,
executorInputPath,
getUtils,
setup,
@@ -114,20 +115,15 @@ test.describe('when using the file tree to', () => {
async ({ browser: _, tronApp }, testInfo) => {
await tronApp.initialise()
- const {
- panesOpen,
- createAndSelectProject,
- pasteCodeInEditor,
- renameFile,
- editorTextMatches,
- } = await getUtils(tronApp.page, test)
+ const { panesOpen, pasteCodeInEditor, renameFile, editorTextMatches } =
+ await getUtils(tronApp.page, test)
await tronApp.page.setViewportSize({ width: 1200, height: 500 })
tronApp.page.on('console', console.log)
await panesOpen(['files', 'code'])
- await createAndSelectProject('project-000')
+ await createProject({ name: 'project-000', page: tronApp.page })
// File the main.kcl with contents
const kclCube = await fsp.readFile(
@@ -167,15 +163,14 @@ test.describe('when using the file tree to', () => {
async ({ browser: _, tronApp }, testInfo) => {
await tronApp.initialise()
- const { panesOpen, createAndSelectProject, createNewFile } =
- await getUtils(tronApp.page, test)
+ const { panesOpen, createNewFile } = await getUtils(tronApp.page, test)
await tronApp.page.setViewportSize({ width: 1200, height: 500 })
tronApp.page.on('console', console.log)
await panesOpen(['files'])
- await createAndSelectProject('project-000')
+ await createProject({ name: 'project-000', page: tronApp.page })
await createNewFile('')
await createNewFile('')
@@ -204,7 +199,6 @@ test.describe('when using the file tree to', () => {
const {
openKclCodePanel,
openFilePanel,
- createAndSelectProject,
pasteCodeInEditor,
createNewFileAndSelect,
renameFile,
@@ -215,7 +209,7 @@ test.describe('when using the file tree to', () => {
await tronApp.page.setViewportSize({ width: 1200, height: 500 })
tronApp.page.on('console', console.log)
- await createAndSelectProject('project-000')
+ await createProject({ name: 'project-000', page: tronApp.page })
await openKclCodePanel()
await openFilePanel()
// File the main.kcl with contents
@@ -263,20 +257,15 @@ test.describe('when using the file tree to', () => {
async ({ browser: _, tronApp }, testInfo) => {
await tronApp.initialise()
- const {
- panesOpen,
- createAndSelectProject,
- pasteCodeInEditor,
- deleteFile,
- editorTextMatches,
- } = await getUtils(tronApp.page, _test)
+ const { panesOpen, pasteCodeInEditor, deleteFile, editorTextMatches } =
+ await getUtils(tronApp.page, _test)
await tronApp.page.setViewportSize({ width: 1200, height: 500 })
tronApp.page.on('console', console.log)
await panesOpen(['files', 'code'])
- await createAndSelectProject('project-000')
+ await createProject({ name: 'project-000', page: tronApp.page })
// File the main.kcl with contents
const kclCube = await fsp.readFile(
'src/wasm-lib/tests/executor/inputs/cube.kcl',
@@ -306,7 +295,6 @@ test.describe('when using the file tree to', () => {
const {
panesOpen,
- createAndSelectProject,
pasteCodeInEditor,
createNewFile,
openDebugPanel,
@@ -318,7 +306,7 @@ test.describe('when using the file tree to', () => {
tronApp.page.on('console', console.log)
await panesOpen(['files', 'code'])
- await createAndSelectProject('project-000')
+ await createProject({ name: 'project-000', page: tronApp.page })
// Create a small file
const kclCube = await fsp.readFile(
@@ -722,7 +710,7 @@ _test.describe('Renaming in the file tree', () => {
})
await _test.step('Rename the folder', async () => {
- await page.waitForTimeout(60000)
+ await page.waitForTimeout(1000)
await folderToRename.click({ button: 'right' })
await _expect(renameMenuItem).toBeVisible()
await renameMenuItem.click()
diff --git a/e2e/playwright/onboarding-tests.spec.ts b/e2e/playwright/onboarding-tests.spec.ts
index 84288213bd..49a4dc3d09 100644
--- a/e2e/playwright/onboarding-tests.spec.ts
+++ b/e2e/playwright/onboarding-tests.spec.ts
@@ -7,6 +7,7 @@ import {
setupElectron,
tearDown,
executorInputPath,
+ createProject,
} from './test-utils'
import { bracket } from 'lib/exampleKcl'
import { onboardingPaths } from 'routes/Onboarding/paths'
@@ -74,13 +75,8 @@ test.describe('Onboarding tests', () => {
const viewportSize = { width: 1200, height: 500 }
await page.setViewportSize(viewportSize)
- // Locators and constants
- const newProjectButton = page.getByRole('button', { name: 'New project' })
- const projectLink = page.getByTestId('project-link')
-
await test.step(`Create a project and open to the onboarding`, async () => {
- await newProjectButton.click()
- await projectLink.click()
+ await createProject({ name: 'project-link', page })
await test.step(`Ensure the engine connection works by testing the sketch button`, async () => {
await u.waitForPageLoad()
})
diff --git a/e2e/playwright/projects.spec.ts b/e2e/playwright/projects.spec.ts
index a27d26faf8..fc1a3c73ba 100644
--- a/e2e/playwright/projects.spec.ts
+++ b/e2e/playwright/projects.spec.ts
@@ -7,7 +7,7 @@ import {
Paths,
setupElectron,
tearDown,
- createProjectAndRenameIt,
+ createProject,
} from './test-utils'
import fsp from 'fs/promises'
import fs from 'fs'
@@ -503,6 +503,245 @@ test(
}
)
+test.describe(`Project management commands`, () => {
+ test(
+ `Rename from project page`,
+ { tag: '@electron' },
+ async ({ browserName }, testInfo) => {
+ const projectName = `my_project_to_rename`
+ const { electronApp, page } = await setupElectron({
+ testInfo,
+ folderSetupFn: async (dir) => {
+ await fsp.mkdir(`${dir}/${projectName}`, { recursive: true })
+ await fsp.copyFile(
+ 'src/wasm-lib/tests/executor/inputs/router-template-slate.kcl',
+ `${dir}/${projectName}/main.kcl`
+ )
+ },
+ })
+ const u = await getUtils(page)
+
+ // Constants and locators
+ const projectHomeLink = page.getByTestId('project-link')
+ const commandButton = page.getByRole('button', { name: 'Commands' })
+ const commandOption = page.getByRole('option', { name: 'rename project' })
+ const projectNameOption = page.getByRole('option', { name: projectName })
+ const projectRenamedName = `project-000`
+ // const projectMenuButton = page.getByTestId('project-sidebar-toggle')
+ const commandContinueButton = page.getByRole('button', {
+ name: 'Continue',
+ })
+ const commandSubmitButton = page.getByRole('button', {
+ name: 'Submit command',
+ })
+ const toastMessage = page.getByText(`Successfully renamed`)
+
+ await test.step(`Setup`, async () => {
+ await page.setViewportSize({ width: 1200, height: 500 })
+ page.on('console', console.log)
+
+ await projectHomeLink.click()
+ await u.waitForPageLoad()
+ })
+
+ await test.step(`Run rename command via command palette`, async () => {
+ await commandButton.click()
+ await commandOption.click()
+ await projectNameOption.click()
+
+ await expect(commandContinueButton).toBeVisible()
+ await commandContinueButton.click()
+
+ await expect(commandSubmitButton).toBeVisible()
+ await commandSubmitButton.click()
+
+ await expect(toastMessage).toBeVisible()
+ })
+
+ // TODO: in future I'd like the behavior to be to
+ // navigate to the new project's page directly,
+ // see ProjectContextProvider.tsx:158
+ await test.step(`Check the project was renamed and we navigated home`, async () => {
+ await expect(projectHomeLink.first()).toBeVisible()
+ await expect(projectHomeLink.first()).toContainText(projectRenamedName)
+ })
+
+ await electronApp.close()
+ }
+ )
+
+ test(
+ `Delete from project page`,
+ { tag: '@electron' },
+ async ({ browserName: _ }, testInfo) => {
+ const projectName = `my_project_to_delete`
+ const { electronApp, page } = await setupElectron({
+ testInfo,
+ folderSetupFn: async (dir) => {
+ await fsp.mkdir(`${dir}/${projectName}`, { recursive: true })
+ await fsp.copyFile(
+ 'src/wasm-lib/tests/executor/inputs/router-template-slate.kcl',
+ `${dir}/${projectName}/main.kcl`
+ )
+ },
+ })
+ const u = await getUtils(page)
+
+ // Constants and locators
+ const projectHomeLink = page.getByTestId('project-link')
+ const commandButton = page.getByRole('button', { name: 'Commands' })
+ const commandOption = page.getByRole('option', { name: 'delete project' })
+ const projectNameOption = page.getByRole('option', { name: projectName })
+ const commandWarning = page.getByText('Are you sure you want to delete?')
+ const commandSubmitButton = page.getByRole('button', {
+ name: 'Submit command',
+ })
+ const toastMessage = page.getByText(`Successfully deleted`)
+ const noProjectsMessage = page.getByText('No Projects found')
+
+ await test.step(`Setup`, async () => {
+ await page.setViewportSize({ width: 1200, height: 500 })
+ page.on('console', console.log)
+
+ await projectHomeLink.click()
+ await u.waitForPageLoad()
+ })
+
+ await test.step(`Run delete command via command palette`, async () => {
+ await commandButton.click()
+ await commandOption.click()
+ await projectNameOption.click()
+
+ await expect(commandWarning).toBeVisible()
+ await expect(commandSubmitButton).toBeVisible()
+ await commandSubmitButton.click()
+
+ await expect(toastMessage).toBeVisible()
+ })
+
+ await test.step(`Check the project was deleted and we navigated home`, async () => {
+ await expect(noProjectsMessage).toBeVisible()
+ })
+
+ await electronApp.close()
+ }
+ )
+ test(
+ `Rename from home page`,
+ { tag: '@electron' },
+ async ({ browserName: _ }, testInfo) => {
+ const projectName = `my_project_to_rename`
+ const { electronApp, page } = await setupElectron({
+ testInfo,
+ folderSetupFn: async (dir) => {
+ await fsp.mkdir(`${dir}/${projectName}`, { recursive: true })
+ await fsp.copyFile(
+ 'src/wasm-lib/tests/executor/inputs/router-template-slate.kcl',
+ `${dir}/${projectName}/main.kcl`
+ )
+ },
+ })
+
+ // Constants and locators
+ const projectHomeLink = page.getByTestId('project-link')
+ const commandButton = page.getByRole('button', { name: 'Commands' })
+ const commandOption = page.getByRole('option', { name: 'rename project' })
+ const projectNameOption = page.getByRole('option', { name: projectName })
+ const projectRenamedName = `project-000`
+ const commandContinueButton = page.getByRole('button', {
+ name: 'Continue',
+ })
+ const commandSubmitButton = page.getByRole('button', {
+ name: 'Submit command',
+ })
+ const toastMessage = page.getByText(`Successfully renamed`)
+
+ await test.step(`Setup`, async () => {
+ await page.setViewportSize({ width: 1200, height: 500 })
+ page.on('console', console.log)
+ await expect(projectHomeLink).toBeVisible()
+ })
+
+ await test.step(`Run rename command via command palette`, async () => {
+ await commandButton.click()
+ await commandOption.click()
+ await projectNameOption.click()
+
+ await expect(commandContinueButton).toBeVisible()
+ await commandContinueButton.click()
+
+ await expect(commandSubmitButton).toBeVisible()
+ await commandSubmitButton.click()
+
+ await expect(toastMessage).toBeVisible()
+ })
+
+ await test.step(`Check the project was renamed`, async () => {
+ await expect(
+ page.getByRole('link', { name: projectRenamedName })
+ ).toBeVisible()
+ await expect(projectHomeLink).not.toHaveText(projectName)
+ })
+
+ await electronApp.close()
+ }
+ )
+ test(
+ `Delete from home page`,
+ { tag: '@electron' },
+ async ({ browserName: _ }, testInfo) => {
+ const projectName = `my_project_to_delete`
+ const { electronApp, page } = await setupElectron({
+ testInfo,
+ folderSetupFn: async (dir) => {
+ await fsp.mkdir(`${dir}/${projectName}`, { recursive: true })
+ await fsp.copyFile(
+ 'src/wasm-lib/tests/executor/inputs/router-template-slate.kcl',
+ `${dir}/${projectName}/main.kcl`
+ )
+ },
+ })
+
+ // Constants and locators
+ const projectHomeLink = page.getByTestId('project-link')
+ const commandButton = page.getByRole('button', { name: 'Commands' })
+ const commandOption = page.getByRole('option', { name: 'delete project' })
+ const projectNameOption = page.getByRole('option', { name: projectName })
+ const commandWarning = page.getByText('Are you sure you want to delete?')
+ const commandSubmitButton = page.getByRole('button', {
+ name: 'Submit command',
+ })
+ const toastMessage = page.getByText(`Successfully deleted`)
+ const noProjectsMessage = page.getByText('No Projects found')
+
+ await test.step(`Setup`, async () => {
+ await page.setViewportSize({ width: 1200, height: 500 })
+ page.on('console', console.log)
+ await expect(projectHomeLink).toBeVisible()
+ })
+
+ await test.step(`Run delete command via command palette`, async () => {
+ await commandButton.click()
+ await commandOption.click()
+ await projectNameOption.click()
+
+ await expect(commandWarning).toBeVisible()
+ await expect(commandSubmitButton).toBeVisible()
+ await commandSubmitButton.click()
+
+ await expect(toastMessage).toBeVisible()
+ })
+
+ await test.step(`Check the project was deleted`, async () => {
+ await expect(projectHomeLink).not.toBeVisible()
+ await expect(noProjectsMessage).toBeVisible()
+ })
+
+ await electronApp.close()
+ }
+ )
+})
+
test(
'File in the file pane should open with a single click',
{ tag: '@electron' },
@@ -643,7 +882,7 @@ test(
page.on('console', console.log)
await test.step('delete the middle project, i.e. the bracket project', async () => {
- const project = page.getByText('bracket')
+ const project = page.getByTestId('project-link').getByText('bracket')
await project.hover()
await project.focus()
@@ -687,10 +926,10 @@ test(
})
await test.step('Check we can still create a project', async () => {
- await page.getByRole('button', { name: 'New project' }).click()
- await expect(page.getByText('Successfully created')).toBeVisible()
- await expect(page.getByText('Successfully created')).not.toBeVisible()
- await expect(page.getByText('project-000')).toBeVisible()
+ await createProject({ name: 'project-000', page, returnHome: true })
+ await expect(
+ page.getByTestId('project-link').filter({ hasText: 'project-000' })
+ ).toBeVisible()
})
await electronApp.close()
@@ -867,17 +1106,16 @@ test.fixme(
const pointOnModel = { x: 660, y: 250 }
const expectedStartCamZPosition = 15633.47
+ // Constants and locators
+ const projectLinks = page.getByTestId('project-link')
+
// expect to see text "No Projects found"
await expect(page.getByText('No Projects found')).toBeVisible()
- await page.getByRole('button', { name: 'New project' }).click()
-
- await expect(page.getByText('Successfully created')).toBeVisible()
- await expect(page.getByText('Successfully created')).not.toBeVisible()
+ await createProject({ name: 'project-000', page, returnHome: true })
+ await expect(projectLinks.getByText('project-000')).toBeVisible()
- await expect(page.getByText('project-000')).toBeVisible()
-
- await page.getByText('project-000').click()
+ await projectLinks.getByText('project-000').click()
await u.waitForPageLoad()
@@ -936,16 +1174,10 @@ extrude001 = extrude(200, sketch001)`)
page.getByRole('button', { name: 'New project' })
).toBeVisible()
- const createProject = async (projectNum: number) => {
- await page.getByRole('button', { name: 'New project' }).click()
- await expect(page.getByText('Successfully created')).toBeVisible()
- await expect(page.getByText('Successfully created')).not.toBeVisible()
-
- const projectNumStr = projectNum.toString().padStart(3, '0')
- await expect(page.getByText(`project-${projectNumStr}`)).toBeVisible()
- }
for (let i = 1; i <= 10; i++) {
- await createProject(i)
+ const name = `project-${i.toString().padStart(3, '0')}`
+ await createProject({ name, page, returnHome: true })
+ await expect(projectLinks.getByText(name)).toBeVisible()
}
await electronApp.close()
}
@@ -1120,11 +1352,10 @@ test(
await page.getByTestId('settings-close-button').click()
await expect(page.getByText('No Projects found')).toBeVisible()
- await page.getByRole('button', { name: 'New project' }).click()
- await expect(page.getByText('Successfully created')).toBeVisible()
- await expect(page.getByText('Successfully created')).not.toBeVisible()
-
- await expect(page.getByText(`project-000`)).toBeVisible()
+ await createProject({ name: 'project-000', page, returnHome: true })
+ await expect(
+ page.getByTestId('project-link').filter({ hasText: 'project-000' })
+ ).toBeVisible()
})
await test.step('We can change back to the original root project directory', async () => {
@@ -1450,7 +1681,7 @@ test(
page.on('console', console.log)
await test.step('Should create and name a project called wrist brace', async () => {
- await createProjectAndRenameIt({ name: 'wrist brace', page })
+ await createProject({ name: 'wrist brace', page, returnHome: true })
})
await test.step('Should go through onboarding', async () => {
diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Inch-scale-1-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Inch-scale-1-Google-Chrome-linux.png
index fb5dd4aac5..78e1c50726 100644
Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Inch-scale-1-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Inch-scale-1-Google-Chrome-linux.png differ
diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Inch-scale-1-Google-Chrome-win32.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Inch-scale-1-Google-Chrome-win32.png
index fd6d67c8b3..5ff8e179df 100644
Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Inch-scale-1-Google-Chrome-win32.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Inch-scale-1-Google-Chrome-win32.png differ
diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Inch-scale-2-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Inch-scale-2-Google-Chrome-linux.png
index 2b075f3000..fe1a02df5c 100644
Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Inch-scale-2-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Inch-scale-2-Google-Chrome-linux.png differ
diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Inch-scale-2-Google-Chrome-win32.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Inch-scale-2-Google-Chrome-win32.png
index 7753b12fc3..7e774fc588 100644
Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Inch-scale-2-Google-Chrome-win32.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Inch-scale-2-Google-Chrome-win32.png differ
diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Millimeter-scale-2-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Millimeter-scale-2-Google-Chrome-linux.png
index 5b8396b93b..1ea5160b96 100644
Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Millimeter-scale-2-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Millimeter-scale-2-Google-Chrome-linux.png differ
diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-circle-should-look-right-1-Google-Chrome-win32.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-circle-should-look-right-1-Google-Chrome-win32.png
index d302db830e..1a21406c02 100644
Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-circle-should-look-right-1-Google-Chrome-win32.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Draft-circle-should-look-right-1-Google-Chrome-win32.png differ
diff --git a/e2e/playwright/test-utils.ts b/e2e/playwright/test-utils.ts
index c739f88fb5..e74f4c1924 100644
--- a/e2e/playwright/test-utils.ts
+++ b/e2e/playwright/test-utils.ts
@@ -467,20 +467,6 @@ export async function getUtils(page: Page, test_?: typeof test) {
return text.replace(/\s+/g, '')
},
- createAndSelectProject: async (hasText: string) => {
- return test_?.step(
- `Create and select project with text "${hasText}"`,
- async () => {
- // Without this, we get unreliable project creation. It's probably
- // due to a race between the FS being read and clicking doing something.
- await page.waitForTimeout(100)
- await page.getByTestId('home-new-file').click()
- const projectLinksPost = page.getByTestId('project-link')
- await projectLinksPost.filter({ hasText }).click()
- }
- )
- },
-
editorTextMatches: async (code: string) => {
const editor = page.locator(editorSelector)
return expect(editor).toHaveText(code, { useInnerText: true })
@@ -980,30 +966,25 @@ export async function isOutOfViewInScrollContainer(
return isOutOfView
}
-export async function createProjectAndRenameIt({
+export async function createProject({
name,
page,
+ returnHome = false,
}: {
name: string
page: Page
+ returnHome?: boolean
}) {
- await page.getByRole('button', { name: 'New project' }).click()
- await expect(page.getByText('Successfully created')).toBeVisible()
- await expect(page.getByText('Successfully created')).not.toBeVisible()
-
- await expect(page.getByText(`project-000`)).toBeVisible()
- await page.getByText(`project-000`).hover()
- await page.getByText(`project-000`).focus()
-
- await page.getByLabel('sketch').first().click()
-
- await page.waitForTimeout(100)
-
- // type the name passed in
- await page.keyboard.press('Backspace')
- await page.keyboard.type(name)
-
- await page.getByLabel('checkmark').last().click()
+ await test.step(`Create project and navigate to it`, async () => {
+ await page.getByRole('button', { name: 'New project' }).click()
+ await page.getByRole('textbox', { name: 'Name' }).fill(name)
+ await page.getByRole('button', { name: 'Continue' }).click()
+
+ if (returnHome) {
+ await page.waitForURL('**/file/**', { waitUntil: 'domcontentloaded' })
+ await page.getByTestId('app-logo').click()
+ }
+ })
}
export function executorInputPath(fileName: string): string {
diff --git a/e2e/playwright/testing-settings.spec.ts b/e2e/playwright/testing-settings.spec.ts
index c2675948c7..0ddd16fbe8 100644
--- a/e2e/playwright/testing-settings.spec.ts
+++ b/e2e/playwright/testing-settings.spec.ts
@@ -7,6 +7,7 @@ import {
setupElectron,
tearDown,
executorInputPath,
+ createProject,
} from './test-utils'
import { SaveSettingsPayload, SettingsLevel } from 'lib/settings/settingsTypes'
import { SETTINGS_FILE_NAME, PROJECT_SETTINGS_FILE_NAME } from 'lib/constants'
@@ -428,8 +429,7 @@ test.describe('Testing settings', () => {
})
await test.step('Check color of logo changed when in modeling view', async () => {
- await page.getByRole('button', { name: 'New project' }).click()
- await page.getByTestId('project-link').first().click()
+ await createProject({ name: 'project-000', page })
await changeColor('58')
await expect(logoLink).toHaveCSS('--primary-hue', '58')
})
@@ -447,7 +447,7 @@ test.describe('Testing settings', () => {
test(
'project settings reload on external change',
{ tag: '@electron' },
- async ({ browserName }, testInfo) => {
+ async ({ browserName: _ }, testInfo) => {
const {
electronApp,
page,
@@ -465,11 +465,7 @@ test.describe('Testing settings', () => {
await expect(projectDirLink).toBeVisible()
})
- const projectLinks = page.getByTestId('project-link')
- const oldCount = await projectLinks.count()
- await page.getByRole('button', { name: 'New project' }).click()
- await expect(projectLinks).toHaveCount(oldCount + 1)
- await projectLinks.filter({ hasText: 'project-000' }).first().click()
+ await createProject({ name: 'project-000', page })
const changeColorFs = async (color: string) => {
const tempSettingsFilePath = join(
diff --git a/e2e/playwright/text-to-cad-tests.spec.ts b/e2e/playwright/text-to-cad-tests.spec.ts
index af14f7cd9a..c4808234e5 100644
--- a/e2e/playwright/text-to-cad-tests.spec.ts
+++ b/e2e/playwright/text-to-cad-tests.spec.ts
@@ -1,5 +1,11 @@
import { test, expect, Page } from '@playwright/test'
-import { getUtils, setup, tearDown, setupElectron } from './test-utils'
+import {
+ getUtils,
+ setup,
+ tearDown,
+ setupElectron,
+ createProject,
+} from './test-utils'
import { join } from 'path'
import fs from 'fs'
@@ -700,12 +706,10 @@ test(
const fileExists = () =>
fs.existsSync(join(dir, projectName, textToCadFileName))
- const {
- createAndSelectProject,
- openFilePanel,
- openKclCodePanel,
- waitForPageLoad,
- } = await getUtils(page, test)
+ const { openFilePanel, openKclCodePanel, waitForPageLoad } = await getUtils(
+ page,
+ test
+ )
await page.setViewportSize({ width: 1200, height: 500 })
@@ -719,7 +723,7 @@ test(
)
// Create and navigate to the project
- await createAndSelectProject('project-000')
+ await createProject({ name: 'project-000', page })
// Wait for Start Sketch otherwise you will not have access Text-to-CAD command
await waitForPageLoad()
diff --git a/src/Router.tsx b/src/Router.tsx
index 665e1bda6b..d135cfc76b 100644
--- a/src/Router.tsx
+++ b/src/Router.tsx
@@ -43,6 +43,7 @@ import { coreDump } from 'lang/wasm'
import { useMemo } from 'react'
import { AppStateProvider } from 'AppState'
import { reportRejection } from 'lib/trap'
+import { ProjectsContextProvider } from 'components/ProjectsContextProvider'
const createRouter = isDesktop() ? createHashRouter : createBrowserRouter
@@ -57,13 +58,15 @@ const router = createRouter([
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
diff --git a/src/components/ProjectsContextProvider.tsx b/src/components/ProjectsContextProvider.tsx
new file mode 100644
index 0000000000..21f672f88d
--- /dev/null
+++ b/src/components/ProjectsContextProvider.tsx
@@ -0,0 +1,289 @@
+import { useMachine } from '@xstate/react'
+import { useCommandsContext } from 'hooks/useCommandsContext'
+import { useFileSystemWatcher } from 'hooks/useFileSystemWatcher'
+import { useProjectsLoader } from 'hooks/useProjectsLoader'
+import { projectsMachine } from 'machines/projectsMachine'
+import { createContext, useEffect, useState } from 'react'
+import { Actor, AnyStateMachine, fromPromise, Prop, StateFrom } from 'xstate'
+import { useLspContext } from './LspProvider'
+import toast from 'react-hot-toast'
+import { useLocation, useNavigate } from 'react-router-dom'
+import { PATHS } from 'lib/paths'
+import {
+ createNewProjectDirectory,
+ listProjects,
+ renameProjectDirectory,
+} from 'lib/desktop'
+import {
+ getNextProjectIndex,
+ interpolateProjectNameWithIndex,
+ doesProjectNameNeedInterpolated,
+} from 'lib/desktopFS'
+import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
+import useStateMachineCommands from 'hooks/useStateMachineCommands'
+import { projectsCommandBarConfig } from 'lib/commandBarConfigs/projectsCommandConfig'
+import { isDesktop } from 'lib/isDesktop'
+
+type MachineContext = {
+ state?: StateFrom
+ send: Prop, 'send'>
+}
+
+export const ProjectsMachineContext = createContext(
+ {} as MachineContext
+)
+
+/**
+ * Watches the project directory and provides project management-related commands,
+ * like "Create project", "Open project", "Delete project", etc.
+ *
+ * If in the future we implement full-fledge project management in the web version,
+ * we can unify these components but for now, we need this to be only for the desktop version.
+ */
+export const ProjectsContextProvider = ({
+ children,
+}: {
+ children: React.ReactNode
+}) => {
+ return isDesktop() ? (
+ {children}
+ ) : (
+ {children}
+ )
+}
+
+const ProjectsContextWeb = ({ children }: { children: React.ReactNode }) => {
+ return (
+ {},
+ }}
+ >
+ {children}
+
+ )
+}
+
+const ProjectsContextDesktop = ({
+ children,
+}: {
+ children: React.ReactNode
+}) => {
+ const navigate = useNavigate()
+ const location = useLocation()
+ const { commandBarSend } = useCommandsContext()
+ const { onProjectOpen } = useLspContext()
+ const {
+ settings: { context: settings },
+ } = useSettingsAuthContext()
+
+ useEffect(() => {
+ console.log(
+ 'project directory changed',
+ settings.app.projectDirectory.current
+ )
+ }, [settings.app.projectDirectory.current])
+
+ const [projectsLoaderTrigger, setProjectsLoaderTrigger] = useState(0)
+ const { projectPaths, projectsDir } = useProjectsLoader([
+ projectsLoaderTrigger,
+ ])
+
+ // Re-read projects listing if the projectDir has any updates.
+ useFileSystemWatcher(
+ async () => {
+ return setProjectsLoaderTrigger(projectsLoaderTrigger + 1)
+ },
+ projectsDir ? [projectsDir] : []
+ )
+
+ const [state, send, actor] = useMachine(
+ projectsMachine.provide({
+ actions: {
+ navigateToProject: ({ context, event }) => {
+ const nameFromEventData =
+ 'data' in event &&
+ event.data &&
+ 'name' in event.data &&
+ event.data.name
+ const nameFromOutputData =
+ 'output' in event &&
+ event.output &&
+ 'name' in event.output &&
+ event.output.name
+
+ const name = nameFromEventData || nameFromOutputData
+
+ if (name) {
+ let projectPath =
+ context.defaultDirectory + window.electron.path.sep + name
+ onProjectOpen(
+ {
+ name,
+ path: projectPath,
+ },
+ null
+ )
+ commandBarSend({ type: 'Close' })
+ const newPathName = `${PATHS.FILE}/${encodeURIComponent(
+ projectPath
+ )}`
+ navigate(newPathName)
+ }
+ },
+ navigateToProjectIfNeeded: ({ event }) => {
+ if (
+ event.type.startsWith('xstate.done.actor.') &&
+ 'output' in event
+ ) {
+ const isInAProject = location.pathname.startsWith(PATHS.FILE)
+ const isInDeletedProject =
+ event.type === 'xstate.done.actor.delete-project' &&
+ isInAProject &&
+ decodeURIComponent(location.pathname).includes(event.output.name)
+ if (isInDeletedProject) {
+ navigate(PATHS.HOME)
+ return
+ }
+
+ const isInRenamedProject =
+ event.type === 'xstate.done.actor.rename-project' &&
+ isInAProject &&
+ decodeURIComponent(location.pathname).includes(
+ event.output.oldName
+ )
+
+ if (isInRenamedProject) {
+ // TODO: In future, we can navigate to the new project path
+ // directly, but we need to coordinate with
+ // @lf94's useFileSystemWatcher in SettingsAuthProvider.tsx:224
+ // Because it's beating us to the punch and updating the route
+ // const newPathName = location.pathname.replace(
+ // encodeURIComponent(event.output.oldName),
+ // encodeURIComponent(event.output.newName)
+ // )
+ // navigate(newPathName)
+ return
+ }
+ }
+ },
+ toastSuccess: ({ event }) =>
+ toast.success(
+ ('data' in event && typeof event.data === 'string' && event.data) ||
+ ('output' in event &&
+ 'message' in event.output &&
+ typeof event.output.message === 'string' &&
+ event.output.message) ||
+ ''
+ ),
+ toastError: ({ event }) =>
+ toast.error(
+ ('data' in event && typeof event.data === 'string' && event.data) ||
+ ('output' in event &&
+ typeof event.output === 'string' &&
+ event.output) ||
+ ''
+ ),
+ },
+ actors: {
+ readProjects: fromPromise(() => listProjects()),
+ createProject: fromPromise(async ({ input }) => {
+ let name = (
+ input && 'name' in input && input.name
+ ? input.name
+ : settings.projects.defaultProjectName.current
+ ).trim()
+
+ if (doesProjectNameNeedInterpolated(name)) {
+ const nextIndex = getNextProjectIndex(name, input.projects)
+ name = interpolateProjectNameWithIndex(name, nextIndex)
+ }
+
+ await createNewProjectDirectory(name)
+
+ return {
+ message: `Successfully created "${name}"`,
+ name,
+ }
+ }),
+ renameProject: fromPromise(async ({ input }) => {
+ const {
+ oldName,
+ newName,
+ defaultProjectName,
+ defaultDirectory,
+ projects,
+ } = input
+ let name = newName ? newName : defaultProjectName
+ if (doesProjectNameNeedInterpolated(name)) {
+ const nextIndex = getNextProjectIndex(name, projects)
+ name = interpolateProjectNameWithIndex(name, nextIndex)
+ }
+
+ console.log('from Project')
+
+ await renameProjectDirectory(
+ window.electron.path.join(defaultDirectory, oldName),
+ name
+ )
+ return {
+ message: `Successfully renamed "${oldName}" to "${name}"`,
+ oldName: oldName,
+ newName: name,
+ }
+ }),
+ deleteProject: fromPromise(async ({ input }) => {
+ await window.electron.rm(
+ window.electron.path.join(input.defaultDirectory, input.name),
+ {
+ recursive: true,
+ }
+ )
+ return {
+ message: `Successfully deleted "${input.name}"`,
+ name: input.name,
+ }
+ }),
+ },
+ guards: {
+ 'Has at least 1 project': ({ event }) => {
+ if (event.type !== 'xstate.done.actor.read-projects') return false
+ console.log(`from has at least 1 project: ${event.output.length}`)
+ return event.output.length ? event.output.length >= 1 : false
+ },
+ },
+ }),
+ {
+ input: {
+ projects: projectPaths,
+ defaultProjectName: settings.projects.defaultProjectName.current,
+ defaultDirectory: settings.app.projectDirectory.current,
+ },
+ }
+ )
+
+ useEffect(() => {
+ send({ type: 'Read projects', data: {} })
+ }, [projectPaths])
+
+ // register all project-related command palette commands
+ useStateMachineCommands({
+ machineId: 'projects',
+ send,
+ state,
+ commandBarConfig: projectsCommandBarConfig,
+ actor,
+ })
+
+ return (
+
+ {children}
+
+ )
+}
diff --git a/src/hooks/useProjectsContext.ts b/src/hooks/useProjectsContext.ts
new file mode 100644
index 0000000000..2cc3551beb
--- /dev/null
+++ b/src/hooks/useProjectsContext.ts
@@ -0,0 +1,6 @@
+import { ProjectsMachineContext } from 'components/ProjectsContextProvider'
+import { useContext } from 'react'
+
+export const useProjectsContext = () => {
+ return useContext(ProjectsMachineContext)
+}
diff --git a/src/hooks/useStateMachineCommands.ts b/src/hooks/useStateMachineCommands.ts
index 14adeb640a..927def5bee 100644
--- a/src/hooks/useStateMachineCommands.ts
+++ b/src/hooks/useStateMachineCommands.ts
@@ -5,7 +5,7 @@ import { useCommandsContext } from './useCommandsContext'
import { modelingMachine } from 'machines/modelingMachine'
import { authMachine } from 'machines/authMachine'
import { settingsMachine } from 'machines/settingsMachine'
-import { homeMachine } from 'machines/homeMachine'
+import { projectsMachine } from 'machines/projectsMachine'
import {
Command,
StateMachineCommandSetConfig,
@@ -22,7 +22,7 @@ export type AllMachines =
| typeof modelingMachine
| typeof settingsMachine
| typeof authMachine
- | typeof homeMachine
+ | typeof projectsMachine
interface UseStateMachineCommandsArgs<
T extends AllMachines,
diff --git a/src/lib/commandBarConfigs/homeCommandConfig.ts b/src/lib/commandBarConfigs/projectsCommandConfig.ts
similarity index 76%
rename from src/lib/commandBarConfigs/homeCommandConfig.ts
rename to src/lib/commandBarConfigs/projectsCommandConfig.ts
index 4ca29bbdcd..deec0a8bdd 100644
--- a/src/lib/commandBarConfigs/homeCommandConfig.ts
+++ b/src/lib/commandBarConfigs/projectsCommandConfig.ts
@@ -1,7 +1,8 @@
+import { CommandBarOverwriteWarning } from 'components/CommandBarOverwriteWarning'
import { StateMachineCommandSetConfig } from 'lib/commandTypes'
-import { homeMachine } from 'machines/homeMachine'
+import { projectsMachine } from 'machines/projectsMachine'
-export type HomeCommandSchema = {
+export type ProjectsCommandSchema = {
'Read projects': {}
'Create project': {
name: string
@@ -18,9 +19,9 @@ export type HomeCommandSchema = {
}
}
-export const homeCommandBarConfig: StateMachineCommandSetConfig<
- typeof homeMachine,
- HomeCommandSchema
+export const projectsCommandBarConfig: StateMachineCommandSetConfig<
+ typeof projectsMachine,
+ ProjectsCommandSchema
> = {
'Open project': {
icon: 'arrowRight',
@@ -53,6 +54,11 @@ export const homeCommandBarConfig: StateMachineCommandSetConfig<
icon: 'close',
description: 'Delete a project',
needsReview: true,
+ reviewMessage: ({ argumentsToSubmit }) =>
+ CommandBarOverwriteWarning({
+ heading: 'Are you sure you want to delete?',
+ message: `This will permanently delete the project "${argumentsToSubmit.name}" and all its contents.`,
+ }),
args: {
name: {
inputType: 'options',
diff --git a/src/lib/createMachineCommand.ts b/src/lib/createMachineCommand.ts
index 8903f2b5f7..0c4d89a5f0 100644
--- a/src/lib/createMachineCommand.ts
+++ b/src/lib/createMachineCommand.ts
@@ -111,6 +111,9 @@ export function createMachineCommand<
if ('displayName' in commandConfig) {
command.displayName = commandConfig.displayName
}
+ if ('reviewMessage' in commandConfig) {
+ command.reviewMessage = commandConfig.reviewMessage
+ }
return command
}
diff --git a/src/machines/homeMachine.ts b/src/machines/projectsMachine.ts
similarity index 57%
rename from src/machines/homeMachine.ts
rename to src/machines/projectsMachine.ts
index ca9a50e81d..322e9c0f6c 100644
--- a/src/machines/homeMachine.ts
+++ b/src/machines/projectsMachine.ts
@@ -1,8 +1,9 @@
import { assign, fromPromise, setup } from 'xstate'
-import { HomeCommandSchema } from 'lib/commandBarConfigs/homeCommandConfig'
+import { ProjectsCommandSchema } from 'lib/commandBarConfigs/projectsCommandConfig'
import { Project } from 'lib/project'
+import { isArray } from 'lib/utils'
-export const homeMachine = setup({
+export const projectsMachine = setup({
types: {
context: {} as {
projects: Project[]
@@ -11,15 +12,36 @@ export const homeMachine = setup({
},
events: {} as
| { type: 'Read projects'; data: {} }
- | { type: 'Open project'; data: HomeCommandSchema['Open project'] }
- | { type: 'Rename project'; data: HomeCommandSchema['Rename project'] }
- | { type: 'Create project'; data: HomeCommandSchema['Create project'] }
- | { type: 'Delete project'; data: HomeCommandSchema['Delete project'] }
+ | { type: 'Open project'; data: ProjectsCommandSchema['Open project'] }
+ | {
+ type: 'Rename project'
+ data: ProjectsCommandSchema['Rename project']
+ }
+ | {
+ type: 'Create project'
+ data: ProjectsCommandSchema['Create project']
+ }
+ | {
+ type: 'Delete project'
+ data: ProjectsCommandSchema['Delete project']
+ }
| { type: 'navigate'; data: { name: string } }
| {
type: 'xstate.done.actor.read-projects'
output: Project[]
}
+ | {
+ type: 'xstate.done.actor.delete-project'
+ output: { message: string; name: string }
+ }
+ | {
+ type: 'xstate.done.actor.create-project'
+ output: { message: string; name: string }
+ }
+ | {
+ type: 'xstate.done.actor.rename-project'
+ output: { message: string; oldName: string; newName: string }
+ }
| { type: 'assign'; data: { [key: string]: any } },
input: {} as {
projects: Project[]
@@ -30,16 +52,20 @@ export const homeMachine = setup({
actions: {
setProjects: assign({
projects: ({ context, event }) =>
- 'output' in event ? event.output : context.projects,
+ 'output' in event && isArray(event.output)
+ ? event.output
+ : context.projects,
}),
toastSuccess: () => {},
toastError: () => {},
navigateToProject: () => {},
+ navigateToProjectIfNeeded: () => {},
},
actors: {
readProjects: fromPromise(() => Promise.resolve([] as Project[])),
- createProject: fromPromise((_: { input: { name: string } }) =>
- Promise.resolve('')
+ createProject: fromPromise(
+ (_: { input: { name: string; projects: Project[] } }) =>
+ Promise.resolve({ message: '' })
),
renameProject: fromPromise(
(_: {
@@ -48,28 +74,35 @@ export const homeMachine = setup({
newName: string
defaultProjectName: string
defaultDirectory: string
+ projects: Project[]
}
- }) => Promise.resolve('')
+ }) =>
+ Promise.resolve({
+ message: '',
+ oldName: '',
+ newName: '',
+ })
),
deleteProject: fromPromise(
(_: { input: { defaultDirectory: string; name: string } }) =>
- Promise.resolve('')
+ Promise.resolve({
+ message: '',
+ name: '',
+ })
),
},
guards: {
'Has at least 1 project': () => false,
},
}).createMachine({
- /** @xstate-layout N4IgpgJg5mDOIC5QAkD2BbMACdBDAxgBYCWAdmAHTK6xampYAOATqgFZj4AusAxAMLMwuLthbtOXANoAGALqJQjVLGJdiqUopAAPRAHYAbPooAWABwBGUwE5zAJgeGArM-MAaEAE9EN0wGYKGX97GX1nGVNDS0MbfwBfeM80TBwCEnIqGiZWDm4+ACUwUlxU8TzpeW1lVXVNbT0EcJNg02d-fzt7fU77Tx8EQ0iKCPtnfUsjGRtLGXtE5IxsPCIySmpacsk+QWFRHIluWQUkEBq1DS1TxqN7ChjzOxtXf0t7a37EcwsRibH-ZzRezA8wLEApZbpNZZTa5ba8AAiYAANmB9lsjlVTuc6ldQDdDOYKP5bm0os5TDJDJ8mlEzPpzIZHA4bO9umCIWlVpkNgcKnwAPKMYp8yTHaoqC71a6IEmBUz6BkWZzWDq2Uw0qzOIJAwz+PXWfSmeZJcFLLkZSi7ERkKCi7i8CCaShkABuqAA1pR8EIRGAALQYyonJSS3ENRDA2wUeyvd6dPVhGw0-RhGOp8IA8xGFkc80rS0Ua3qUh2oO8MDMVjMCiMZEiABmqGY6AoPr2AaD4uxYcuEYQoQpQWNNjsMnMgLGKbT3TC7TcOfsNjzqQL0KKJXQtvtXEdzoobs9lCEm87cMxIbOvel+MQqtMQRmS5ks31sZpAUsZkcIX+cQZJIrpC3KUBupTbuWlbVrW9ZcE2LYUCepRnocwYSrUfYyggbzvBQ+jMq49imLYwTUt4iCft+5i-u0-7UfoQEWtCSKoiWZbnruTqZIeXoUBAKJoihFTdqGGE3rod7UdqsQTI8hiGAqrIauRA7RvYeoqhO1jtAqjFrpkLFohBHEVlWzYwY2zatvxrFCWKWKiVKeISdh4yBJE-jGs4fhhA4zg0kRNgxhplhaW0nn4XpUKZEUuAQMZqF8FxLqkO6vG+hAgYcbAIlXmJzmNERdy0RYNiKgpthxDSEU6q8MSTJYjWGFFIEULF8WljuSX7jxx7CJlQY5ZYl44pht4IP61gyPc8njt0lIuH51UKrVVITEyMy2C1hbtQl-KmdBdaWQhGVZYluWjeJjSTf402shMEyuEyljPAFL0UNmMiuN86lWHMiSmvQ-HwKcnL6WA6FOf2k3mESMRDA4RpUm4U4qf6gSEt0QIvvqfjOCaiyrtF6zZPQXWQ+GWFlUEsbmNMf1TV9NLeXDcqRIySnNaaYPEzC5M9vl-b+IyFCjupryPF9jKWP5Kks-cbMWLERHRNt0LFntkgU2NLk4dqsz43YsTK++Kk2C+MbTOOcxzOMrhqzFxTgZ1Qba1dd6BUE1jGsLMxxK9KlDNqm3tMLUQvqYlgO5QhlsTubsFXesTTUuPTfHExshDS0RftRftGgEnTZtHbX9Zr+QJ-2S4Y3qnmTC+4tMyp1EfeOnmeQqdOhyXQrFOXXCV1hCkmLDOnBJYvRRDSsyRzGjiKj0lKdAkANAA */
+ /** @xstate-layout N4IgpgJg5mDOIC5QAkD2BbMACdBDAxgBYCWAdmAMS6yzFSkDaADALqKgAOqtALsaqXYgAHogAsAJgA0IAJ6IAjBIkA2AHQAOCUw0qNkjQE4xYjQF8zMtJhwES5NcmpZSqLBwBOqAFZh8PWAoAJTBcCHcvX39YZjYkEC5efkF40QQFFQBmAHY1Qwz9QwBWU0NMrRl5BGyxBTUVJlVM01rtBXNLEGtsPCIyMEdnVwifPwCKAGEPUJ5sT1H-WKFE4j4BITSMzMzNIsyJDSZMhSYmFWzKxAkTNSYxIuU9zOKT7IsrDB67fsHYEajxiEwv8xjFWMtuKtkhsrpJboZsioincFMdTIjLggVGJ1GImMVMg0JISjEV3l1PrY+g4nH95gDAiFSLgbPSxkt4is1ilQGlrhJ4YjkbU0RoMXJEIYkWoikV8hJinjdOdyd0qfYBrSQdFJtNcLNtTwOZxIdyYQh+YKkSjReKqqYBQoEQpsgiSi9MqrKb0Nb9DYEACJgAA2YANbMW4M5puhqVhAvxQptCnRKkxCleuw0Gm2+2aRVRXpsPp+Woj4wA8hwwKRDcaEjH1nGLXDE9aRSmxWmJVipWocmdsbL8kxC501SWHFMZmQoIaKBABAMyAA3VAAawG+D1swAtOX61zY7zENlEWoMkoatcdPoipjCWIZRIirozsPiYYi19qQNp-rZ3nMAPC8Dw1A4YN9QAM1QDx0DUbcZjAfdInZKMTSSJsT2qc9Lwka8lTvTFTB2WUMiyQwc3yPRv3VH4mRZQDywXJc1FXDcBmmZlMBQhYjXQhtMJ5ERT1wlQr0kQj7kxKiZTxM5s2zbQEVoycBgY9AmNQ-wKGA0DwMgngYLgtQuJZZCDwEo8sJEnD1Dwgjb2kntig0GUkWVfZDEMfFVO+Bwg1DPhSDnZjFwcdjNzUCAQzDCztP4uIMKhGy0jPezxPwySnPvHsTjlPIhyOHRsnwlM-N-NRArDLS+N0kDYIM6DYPgmKgvivjD0bYS+VbBF21RTs7UUUcimfRpXyYRF8LFCrfSBCBaoZFiItINcor1CBeIZLqhPNdKL0yxzs2cqoNDqdpSo0EoSoLOb6NCRaQv9FblzWjjTMe7bQQYBQksElKesUMRjFubRMllCHsm2a5MVldRDBfFpmj0IpxPuhwFqW0F6v0iDmpMzbvuiXbAfNFNQcaI5IaKaH9jETFsUMNRVDxOU5TxBULE6VwYvgeIJ38sAIT25td27KpdzG7yZeho5slHVmMc1IY3HLfnkrNZt2hOW5smRCQM2uhQHkZ6VtkRBFnmu0qFGVv11ZFsnm0kdN8QFJVjlUPZ9co+3-2C0KEqdrXsJMJ8ryc86iUyYiCvxFNzldb3jHtjTsf8EPj1ssQchZlR8jR5EmDRrIGZcuF7maF1aZtslx29IWqtiwPDSz1LxDxepnlOfYDghp03eyOpngzXuYdHNPHozgJ26B9IXR2cTvP0BVkSRXKqgLtyq8OfQcXOwlubMIA */
id: 'Home machine',
initial: 'Reading projects',
- context: {
- projects: [],
- defaultProjectName: '',
- defaultDirectory: '',
- },
+ context: ({ input }) => ({
+ ...input,
+ }),
on: {
assign: {
@@ -110,7 +143,9 @@ export const homeMachine = setup({
},
'Open project': {
- target: 'Opening project',
+ target: 'Reading projects',
+ actions: 'navigateToProject',
+ reenter: true,
},
},
},
@@ -119,20 +154,22 @@ export const homeMachine = setup({
invoke: {
id: 'create-project',
src: 'createProject',
- input: ({ event }) => {
+ input: ({ event, context }) => {
if (event.type !== 'Create project') {
return {
name: '',
+ projects: context.projects,
}
}
return {
name: event.data.name,
+ projects: context.projects,
}
},
onDone: [
{
target: 'Reading projects',
- actions: ['toastSuccess'],
+ actions: ['toastSuccess', 'navigateToProject'],
},
],
onError: [
@@ -156,6 +193,7 @@ export const homeMachine = setup({
defaultDirectory: context.defaultDirectory,
oldName: '',
newName: '',
+ projects: context.projects,
}
}
return {
@@ -163,12 +201,13 @@ export const homeMachine = setup({
defaultDirectory: context.defaultDirectory,
oldName: event.data.oldName,
newName: event.data.newName,
+ projects: context.projects,
}
},
onDone: [
{
target: '#Home machine.Reading projects',
- actions: ['toastSuccess'],
+ actions: ['toastSuccess', 'navigateToProjectIfNeeded'],
},
],
onError: [
@@ -199,7 +238,7 @@ export const homeMachine = setup({
},
onDone: [
{
- actions: ['toastSuccess'],
+ actions: ['toastSuccess', 'navigateToProjectIfNeeded'],
target: '#Home machine.Reading projects',
},
],
@@ -233,9 +272,5 @@ export const homeMachine = setup({
],
},
},
-
- 'Opening project': {
- entry: ['navigateToProject'],
- },
},
})
diff --git a/src/routes/Home.tsx b/src/routes/Home.tsx
index 0a5331fd78..f54fcbe516 100644
--- a/src/routes/Home.tsx
+++ b/src/routes/Home.tsx
@@ -1,60 +1,42 @@
import { FormEvent, useEffect, useRef, useState } from 'react'
-import {
- getNextProjectIndex,
- interpolateProjectNameWithIndex,
- doesProjectNameNeedInterpolated,
-} from 'lib/desktopFS'
import { ActionButton } from 'components/ActionButton'
-import { toast } from 'react-hot-toast'
import { AppHeader } from 'components/AppHeader'
import ProjectCard from 'components/ProjectCard/ProjectCard'
import { useNavigate, useSearchParams } from 'react-router-dom'
import { Link } from 'react-router-dom'
import Loading from 'components/Loading'
-import { useMachine } from '@xstate/react'
-import { homeMachine } from '../machines/homeMachine'
-import { fromPromise } from 'xstate'
import { PATHS } from 'lib/paths'
import {
getNextSearchParams,
getSortFunction,
getSortIcon,
} from '../lib/sorting'
-import useStateMachineCommands from '../hooks/useStateMachineCommands'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
-import { useCommandsContext } from 'hooks/useCommandsContext'
-import { homeCommandBarConfig } from 'lib/commandBarConfigs/homeCommandConfig'
import { useHotkeys } from 'react-hotkeys-hook'
import { isDesktop } from 'lib/isDesktop'
import { kclManager } from 'lib/singletons'
-import { useLspContext } from 'components/LspProvider'
import { useRefreshSettings } from 'hooks/useRefreshSettings'
import { LowerRightControls } from 'components/LowerRightControls'
-import {
- createNewProjectDirectory,
- listProjects,
- renameProjectDirectory,
-} from 'lib/desktop'
import { ProjectSearchBar, useProjectSearch } from 'components/ProjectSearchBar'
import { Project } from 'lib/project'
import { useFileSystemWatcher } from 'hooks/useFileSystemWatcher'
import { useProjectsLoader } from 'hooks/useProjectsLoader'
+import { useProjectsContext } from 'hooks/useProjectsContext'
+import { useCommandsContext } from 'hooks/useCommandsContext'
// This route only opens in the desktop context for now,
// as defined in Router.tsx, so we can use the desktop APIs and types.
const Home = () => {
+ const { state, send } = useProjectsContext()
+ const { commandBarSend } = useCommandsContext()
const [projectsLoaderTrigger, setProjectsLoaderTrigger] = useState(0)
- const { projectPaths, projectsDir } = useProjectsLoader([
- projectsLoaderTrigger,
- ])
+ const { projectsDir } = useProjectsLoader([projectsLoaderTrigger])
useRefreshSettings(PATHS.HOME + 'SETTINGS')
- const { commandBarSend } = useCommandsContext()
const navigate = useNavigate()
const {
settings: { context: settings },
} = useSettingsAuthContext()
- const { onProjectOpen } = useLspContext()
// Cancel all KCL executions while on the home page
useEffect(() => {
@@ -73,107 +55,6 @@ const Home = () => {
)
const ref = useRef(null)
- const [state, send, actor] = useMachine(
- homeMachine.provide({
- actions: {
- navigateToProject: ({ context, event }) => {
- if ('data' in event && event.data && 'name' in event.data) {
- let projectPath =
- context.defaultDirectory +
- window.electron.path.sep +
- event.data.name
- onProjectOpen(
- {
- name: event.data.name,
- path: projectPath,
- },
- null
- )
- commandBarSend({ type: 'Close' })
- navigate(`${PATHS.FILE}/${encodeURIComponent(projectPath)}`)
- }
- },
- toastSuccess: ({ event }) =>
- toast.success(
- ('data' in event && typeof event.data === 'string' && event.data) ||
- ('output' in event &&
- typeof event.output === 'string' &&
- event.output) ||
- ''
- ),
- toastError: ({ event }) =>
- toast.error(
- ('data' in event && typeof event.data === 'string' && event.data) ||
- ('output' in event &&
- typeof event.output === 'string' &&
- event.output) ||
- ''
- ),
- },
- actors: {
- readProjects: fromPromise(() => listProjects()),
- createProject: fromPromise(async ({ input }) => {
- let name = (
- input && 'name' in input && input.name
- ? input.name
- : settings.projects.defaultProjectName.current
- ).trim()
-
- if (doesProjectNameNeedInterpolated(name)) {
- const nextIndex = getNextProjectIndex(name, projects)
- name = interpolateProjectNameWithIndex(name, nextIndex)
- }
-
- await createNewProjectDirectory(name)
-
- return `Successfully created "${name}"`
- }),
- renameProject: fromPromise(async ({ input }) => {
- const { oldName, newName, defaultProjectName, defaultDirectory } =
- input
- let name = newName ? newName : defaultProjectName
- if (doesProjectNameNeedInterpolated(name)) {
- const nextIndex = await getNextProjectIndex(name, projects)
- name = interpolateProjectNameWithIndex(name, nextIndex)
- }
-
- await renameProjectDirectory(
- window.electron.path.join(defaultDirectory, oldName),
- name
- )
- return `Successfully renamed "${oldName}" to "${name}"`
- }),
- deleteProject: fromPromise(async ({ input }) => {
- await window.electron.rm(
- window.electron.path.join(input.defaultDirectory, input.name),
- {
- recursive: true,
- }
- )
- return `Successfully deleted "${input.name}"`
- }),
- },
- guards: {
- 'Has at least 1 project': ({ event }) => {
- if (event.type !== 'xstate.done.actor.read-projects') return false
- console.log(`from has at least 1 project: ${event.output.length}`)
- return event.output.length ? event.output.length >= 1 : false
- },
- },
- }),
- {
- input: {
- projects: projectPaths,
- defaultProjectName: settings.projects.defaultProjectName.current,
- defaultDirectory: settings.app.projectDirectory.current,
- },
- }
- )
-
- useEffect(() => {
- send({ type: 'Read projects', data: {} })
- }, [projectPaths])
-
// Re-read projects listing if the projectDir has any updates.
useFileSystemWatcher(
async () => {
@@ -182,21 +63,13 @@ const Home = () => {
projectsDir ? [projectsDir] : []
)
- const { projects } = state.context
+ const projects = state?.context.projects ?? []
const [searchParams, setSearchParams] = useSearchParams()
const { searchResults, query, setQuery } = useProjectSearch(projects)
const sort = searchParams.get('sort_by') ?? 'modified:desc'
const isSortByModified = sort?.includes('modified') || !sort || sort === null
- useStateMachineCommands({
- machineId: 'home',
- send,
- state,
- commandBarConfig: homeCommandBarConfig,
- actor,
- })
-
// Update the default project name and directory in the home machine
// when the settings change
useEffect(() => {
@@ -247,7 +120,16 @@ const Home = () => {
- send({ type: 'Create project', data: { name: '' } })
+ commandBarSend({
+ type: 'Find and select command',
+ data: {
+ groupId: 'projects',
+ name: 'Create project',
+ argDefaultValues: {
+ name: settings.projects.defaultProjectName.current,
+ },
+ },
+ })
}
className="group !bg-primary !text-chalkboard-10 !border-primary hover:shadow-inner hover:hue-rotate-15"
iconStart={{
@@ -326,7 +208,7 @@ const Home = () => {
data-testid="home-section"
className="flex-1 overflow-y-auto pr-2 pb-24"
>
- {state.matches('Reading projects') ? (
+ {state?.matches('Reading projects') ? (
Loading your Projects...
) : (
<>