Chore: separate out projectMachine from Home route (#4109)
* Rename `homeMachine` and accessories to `projectsMachine` * Separate out `/home` route from `projectsMachine` * Add logic to navigate out from deleted or renamed project * Show a warning in the command palette for deleting a project * Make it navigate when you create a project * Update "New project" button to use command bar flow Closes #2585 * More explicit warning message text * Make projects watching code not run in web * Tests first version: nested loops * Tests second version: flattened * Remove console logs * Fix tsc * @jtran feedback, use the type guard util * A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest) * A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest) * A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest) * Fix tests that relied on one-click, no-navigation project creation * Revert "A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest)" This reverts commit7545b61b49
. * Revert "A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest)" This reverts commit3d2e48732c
. * Add a mask to the state indicator to client-side scale test * A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest) * A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest) * A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest) * Fix lint * Fix tsc * Fix a couple stray tests that still relied on the old way of creating projects * De-flake another text that could be thrown off by toast-based selectors * FMT * Dumb test error because I was rushing * A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest) * Ahhh more flaky toasts, they're everywhere! * A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest) * A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest) * A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest) * Re-run CI * A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest) * Re-run CI * Fix one test added since this PR was made * Fix a few tests that failed due to changes since PR was made * Prevent double selector issue in Ubuntu test * Prevent *a different* double selector issue --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
@ -3,6 +3,7 @@ import { test, expect } from './fixtures/fixtureSetup'
|
|||||||
import * as fsp from 'fs/promises'
|
import * as fsp from 'fs/promises'
|
||||||
import * as fs from 'fs'
|
import * as fs from 'fs'
|
||||||
import {
|
import {
|
||||||
|
createProject,
|
||||||
executorInputPath,
|
executorInputPath,
|
||||||
getUtils,
|
getUtils,
|
||||||
setup,
|
setup,
|
||||||
@ -114,20 +115,15 @@ test.describe('when using the file tree to', () => {
|
|||||||
async ({ browser: _, tronApp }, testInfo) => {
|
async ({ browser: _, tronApp }, testInfo) => {
|
||||||
await tronApp.initialise()
|
await tronApp.initialise()
|
||||||
|
|
||||||
const {
|
const { panesOpen, pasteCodeInEditor, renameFile, editorTextMatches } =
|
||||||
panesOpen,
|
await getUtils(tronApp.page, test)
|
||||||
createAndSelectProject,
|
|
||||||
pasteCodeInEditor,
|
|
||||||
renameFile,
|
|
||||||
editorTextMatches,
|
|
||||||
} = await getUtils(tronApp.page, test)
|
|
||||||
|
|
||||||
await tronApp.page.setViewportSize({ width: 1200, height: 500 })
|
await tronApp.page.setViewportSize({ width: 1200, height: 500 })
|
||||||
tronApp.page.on('console', console.log)
|
tronApp.page.on('console', console.log)
|
||||||
|
|
||||||
await panesOpen(['files', 'code'])
|
await panesOpen(['files', 'code'])
|
||||||
|
|
||||||
await createAndSelectProject('project-000')
|
await createProject({ name: 'project-000', page: tronApp.page })
|
||||||
|
|
||||||
// File the main.kcl with contents
|
// File the main.kcl with contents
|
||||||
const kclCube = await fsp.readFile(
|
const kclCube = await fsp.readFile(
|
||||||
@ -167,15 +163,14 @@ test.describe('when using the file tree to', () => {
|
|||||||
async ({ browser: _, tronApp }, testInfo) => {
|
async ({ browser: _, tronApp }, testInfo) => {
|
||||||
await tronApp.initialise()
|
await tronApp.initialise()
|
||||||
|
|
||||||
const { panesOpen, createAndSelectProject, createNewFile } =
|
const { panesOpen, createNewFile } = await getUtils(tronApp.page, test)
|
||||||
await getUtils(tronApp.page, test)
|
|
||||||
|
|
||||||
await tronApp.page.setViewportSize({ width: 1200, height: 500 })
|
await tronApp.page.setViewportSize({ width: 1200, height: 500 })
|
||||||
tronApp.page.on('console', console.log)
|
tronApp.page.on('console', console.log)
|
||||||
|
|
||||||
await panesOpen(['files'])
|
await panesOpen(['files'])
|
||||||
|
|
||||||
await createAndSelectProject('project-000')
|
await createProject({ name: 'project-000', page: tronApp.page })
|
||||||
|
|
||||||
await createNewFile('')
|
await createNewFile('')
|
||||||
await createNewFile('')
|
await createNewFile('')
|
||||||
@ -204,7 +199,6 @@ test.describe('when using the file tree to', () => {
|
|||||||
const {
|
const {
|
||||||
openKclCodePanel,
|
openKclCodePanel,
|
||||||
openFilePanel,
|
openFilePanel,
|
||||||
createAndSelectProject,
|
|
||||||
pasteCodeInEditor,
|
pasteCodeInEditor,
|
||||||
createNewFileAndSelect,
|
createNewFileAndSelect,
|
||||||
renameFile,
|
renameFile,
|
||||||
@ -215,7 +209,7 @@ test.describe('when using the file tree to', () => {
|
|||||||
await tronApp.page.setViewportSize({ width: 1200, height: 500 })
|
await tronApp.page.setViewportSize({ width: 1200, height: 500 })
|
||||||
tronApp.page.on('console', console.log)
|
tronApp.page.on('console', console.log)
|
||||||
|
|
||||||
await createAndSelectProject('project-000')
|
await createProject({ name: 'project-000', page: tronApp.page })
|
||||||
await openKclCodePanel()
|
await openKclCodePanel()
|
||||||
await openFilePanel()
|
await openFilePanel()
|
||||||
// File the main.kcl with contents
|
// File the main.kcl with contents
|
||||||
@ -263,20 +257,15 @@ test.describe('when using the file tree to', () => {
|
|||||||
async ({ browser: _, tronApp }, testInfo) => {
|
async ({ browser: _, tronApp }, testInfo) => {
|
||||||
await tronApp.initialise()
|
await tronApp.initialise()
|
||||||
|
|
||||||
const {
|
const { panesOpen, pasteCodeInEditor, deleteFile, editorTextMatches } =
|
||||||
panesOpen,
|
await getUtils(tronApp.page, _test)
|
||||||
createAndSelectProject,
|
|
||||||
pasteCodeInEditor,
|
|
||||||
deleteFile,
|
|
||||||
editorTextMatches,
|
|
||||||
} = await getUtils(tronApp.page, _test)
|
|
||||||
|
|
||||||
await tronApp.page.setViewportSize({ width: 1200, height: 500 })
|
await tronApp.page.setViewportSize({ width: 1200, height: 500 })
|
||||||
tronApp.page.on('console', console.log)
|
tronApp.page.on('console', console.log)
|
||||||
|
|
||||||
await panesOpen(['files', 'code'])
|
await panesOpen(['files', 'code'])
|
||||||
|
|
||||||
await createAndSelectProject('project-000')
|
await createProject({ name: 'project-000', page: tronApp.page })
|
||||||
// File the main.kcl with contents
|
// File the main.kcl with contents
|
||||||
const kclCube = await fsp.readFile(
|
const kclCube = await fsp.readFile(
|
||||||
'src/wasm-lib/tests/executor/inputs/cube.kcl',
|
'src/wasm-lib/tests/executor/inputs/cube.kcl',
|
||||||
@ -306,7 +295,6 @@ test.describe('when using the file tree to', () => {
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
panesOpen,
|
panesOpen,
|
||||||
createAndSelectProject,
|
|
||||||
pasteCodeInEditor,
|
pasteCodeInEditor,
|
||||||
createNewFile,
|
createNewFile,
|
||||||
openDebugPanel,
|
openDebugPanel,
|
||||||
@ -318,7 +306,7 @@ test.describe('when using the file tree to', () => {
|
|||||||
tronApp.page.on('console', console.log)
|
tronApp.page.on('console', console.log)
|
||||||
|
|
||||||
await panesOpen(['files', 'code'])
|
await panesOpen(['files', 'code'])
|
||||||
await createAndSelectProject('project-000')
|
await createProject({ name: 'project-000', page: tronApp.page })
|
||||||
|
|
||||||
// Create a small file
|
// Create a small file
|
||||||
const kclCube = await fsp.readFile(
|
const kclCube = await fsp.readFile(
|
||||||
@ -722,7 +710,7 @@ _test.describe('Renaming in the file tree', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
await _test.step('Rename the folder', async () => {
|
await _test.step('Rename the folder', async () => {
|
||||||
await page.waitForTimeout(60000)
|
await page.waitForTimeout(1000)
|
||||||
await folderToRename.click({ button: 'right' })
|
await folderToRename.click({ button: 'right' })
|
||||||
await _expect(renameMenuItem).toBeVisible()
|
await _expect(renameMenuItem).toBeVisible()
|
||||||
await renameMenuItem.click()
|
await renameMenuItem.click()
|
||||||
|
@ -7,6 +7,7 @@ import {
|
|||||||
setupElectron,
|
setupElectron,
|
||||||
tearDown,
|
tearDown,
|
||||||
executorInputPath,
|
executorInputPath,
|
||||||
|
createProject,
|
||||||
} from './test-utils'
|
} from './test-utils'
|
||||||
import { bracket } from 'lib/exampleKcl'
|
import { bracket } from 'lib/exampleKcl'
|
||||||
import { onboardingPaths } from 'routes/Onboarding/paths'
|
import { onboardingPaths } from 'routes/Onboarding/paths'
|
||||||
@ -74,13 +75,8 @@ test.describe('Onboarding tests', () => {
|
|||||||
const viewportSize = { width: 1200, height: 500 }
|
const viewportSize = { width: 1200, height: 500 }
|
||||||
await page.setViewportSize(viewportSize)
|
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 test.step(`Create a project and open to the onboarding`, async () => {
|
||||||
await newProjectButton.click()
|
await createProject({ name: 'project-link', page })
|
||||||
await projectLink.click()
|
|
||||||
await test.step(`Ensure the engine connection works by testing the sketch button`, async () => {
|
await test.step(`Ensure the engine connection works by testing the sketch button`, async () => {
|
||||||
await u.waitForPageLoad()
|
await u.waitForPageLoad()
|
||||||
})
|
})
|
||||||
|
@ -7,7 +7,7 @@ import {
|
|||||||
Paths,
|
Paths,
|
||||||
setupElectron,
|
setupElectron,
|
||||||
tearDown,
|
tearDown,
|
||||||
createProjectAndRenameIt,
|
createProject,
|
||||||
} from './test-utils'
|
} from './test-utils'
|
||||||
import fsp from 'fs/promises'
|
import fsp from 'fs/promises'
|
||||||
import fs from 'fs'
|
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(
|
test(
|
||||||
'File in the file pane should open with a single click',
|
'File in the file pane should open with a single click',
|
||||||
{ tag: '@electron' },
|
{ tag: '@electron' },
|
||||||
@ -644,7 +883,7 @@ test(
|
|||||||
page.on('console', console.log)
|
page.on('console', console.log)
|
||||||
|
|
||||||
await test.step('delete the middle project, i.e. the bracket project', async () => {
|
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.hover()
|
||||||
await project.focus()
|
await project.focus()
|
||||||
@ -688,10 +927,10 @@ test(
|
|||||||
})
|
})
|
||||||
|
|
||||||
await test.step('Check we can still create a project', async () => {
|
await test.step('Check we can still create a project', async () => {
|
||||||
await page.getByRole('button', { name: 'New project' }).click()
|
await createProject({ name: 'project-000', page, returnHome: true })
|
||||||
await expect(page.getByText('Successfully created')).toBeVisible()
|
await expect(
|
||||||
await expect(page.getByText('Successfully created')).not.toBeVisible()
|
page.getByTestId('project-link').filter({ hasText: 'project-000' })
|
||||||
await expect(page.getByText('project-000')).toBeVisible()
|
).toBeVisible()
|
||||||
})
|
})
|
||||||
|
|
||||||
await electronApp.close()
|
await electronApp.close()
|
||||||
@ -868,17 +1107,16 @@ test.fixme(
|
|||||||
const pointOnModel = { x: 660, y: 250 }
|
const pointOnModel = { x: 660, y: 250 }
|
||||||
const expectedStartCamZPosition = 15633.47
|
const expectedStartCamZPosition = 15633.47
|
||||||
|
|
||||||
|
// Constants and locators
|
||||||
|
const projectLinks = page.getByTestId('project-link')
|
||||||
|
|
||||||
// expect to see text "No Projects found"
|
// expect to see text "No Projects found"
|
||||||
await expect(page.getByText('No Projects found')).toBeVisible()
|
await expect(page.getByText('No Projects found')).toBeVisible()
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'New project' }).click()
|
await createProject({ name: 'project-000', page, returnHome: true })
|
||||||
|
await expect(projectLinks.getByText('project-000')).toBeVisible()
|
||||||
|
|
||||||
await expect(page.getByText('Successfully created')).toBeVisible()
|
await projectLinks.getByText('project-000').click()
|
||||||
await expect(page.getByText('Successfully created')).not.toBeVisible()
|
|
||||||
|
|
||||||
await expect(page.getByText('project-000')).toBeVisible()
|
|
||||||
|
|
||||||
await page.getByText('project-000').click()
|
|
||||||
|
|
||||||
await u.waitForPageLoad()
|
await u.waitForPageLoad()
|
||||||
|
|
||||||
@ -937,16 +1175,10 @@ extrude001 = extrude(200, sketch001)`)
|
|||||||
page.getByRole('button', { name: 'New project' })
|
page.getByRole('button', { name: 'New project' })
|
||||||
).toBeVisible()
|
).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++) {
|
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()
|
await electronApp.close()
|
||||||
}
|
}
|
||||||
@ -1121,11 +1353,10 @@ test(
|
|||||||
await page.getByTestId('settings-close-button').click()
|
await page.getByTestId('settings-close-button').click()
|
||||||
|
|
||||||
await expect(page.getByText('No Projects found')).toBeVisible()
|
await expect(page.getByText('No Projects found')).toBeVisible()
|
||||||
await page.getByRole('button', { name: 'New project' }).click()
|
await createProject({ name: 'project-000', page, returnHome: true })
|
||||||
await expect(page.getByText('Successfully created')).toBeVisible()
|
await expect(
|
||||||
await expect(page.getByText('Successfully created')).not.toBeVisible()
|
page.getByTestId('project-link').filter({ hasText: 'project-000' })
|
||||||
|
).toBeVisible()
|
||||||
await expect(page.getByText(`project-000`)).toBeVisible()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
await test.step('We can change back to the original root project directory', async () => {
|
await test.step('We can change back to the original root project directory', async () => {
|
||||||
@ -1451,7 +1682,7 @@ test(
|
|||||||
page.on('console', console.log)
|
page.on('console', console.log)
|
||||||
|
|
||||||
await test.step('Should create and name a project called wrist brace', async () => {
|
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 () => {
|
await test.step('Should go through onboarding', async () => {
|
||||||
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 52 KiB |
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 51 KiB |
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
@ -467,20 +467,6 @@ export async function getUtils(page: Page, test_?: typeof test) {
|
|||||||
return text.replace(/\s+/g, '')
|
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) => {
|
editorTextMatches: async (code: string) => {
|
||||||
const editor = page.locator(editorSelector)
|
const editor = page.locator(editorSelector)
|
||||||
return expect(editor).toHaveText(code, { useInnerText: true })
|
return expect(editor).toHaveText(code, { useInnerText: true })
|
||||||
@ -980,30 +966,25 @@ export async function isOutOfViewInScrollContainer(
|
|||||||
return isOutOfView
|
return isOutOfView
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createProjectAndRenameIt({
|
export async function createProject({
|
||||||
name,
|
name,
|
||||||
page,
|
page,
|
||||||
|
returnHome = false,
|
||||||
}: {
|
}: {
|
||||||
name: string
|
name: string
|
||||||
page: Page
|
page: Page
|
||||||
|
returnHome?: boolean
|
||||||
}) {
|
}) {
|
||||||
|
await test.step(`Create project and navigate to it`, async () => {
|
||||||
await page.getByRole('button', { name: 'New project' }).click()
|
await page.getByRole('button', { name: 'New project' }).click()
|
||||||
await expect(page.getByText('Successfully created')).toBeVisible()
|
await page.getByRole('textbox', { name: 'Name' }).fill(name)
|
||||||
await expect(page.getByText('Successfully created')).not.toBeVisible()
|
await page.getByRole('button', { name: 'Continue' }).click()
|
||||||
|
|
||||||
await expect(page.getByText(`project-000`)).toBeVisible()
|
if (returnHome) {
|
||||||
await page.getByText(`project-000`).hover()
|
await page.waitForURL('**/file/**', { waitUntil: 'domcontentloaded' })
|
||||||
await page.getByText(`project-000`).focus()
|
await page.getByTestId('app-logo').click()
|
||||||
|
}
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function executorInputPath(fileName: string): string {
|
export function executorInputPath(fileName: string): string {
|
||||||
|
@ -7,6 +7,7 @@ import {
|
|||||||
setupElectron,
|
setupElectron,
|
||||||
tearDown,
|
tearDown,
|
||||||
executorInputPath,
|
executorInputPath,
|
||||||
|
createProject,
|
||||||
} from './test-utils'
|
} from './test-utils'
|
||||||
import { SaveSettingsPayload, SettingsLevel } from 'lib/settings/settingsTypes'
|
import { SaveSettingsPayload, SettingsLevel } from 'lib/settings/settingsTypes'
|
||||||
import { SETTINGS_FILE_NAME, PROJECT_SETTINGS_FILE_NAME } from 'lib/constants'
|
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 test.step('Check color of logo changed when in modeling view', async () => {
|
||||||
await page.getByRole('button', { name: 'New project' }).click()
|
await createProject({ name: 'project-000', page })
|
||||||
await page.getByTestId('project-link').first().click()
|
|
||||||
await changeColor('58')
|
await changeColor('58')
|
||||||
await expect(logoLink).toHaveCSS('--primary-hue', '58')
|
await expect(logoLink).toHaveCSS('--primary-hue', '58')
|
||||||
})
|
})
|
||||||
@ -447,7 +447,7 @@ test.describe('Testing settings', () => {
|
|||||||
test(
|
test(
|
||||||
'project settings reload on external change',
|
'project settings reload on external change',
|
||||||
{ tag: '@electron' },
|
{ tag: '@electron' },
|
||||||
async ({ browserName }, testInfo) => {
|
async ({ browserName: _ }, testInfo) => {
|
||||||
const {
|
const {
|
||||||
electronApp,
|
electronApp,
|
||||||
page,
|
page,
|
||||||
@ -465,11 +465,7 @@ test.describe('Testing settings', () => {
|
|||||||
await expect(projectDirLink).toBeVisible()
|
await expect(projectDirLink).toBeVisible()
|
||||||
})
|
})
|
||||||
|
|
||||||
const projectLinks = page.getByTestId('project-link')
|
await createProject({ name: 'project-000', page })
|
||||||
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()
|
|
||||||
|
|
||||||
const changeColorFs = async (color: string) => {
|
const changeColorFs = async (color: string) => {
|
||||||
const tempSettingsFilePath = join(
|
const tempSettingsFilePath = join(
|
||||||
|
@ -1,5 +1,11 @@
|
|||||||
import { test, expect, Page } from '@playwright/test'
|
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 { join } from 'path'
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
|
|
||||||
@ -700,12 +706,10 @@ test(
|
|||||||
const fileExists = () =>
|
const fileExists = () =>
|
||||||
fs.existsSync(join(dir, projectName, textToCadFileName))
|
fs.existsSync(join(dir, projectName, textToCadFileName))
|
||||||
|
|
||||||
const {
|
const { openFilePanel, openKclCodePanel, waitForPageLoad } = await getUtils(
|
||||||
createAndSelectProject,
|
page,
|
||||||
openFilePanel,
|
test
|
||||||
openKclCodePanel,
|
)
|
||||||
waitForPageLoad,
|
|
||||||
} = await getUtils(page, test)
|
|
||||||
|
|
||||||
await page.setViewportSize({ width: 1200, height: 500 })
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
|
|
||||||
@ -721,7 +725,7 @@ test(
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Create and navigate to the project
|
// 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
|
// Wait for Start Sketch otherwise you will not have access Text-to-CAD command
|
||||||
await waitForPageLoad()
|
await waitForPageLoad()
|
||||||
|
@ -43,6 +43,7 @@ import { coreDump } from 'lang/wasm'
|
|||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import { AppStateProvider } from 'AppState'
|
import { AppStateProvider } from 'AppState'
|
||||||
import { reportRejection } from 'lib/trap'
|
import { reportRejection } from 'lib/trap'
|
||||||
|
import { ProjectsContextProvider } from 'components/ProjectsContextProvider'
|
||||||
|
|
||||||
const createRouter = isDesktop() ? createHashRouter : createBrowserRouter
|
const createRouter = isDesktop() ? createHashRouter : createBrowserRouter
|
||||||
|
|
||||||
@ -57,6 +58,7 @@ const router = createRouter([
|
|||||||
<CommandBarProvider>
|
<CommandBarProvider>
|
||||||
<SettingsAuthProvider>
|
<SettingsAuthProvider>
|
||||||
<LspProvider>
|
<LspProvider>
|
||||||
|
<ProjectsContextProvider>
|
||||||
<KclContextProvider>
|
<KclContextProvider>
|
||||||
<AppStateProvider>
|
<AppStateProvider>
|
||||||
<MachineManagerProvider>
|
<MachineManagerProvider>
|
||||||
@ -64,6 +66,7 @@ const router = createRouter([
|
|||||||
</MachineManagerProvider>
|
</MachineManagerProvider>
|
||||||
</AppStateProvider>
|
</AppStateProvider>
|
||||||
</KclContextProvider>
|
</KclContextProvider>
|
||||||
|
</ProjectsContextProvider>
|
||||||
</LspProvider>
|
</LspProvider>
|
||||||
</SettingsAuthProvider>
|
</SettingsAuthProvider>
|
||||||
</CommandBarProvider>
|
</CommandBarProvider>
|
||||||
|
289
src/components/ProjectsContextProvider.tsx
Normal file
@ -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<T extends AnyStateMachine> = {
|
||||||
|
state?: StateFrom<T>
|
||||||
|
send: Prop<Actor<T>, 'send'>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProjectsMachineContext = createContext(
|
||||||
|
{} as MachineContext<typeof projectsMachine>
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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() ? (
|
||||||
|
<ProjectsContextDesktop>{children}</ProjectsContextDesktop>
|
||||||
|
) : (
|
||||||
|
<ProjectsContextWeb>{children}</ProjectsContextWeb>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProjectsContextWeb = ({ children }: { children: React.ReactNode }) => {
|
||||||
|
return (
|
||||||
|
<ProjectsMachineContext.Provider
|
||||||
|
value={{
|
||||||
|
state: undefined,
|
||||||
|
send: () => {},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ProjectsMachineContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<ProjectsMachineContext.Provider
|
||||||
|
value={{
|
||||||
|
state,
|
||||||
|
send,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ProjectsMachineContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
6
src/hooks/useProjectsContext.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { ProjectsMachineContext } from 'components/ProjectsContextProvider'
|
||||||
|
import { useContext } from 'react'
|
||||||
|
|
||||||
|
export const useProjectsContext = () => {
|
||||||
|
return useContext(ProjectsMachineContext)
|
||||||
|
}
|
@ -5,7 +5,7 @@ import { useCommandsContext } from './useCommandsContext'
|
|||||||
import { modelingMachine } from 'machines/modelingMachine'
|
import { modelingMachine } from 'machines/modelingMachine'
|
||||||
import { authMachine } from 'machines/authMachine'
|
import { authMachine } from 'machines/authMachine'
|
||||||
import { settingsMachine } from 'machines/settingsMachine'
|
import { settingsMachine } from 'machines/settingsMachine'
|
||||||
import { homeMachine } from 'machines/homeMachine'
|
import { projectsMachine } from 'machines/projectsMachine'
|
||||||
import {
|
import {
|
||||||
Command,
|
Command,
|
||||||
StateMachineCommandSetConfig,
|
StateMachineCommandSetConfig,
|
||||||
@ -22,7 +22,7 @@ export type AllMachines =
|
|||||||
| typeof modelingMachine
|
| typeof modelingMachine
|
||||||
| typeof settingsMachine
|
| typeof settingsMachine
|
||||||
| typeof authMachine
|
| typeof authMachine
|
||||||
| typeof homeMachine
|
| typeof projectsMachine
|
||||||
|
|
||||||
interface UseStateMachineCommandsArgs<
|
interface UseStateMachineCommandsArgs<
|
||||||
T extends AllMachines,
|
T extends AllMachines,
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
|
import { CommandBarOverwriteWarning } from 'components/CommandBarOverwriteWarning'
|
||||||
import { StateMachineCommandSetConfig } from 'lib/commandTypes'
|
import { StateMachineCommandSetConfig } from 'lib/commandTypes'
|
||||||
import { homeMachine } from 'machines/homeMachine'
|
import { projectsMachine } from 'machines/projectsMachine'
|
||||||
|
|
||||||
export type HomeCommandSchema = {
|
export type ProjectsCommandSchema = {
|
||||||
'Read projects': {}
|
'Read projects': {}
|
||||||
'Create project': {
|
'Create project': {
|
||||||
name: string
|
name: string
|
||||||
@ -18,9 +19,9 @@ export type HomeCommandSchema = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const homeCommandBarConfig: StateMachineCommandSetConfig<
|
export const projectsCommandBarConfig: StateMachineCommandSetConfig<
|
||||||
typeof homeMachine,
|
typeof projectsMachine,
|
||||||
HomeCommandSchema
|
ProjectsCommandSchema
|
||||||
> = {
|
> = {
|
||||||
'Open project': {
|
'Open project': {
|
||||||
icon: 'arrowRight',
|
icon: 'arrowRight',
|
||||||
@ -53,6 +54,11 @@ export const homeCommandBarConfig: StateMachineCommandSetConfig<
|
|||||||
icon: 'close',
|
icon: 'close',
|
||||||
description: 'Delete a project',
|
description: 'Delete a project',
|
||||||
needsReview: true,
|
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: {
|
args: {
|
||||||
name: {
|
name: {
|
||||||
inputType: 'options',
|
inputType: 'options',
|
@ -111,6 +111,9 @@ export function createMachineCommand<
|
|||||||
if ('displayName' in commandConfig) {
|
if ('displayName' in commandConfig) {
|
||||||
command.displayName = commandConfig.displayName
|
command.displayName = commandConfig.displayName
|
||||||
}
|
}
|
||||||
|
if ('reviewMessage' in commandConfig) {
|
||||||
|
command.reviewMessage = commandConfig.reviewMessage
|
||||||
|
}
|
||||||
|
|
||||||
return command
|
return command
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import { assign, fromPromise, setup } from 'xstate'
|
import { assign, fromPromise, setup } from 'xstate'
|
||||||
import { HomeCommandSchema } from 'lib/commandBarConfigs/homeCommandConfig'
|
import { ProjectsCommandSchema } from 'lib/commandBarConfigs/projectsCommandConfig'
|
||||||
import { Project } from 'lib/project'
|
import { Project } from 'lib/project'
|
||||||
|
import { isArray } from 'lib/utils'
|
||||||
|
|
||||||
export const homeMachine = setup({
|
export const projectsMachine = setup({
|
||||||
types: {
|
types: {
|
||||||
context: {} as {
|
context: {} as {
|
||||||
projects: Project[]
|
projects: Project[]
|
||||||
@ -11,15 +12,36 @@ export const homeMachine = setup({
|
|||||||
},
|
},
|
||||||
events: {} as
|
events: {} as
|
||||||
| { type: 'Read projects'; data: {} }
|
| { type: 'Read projects'; data: {} }
|
||||||
| { type: 'Open project'; data: HomeCommandSchema['Open project'] }
|
| { type: 'Open project'; data: ProjectsCommandSchema['Open project'] }
|
||||||
| { type: 'Rename project'; data: HomeCommandSchema['Rename project'] }
|
| {
|
||||||
| { type: 'Create project'; data: HomeCommandSchema['Create project'] }
|
type: 'Rename project'
|
||||||
| { type: 'Delete project'; data: HomeCommandSchema['Delete 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: 'navigate'; data: { name: string } }
|
||||||
| {
|
| {
|
||||||
type: 'xstate.done.actor.read-projects'
|
type: 'xstate.done.actor.read-projects'
|
||||||
output: Project[]
|
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 } },
|
| { type: 'assign'; data: { [key: string]: any } },
|
||||||
input: {} as {
|
input: {} as {
|
||||||
projects: Project[]
|
projects: Project[]
|
||||||
@ -30,16 +52,20 @@ export const homeMachine = setup({
|
|||||||
actions: {
|
actions: {
|
||||||
setProjects: assign({
|
setProjects: assign({
|
||||||
projects: ({ context, event }) =>
|
projects: ({ context, event }) =>
|
||||||
'output' in event ? event.output : context.projects,
|
'output' in event && isArray(event.output)
|
||||||
|
? event.output
|
||||||
|
: context.projects,
|
||||||
}),
|
}),
|
||||||
toastSuccess: () => {},
|
toastSuccess: () => {},
|
||||||
toastError: () => {},
|
toastError: () => {},
|
||||||
navigateToProject: () => {},
|
navigateToProject: () => {},
|
||||||
|
navigateToProjectIfNeeded: () => {},
|
||||||
},
|
},
|
||||||
actors: {
|
actors: {
|
||||||
readProjects: fromPromise(() => Promise.resolve([] as Project[])),
|
readProjects: fromPromise(() => Promise.resolve([] as Project[])),
|
||||||
createProject: fromPromise((_: { input: { name: string } }) =>
|
createProject: fromPromise(
|
||||||
Promise.resolve('')
|
(_: { input: { name: string; projects: Project[] } }) =>
|
||||||
|
Promise.resolve({ message: '' })
|
||||||
),
|
),
|
||||||
renameProject: fromPromise(
|
renameProject: fromPromise(
|
||||||
(_: {
|
(_: {
|
||||||
@ -48,28 +74,35 @@ export const homeMachine = setup({
|
|||||||
newName: string
|
newName: string
|
||||||
defaultProjectName: string
|
defaultProjectName: string
|
||||||
defaultDirectory: string
|
defaultDirectory: string
|
||||||
|
projects: Project[]
|
||||||
}
|
}
|
||||||
}) => Promise.resolve('')
|
}) =>
|
||||||
|
Promise.resolve({
|
||||||
|
message: '',
|
||||||
|
oldName: '',
|
||||||
|
newName: '',
|
||||||
|
})
|
||||||
),
|
),
|
||||||
deleteProject: fromPromise(
|
deleteProject: fromPromise(
|
||||||
(_: { input: { defaultDirectory: string; name: string } }) =>
|
(_: { input: { defaultDirectory: string; name: string } }) =>
|
||||||
Promise.resolve('')
|
Promise.resolve({
|
||||||
|
message: '',
|
||||||
|
name: '',
|
||||||
|
})
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
guards: {
|
guards: {
|
||||||
'Has at least 1 project': () => false,
|
'Has at least 1 project': () => false,
|
||||||
},
|
},
|
||||||
}).createMachine({
|
}).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',
|
id: 'Home machine',
|
||||||
|
|
||||||
initial: 'Reading projects',
|
initial: 'Reading projects',
|
||||||
|
|
||||||
context: {
|
context: ({ input }) => ({
|
||||||
projects: [],
|
...input,
|
||||||
defaultProjectName: '',
|
}),
|
||||||
defaultDirectory: '',
|
|
||||||
},
|
|
||||||
|
|
||||||
on: {
|
on: {
|
||||||
assign: {
|
assign: {
|
||||||
@ -110,7 +143,9 @@ export const homeMachine = setup({
|
|||||||
},
|
},
|
||||||
|
|
||||||
'Open project': {
|
'Open project': {
|
||||||
target: 'Opening project',
|
target: 'Reading projects',
|
||||||
|
actions: 'navigateToProject',
|
||||||
|
reenter: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -119,20 +154,22 @@ export const homeMachine = setup({
|
|||||||
invoke: {
|
invoke: {
|
||||||
id: 'create-project',
|
id: 'create-project',
|
||||||
src: 'createProject',
|
src: 'createProject',
|
||||||
input: ({ event }) => {
|
input: ({ event, context }) => {
|
||||||
if (event.type !== 'Create project') {
|
if (event.type !== 'Create project') {
|
||||||
return {
|
return {
|
||||||
name: '',
|
name: '',
|
||||||
|
projects: context.projects,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
name: event.data.name,
|
name: event.data.name,
|
||||||
|
projects: context.projects,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onDone: [
|
onDone: [
|
||||||
{
|
{
|
||||||
target: 'Reading projects',
|
target: 'Reading projects',
|
||||||
actions: ['toastSuccess'],
|
actions: ['toastSuccess', 'navigateToProject'],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
onError: [
|
onError: [
|
||||||
@ -156,6 +193,7 @@ export const homeMachine = setup({
|
|||||||
defaultDirectory: context.defaultDirectory,
|
defaultDirectory: context.defaultDirectory,
|
||||||
oldName: '',
|
oldName: '',
|
||||||
newName: '',
|
newName: '',
|
||||||
|
projects: context.projects,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
@ -163,12 +201,13 @@ export const homeMachine = setup({
|
|||||||
defaultDirectory: context.defaultDirectory,
|
defaultDirectory: context.defaultDirectory,
|
||||||
oldName: event.data.oldName,
|
oldName: event.data.oldName,
|
||||||
newName: event.data.newName,
|
newName: event.data.newName,
|
||||||
|
projects: context.projects,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onDone: [
|
onDone: [
|
||||||
{
|
{
|
||||||
target: '#Home machine.Reading projects',
|
target: '#Home machine.Reading projects',
|
||||||
actions: ['toastSuccess'],
|
actions: ['toastSuccess', 'navigateToProjectIfNeeded'],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
onError: [
|
onError: [
|
||||||
@ -199,7 +238,7 @@ export const homeMachine = setup({
|
|||||||
},
|
},
|
||||||
onDone: [
|
onDone: [
|
||||||
{
|
{
|
||||||
actions: ['toastSuccess'],
|
actions: ['toastSuccess', 'navigateToProjectIfNeeded'],
|
||||||
target: '#Home machine.Reading projects',
|
target: '#Home machine.Reading projects',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@ -233,9 +272,5 @@ export const homeMachine = setup({
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
'Opening project': {
|
|
||||||
entry: ['navigateToProject'],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
})
|
})
|
@ -1,60 +1,42 @@
|
|||||||
import { FormEvent, useEffect, useRef, useState } from 'react'
|
import { FormEvent, useEffect, useRef, useState } from 'react'
|
||||||
import {
|
|
||||||
getNextProjectIndex,
|
|
||||||
interpolateProjectNameWithIndex,
|
|
||||||
doesProjectNameNeedInterpolated,
|
|
||||||
} from 'lib/desktopFS'
|
|
||||||
import { ActionButton } from 'components/ActionButton'
|
import { ActionButton } from 'components/ActionButton'
|
||||||
import { toast } from 'react-hot-toast'
|
|
||||||
import { AppHeader } from 'components/AppHeader'
|
import { AppHeader } from 'components/AppHeader'
|
||||||
import ProjectCard from 'components/ProjectCard/ProjectCard'
|
import ProjectCard from 'components/ProjectCard/ProjectCard'
|
||||||
import { useNavigate, useSearchParams } from 'react-router-dom'
|
import { useNavigate, useSearchParams } from 'react-router-dom'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import Loading from 'components/Loading'
|
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 { PATHS } from 'lib/paths'
|
||||||
import {
|
import {
|
||||||
getNextSearchParams,
|
getNextSearchParams,
|
||||||
getSortFunction,
|
getSortFunction,
|
||||||
getSortIcon,
|
getSortIcon,
|
||||||
} from '../lib/sorting'
|
} from '../lib/sorting'
|
||||||
import useStateMachineCommands from '../hooks/useStateMachineCommands'
|
|
||||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
|
||||||
import { homeCommandBarConfig } from 'lib/commandBarConfigs/homeCommandConfig'
|
|
||||||
import { useHotkeys } from 'react-hotkeys-hook'
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
import { isDesktop } from 'lib/isDesktop'
|
import { isDesktop } from 'lib/isDesktop'
|
||||||
import { kclManager } from 'lib/singletons'
|
import { kclManager } from 'lib/singletons'
|
||||||
import { useLspContext } from 'components/LspProvider'
|
|
||||||
import { useRefreshSettings } from 'hooks/useRefreshSettings'
|
import { useRefreshSettings } from 'hooks/useRefreshSettings'
|
||||||
import { LowerRightControls } from 'components/LowerRightControls'
|
import { LowerRightControls } from 'components/LowerRightControls'
|
||||||
import {
|
|
||||||
createNewProjectDirectory,
|
|
||||||
listProjects,
|
|
||||||
renameProjectDirectory,
|
|
||||||
} from 'lib/desktop'
|
|
||||||
import { ProjectSearchBar, useProjectSearch } from 'components/ProjectSearchBar'
|
import { ProjectSearchBar, useProjectSearch } from 'components/ProjectSearchBar'
|
||||||
import { Project } from 'lib/project'
|
import { Project } from 'lib/project'
|
||||||
import { useFileSystemWatcher } from 'hooks/useFileSystemWatcher'
|
import { useFileSystemWatcher } from 'hooks/useFileSystemWatcher'
|
||||||
import { useProjectsLoader } from 'hooks/useProjectsLoader'
|
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,
|
// This route only opens in the desktop context for now,
|
||||||
// as defined in Router.tsx, so we can use the desktop APIs and types.
|
// as defined in Router.tsx, so we can use the desktop APIs and types.
|
||||||
const Home = () => {
|
const Home = () => {
|
||||||
|
const { state, send } = useProjectsContext()
|
||||||
|
const { commandBarSend } = useCommandsContext()
|
||||||
const [projectsLoaderTrigger, setProjectsLoaderTrigger] = useState(0)
|
const [projectsLoaderTrigger, setProjectsLoaderTrigger] = useState(0)
|
||||||
const { projectPaths, projectsDir } = useProjectsLoader([
|
const { projectsDir } = useProjectsLoader([projectsLoaderTrigger])
|
||||||
projectsLoaderTrigger,
|
|
||||||
])
|
|
||||||
|
|
||||||
useRefreshSettings(PATHS.HOME + 'SETTINGS')
|
useRefreshSettings(PATHS.HOME + 'SETTINGS')
|
||||||
const { commandBarSend } = useCommandsContext()
|
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const {
|
const {
|
||||||
settings: { context: settings },
|
settings: { context: settings },
|
||||||
} = useSettingsAuthContext()
|
} = useSettingsAuthContext()
|
||||||
const { onProjectOpen } = useLspContext()
|
|
||||||
|
|
||||||
// Cancel all KCL executions while on the home page
|
// Cancel all KCL executions while on the home page
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -73,107 +55,6 @@ const Home = () => {
|
|||||||
)
|
)
|
||||||
const ref = useRef<HTMLDivElement>(null)
|
const ref = useRef<HTMLDivElement>(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.
|
// Re-read projects listing if the projectDir has any updates.
|
||||||
useFileSystemWatcher(
|
useFileSystemWatcher(
|
||||||
async () => {
|
async () => {
|
||||||
@ -182,21 +63,13 @@ const Home = () => {
|
|||||||
projectsDir ? [projectsDir] : []
|
projectsDir ? [projectsDir] : []
|
||||||
)
|
)
|
||||||
|
|
||||||
const { projects } = state.context
|
const projects = state?.context.projects ?? []
|
||||||
const [searchParams, setSearchParams] = useSearchParams()
|
const [searchParams, setSearchParams] = useSearchParams()
|
||||||
const { searchResults, query, setQuery } = useProjectSearch(projects)
|
const { searchResults, query, setQuery } = useProjectSearch(projects)
|
||||||
const sort = searchParams.get('sort_by') ?? 'modified:desc'
|
const sort = searchParams.get('sort_by') ?? 'modified:desc'
|
||||||
|
|
||||||
const isSortByModified = sort?.includes('modified') || !sort || sort === null
|
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
|
// Update the default project name and directory in the home machine
|
||||||
// when the settings change
|
// when the settings change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -247,7 +120,16 @@ const Home = () => {
|
|||||||
<ActionButton
|
<ActionButton
|
||||||
Element="button"
|
Element="button"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
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"
|
className="group !bg-primary !text-chalkboard-10 !border-primary hover:shadow-inner hover:hue-rotate-15"
|
||||||
iconStart={{
|
iconStart={{
|
||||||
@ -326,7 +208,7 @@ const Home = () => {
|
|||||||
data-testid="home-section"
|
data-testid="home-section"
|
||||||
className="flex-1 overflow-y-auto pr-2 pb-24"
|
className="flex-1 overflow-y-auto pr-2 pb-24"
|
||||||
>
|
>
|
||||||
{state.matches('Reading projects') ? (
|
{state?.matches('Reading projects') ? (
|
||||||
<Loading>Loading your Projects...</Loading>
|
<Loading>Loading your Projects...</Loading>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|