Fix existing: file renaming (and more things that spin out of settings file path parsing) (#3584)
* Fix the behavior so that we navigate to the new file path * This change is done in other PRs but is also necessary here * Add an Electron Playwright test for renaming a file * Add tests for renaming dir, one is failing * Don't need that console.warn * Add DeepPartial utility type * Fix settings parsing so that project path parsing is fixed * Move URL check after DOM checks * Revert this fallback behavior from https://github.com/KittyCAD/modeling-app/pull/3564 as we don't need it now that config parsing is fixed * Make new bad prompt each run * Fix onboarding asset path in web * Remove double parsing of settings config * Remove unused imports * More unused imports * Fix broken rename test * Update src/lib/desktop.ts Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch> * Add test for renaming file we do not have open * fmt --------- Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch>
This commit is contained in:
@ -10,6 +10,7 @@ import {
|
||||
import fsp from 'fs/promises'
|
||||
import fs from 'fs'
|
||||
import { join } from 'path'
|
||||
import { FILE_EXT } from 'lib/constants'
|
||||
|
||||
test.afterEach(async ({ page }, testInfo) => {
|
||||
await tearDown(page, testInfo)
|
||||
@ -1337,3 +1338,369 @@ test(
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
test.describe('Renaming in the file tree', () => {
|
||||
test(
|
||||
'A file you have open',
|
||||
{ tag: '@electron' },
|
||||
async ({ browser: _ }, testInfo) => {
|
||||
test.skip(
|
||||
process.platform === 'win32',
|
||||
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
|
||||
)
|
||||
const { electronApp, page } = await setupElectron({
|
||||
testInfo,
|
||||
folderSetupFn: async (dir) => {
|
||||
await fsp.mkdir(join(dir, 'Test Project'), { recursive: true })
|
||||
const exampleDir = join(
|
||||
'src',
|
||||
'wasm-lib',
|
||||
'tests',
|
||||
'executor',
|
||||
'inputs'
|
||||
)
|
||||
await fsp.copyFile(
|
||||
join(exampleDir, 'basic_fillet_cube_end.kcl'),
|
||||
join(dir, 'Test Project', 'main.kcl')
|
||||
)
|
||||
await fsp.copyFile(
|
||||
join(exampleDir, 'cylinder.kcl'),
|
||||
join(dir, 'Test Project', 'fileToRename.kcl')
|
||||
)
|
||||
},
|
||||
})
|
||||
const u = await getUtils(page)
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
page.on('console', console.log)
|
||||
|
||||
// Constants and locators
|
||||
const projectLink = page.getByText('Test Project')
|
||||
const projectMenuButton = page.getByTestId('project-sidebar-toggle')
|
||||
const fileToRename = page
|
||||
.getByRole('listitem')
|
||||
.filter({ has: page.getByRole('button', { name: 'fileToRename.kcl' }) })
|
||||
const renamedFile = page
|
||||
.getByRole('listitem')
|
||||
.filter({ has: page.getByRole('button', { name: 'newFileName.kcl' }) })
|
||||
const renameMenuItem = page.getByRole('button', { name: 'Rename' })
|
||||
const renameInput = page.getByPlaceholder('fileToRename.kcl')
|
||||
const newFileName = 'newFileName'
|
||||
const codeLocator = page.locator('.cm-content')
|
||||
|
||||
await test.step('Open project and file pane', async () => {
|
||||
await expect(projectLink).toBeVisible()
|
||||
await projectLink.click()
|
||||
await expect(projectMenuButton).toBeVisible()
|
||||
await expect(projectMenuButton).toContainText('main.kcl')
|
||||
|
||||
await u.openFilePanel()
|
||||
await expect(fileToRename).toBeVisible()
|
||||
await fileToRename.click()
|
||||
await expect(projectMenuButton).toContainText('fileToRename.kcl')
|
||||
await u.openKclCodePanel()
|
||||
await expect(codeLocator).toContainText('circle(')
|
||||
await u.closeKclCodePanel()
|
||||
})
|
||||
|
||||
await test.step('Rename the file', async () => {
|
||||
await fileToRename.click({ button: 'right' })
|
||||
await renameMenuItem.click()
|
||||
await expect(renameInput).toBeVisible()
|
||||
await renameInput.fill(newFileName)
|
||||
await page.keyboard.press('Enter')
|
||||
})
|
||||
|
||||
await test.step('Verify the file is renamed', async () => {
|
||||
await expect(fileToRename).not.toBeAttached()
|
||||
await expect(renamedFile).toBeVisible()
|
||||
})
|
||||
|
||||
await test.step('Verify we navigated', async () => {
|
||||
await expect(projectMenuButton).toContainText(newFileName + FILE_EXT)
|
||||
const url = page.url()
|
||||
expect(url).toContain(newFileName)
|
||||
await expect(projectMenuButton).not.toContainText('fileToRename.kcl')
|
||||
await expect(projectMenuButton).not.toContainText('main.kcl')
|
||||
expect(url).not.toContain('fileToRename.kcl')
|
||||
expect(url).not.toContain('main.kcl')
|
||||
|
||||
await u.openKclCodePanel()
|
||||
await expect(codeLocator).toContainText('circle(')
|
||||
})
|
||||
|
||||
await electronApp.close()
|
||||
}
|
||||
)
|
||||
|
||||
test(
|
||||
'A file you do not have open',
|
||||
{ tag: '@electron' },
|
||||
async ({ browser: _ }, testInfo) => {
|
||||
test.skip(
|
||||
process.platform === 'win32',
|
||||
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
|
||||
)
|
||||
const { electronApp, page } = await setupElectron({
|
||||
testInfo,
|
||||
folderSetupFn: async (dir) => {
|
||||
await fsp.mkdir(join(dir, 'Test Project'), { recursive: true })
|
||||
const exampleDir = join(
|
||||
'src',
|
||||
'wasm-lib',
|
||||
'tests',
|
||||
'executor',
|
||||
'inputs'
|
||||
)
|
||||
await fsp.copyFile(
|
||||
join(exampleDir, 'basic_fillet_cube_end.kcl'),
|
||||
join(dir, 'Test Project', 'main.kcl')
|
||||
)
|
||||
await fsp.copyFile(
|
||||
join(exampleDir, 'cylinder.kcl'),
|
||||
join(dir, 'Test Project', 'fileToRename.kcl')
|
||||
)
|
||||
},
|
||||
})
|
||||
const u = await getUtils(page)
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
page.on('console', console.log)
|
||||
|
||||
// Constants and locators
|
||||
const newFileName = 'newFileName'
|
||||
const projectLink = page.getByText('Test Project')
|
||||
const projectMenuButton = page.getByTestId('project-sidebar-toggle')
|
||||
const fileToRename = page
|
||||
.getByRole('listitem')
|
||||
.filter({ has: page.getByRole('button', { name: 'fileToRename.kcl' }) })
|
||||
const renamedFile = page.getByRole('listitem').filter({
|
||||
has: page.getByRole('button', { name: newFileName + FILE_EXT }),
|
||||
})
|
||||
const renameMenuItem = page.getByRole('button', { name: 'Rename' })
|
||||
const renameInput = page.getByPlaceholder('fileToRename.kcl')
|
||||
const codeLocator = page.locator('.cm-content')
|
||||
|
||||
await test.step('Open project and file pane', async () => {
|
||||
await expect(projectLink).toBeVisible()
|
||||
await projectLink.click()
|
||||
await expect(projectMenuButton).toBeVisible()
|
||||
await expect(projectMenuButton).toContainText('main.kcl')
|
||||
|
||||
await u.openFilePanel()
|
||||
await expect(fileToRename).toBeVisible()
|
||||
})
|
||||
|
||||
await test.step('Rename the file', async () => {
|
||||
await fileToRename.click({ button: 'right' })
|
||||
await renameMenuItem.click()
|
||||
await expect(renameInput).toBeVisible()
|
||||
await renameInput.fill(newFileName)
|
||||
await page.keyboard.press('Enter')
|
||||
})
|
||||
|
||||
await test.step('Verify the file is renamed', async () => {
|
||||
await expect(fileToRename).not.toBeAttached()
|
||||
await expect(renamedFile).toBeVisible()
|
||||
})
|
||||
|
||||
await test.step('Verify we have not navigated', async () => {
|
||||
await expect(projectMenuButton).toContainText('main.kcl')
|
||||
await expect(projectMenuButton).not.toContainText(
|
||||
newFileName + FILE_EXT
|
||||
)
|
||||
await expect(projectMenuButton).not.toContainText('fileToRename.kcl')
|
||||
|
||||
const url = page.url()
|
||||
expect(url).toContain('main.kcl')
|
||||
expect(url).not.toContain(newFileName)
|
||||
expect(url).not.toContain('fileToRename.kcl')
|
||||
|
||||
await u.openKclCodePanel()
|
||||
await expect(codeLocator).toContainText('fillet(')
|
||||
})
|
||||
|
||||
await electronApp.close()
|
||||
}
|
||||
)
|
||||
|
||||
test(
|
||||
`A folder you're not inside`,
|
||||
{ tag: '@electron' },
|
||||
async ({ browser: _ }, testInfo) => {
|
||||
test.skip(
|
||||
process.platform === 'win32',
|
||||
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
|
||||
)
|
||||
const { electronApp, page } = await setupElectron({
|
||||
testInfo,
|
||||
folderSetupFn: async (dir) => {
|
||||
await fsp.mkdir(join(dir, 'Test Project'), { recursive: true })
|
||||
await fsp.mkdir(join(dir, 'Test Project', 'folderToRename'), {
|
||||
recursive: true,
|
||||
})
|
||||
const exampleDir = join(
|
||||
'src',
|
||||
'wasm-lib',
|
||||
'tests',
|
||||
'executor',
|
||||
'inputs'
|
||||
)
|
||||
await fsp.copyFile(
|
||||
join(exampleDir, 'basic_fillet_cube_end.kcl'),
|
||||
join(dir, 'Test Project', 'main.kcl')
|
||||
)
|
||||
await fsp.copyFile(
|
||||
join(exampleDir, 'cylinder.kcl'),
|
||||
join(dir, 'Test Project', 'folderToRename', 'someFileWithin.kcl')
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
const u = await getUtils(page)
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
page.on('console', console.log)
|
||||
|
||||
// Constants and locators
|
||||
const projectLink = page.getByText('Test Project')
|
||||
const projectMenuButton = page.getByTestId('project-sidebar-toggle')
|
||||
const folderToRename = page.getByRole('button', {
|
||||
name: 'folderToRename',
|
||||
})
|
||||
const renamedFolder = page.getByRole('button', { name: 'newFolderName' })
|
||||
const renameMenuItem = page.getByRole('button', { name: 'Rename' })
|
||||
const renameInput = page.getByPlaceholder('folderToRename')
|
||||
const newFolderName = 'newFolderName'
|
||||
|
||||
await test.step('Open project and file pane', async () => {
|
||||
await expect(projectLink).toBeVisible()
|
||||
await projectLink.click()
|
||||
await expect(projectMenuButton).toBeVisible()
|
||||
await expect(projectMenuButton).toContainText('main.kcl')
|
||||
|
||||
const url = page.url()
|
||||
expect(url).toContain('main.kcl')
|
||||
expect(url).not.toContain('folderToRename')
|
||||
|
||||
await u.openFilePanel()
|
||||
await expect(folderToRename).toBeVisible()
|
||||
})
|
||||
|
||||
await test.step('Rename the folder', async () => {
|
||||
await folderToRename.click({ button: 'right' })
|
||||
await expect(renameMenuItem).toBeVisible()
|
||||
await renameMenuItem.click()
|
||||
await expect(renameInput).toBeVisible()
|
||||
await renameInput.fill(newFolderName)
|
||||
await page.keyboard.press('Enter')
|
||||
})
|
||||
|
||||
await test.step('Verify the folder is renamed, and no navigation occurred', async () => {
|
||||
const url = page.url()
|
||||
expect(url).toContain('main.kcl')
|
||||
expect(url).not.toContain('folderToRename')
|
||||
|
||||
await expect(projectMenuButton).toContainText('main.kcl')
|
||||
await expect(renamedFolder).toBeVisible()
|
||||
await expect(folderToRename).not.toBeAttached()
|
||||
})
|
||||
|
||||
await electronApp.close()
|
||||
}
|
||||
)
|
||||
|
||||
test(
|
||||
`A folder you are inside`,
|
||||
{ tag: '@electron' },
|
||||
async ({ browser: _ }, testInfo) => {
|
||||
test.skip(
|
||||
process.platform === 'win32',
|
||||
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
|
||||
)
|
||||
const exampleDir = join('src', 'wasm-lib', 'tests', 'executor', 'inputs')
|
||||
const { electronApp, page } = await setupElectron({
|
||||
testInfo,
|
||||
folderSetupFn: async (dir) => {
|
||||
await fsp.mkdir(join(dir, 'Test Project'), { recursive: true })
|
||||
await fsp.mkdir(join(dir, 'Test Project', 'folderToRename'), {
|
||||
recursive: true,
|
||||
})
|
||||
await fsp.copyFile(
|
||||
join(exampleDir, 'basic_fillet_cube_end.kcl'),
|
||||
join(dir, 'Test Project', 'main.kcl')
|
||||
)
|
||||
await fsp.copyFile(
|
||||
join(exampleDir, 'cylinder.kcl'),
|
||||
join(dir, 'Test Project', 'folderToRename', 'someFileWithin.kcl')
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
const u = await getUtils(page)
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
page.on('console', console.log)
|
||||
|
||||
// Constants and locators
|
||||
const projectLink = page.getByText('Test Project')
|
||||
const projectMenuButton = page.getByTestId('project-sidebar-toggle')
|
||||
const folderToRename = page.getByRole('button', {
|
||||
name: 'folderToRename',
|
||||
})
|
||||
const renamedFolder = page.getByRole('button', { name: 'newFolderName' })
|
||||
const fileWithinFolder = page.getByRole('listitem').filter({
|
||||
has: page.getByRole('button', { name: 'someFileWithin.kcl' }),
|
||||
})
|
||||
const renameMenuItem = page.getByRole('button', { name: 'Rename' })
|
||||
const renameInput = page.getByPlaceholder('folderToRename')
|
||||
const newFolderName = 'newFolderName'
|
||||
|
||||
await test.step('Open project and navigate into folder', async () => {
|
||||
await expect(projectLink).toBeVisible()
|
||||
await projectLink.click()
|
||||
await expect(projectMenuButton).toBeVisible()
|
||||
await expect(projectMenuButton).toContainText('main.kcl')
|
||||
|
||||
const url = page.url()
|
||||
expect(url).toContain('main.kcl')
|
||||
expect(url).not.toContain('folderToRename')
|
||||
|
||||
await u.openFilePanel()
|
||||
await expect(folderToRename).toBeVisible()
|
||||
await folderToRename.click()
|
||||
await expect(fileWithinFolder).toBeVisible()
|
||||
await fileWithinFolder.click()
|
||||
|
||||
await expect(projectMenuButton).toContainText('someFileWithin.kcl')
|
||||
const newUrl = page.url()
|
||||
expect(newUrl).toContain('folderToRename')
|
||||
expect(newUrl).toContain('someFileWithin.kcl')
|
||||
expect(newUrl).not.toContain('main.kcl')
|
||||
})
|
||||
|
||||
await test.step('Rename the folder', async () => {
|
||||
await folderToRename.click({ button: 'right' })
|
||||
await expect(renameMenuItem).toBeVisible()
|
||||
await renameMenuItem.click()
|
||||
await expect(renameInput).toBeVisible()
|
||||
await renameInput.fill(newFolderName)
|
||||
await page.keyboard.press('Enter')
|
||||
})
|
||||
|
||||
await test.step('Verify the folder is renamed, and navigated to new path', async () => {
|
||||
const urlSnippet = encodeURIComponent(
|
||||
join(newFolderName, 'someFileWithin.kcl')
|
||||
)
|
||||
await page.waitForURL(new RegExp(urlSnippet))
|
||||
await expect(projectMenuButton).toContainText('someFileWithin.kcl')
|
||||
await expect(renamedFolder).toBeVisible()
|
||||
await expect(folderToRename).not.toBeAttached()
|
||||
|
||||
// URL is synchronous, so we check the other stuff first
|
||||
const url = page.url()
|
||||
expect(url).not.toContain('main.kcl')
|
||||
expect(url).toContain(newFolderName)
|
||||
expect(url).toContain('someFileWithin.kcl')
|
||||
})
|
||||
|
||||
await electronApp.close()
|
||||
}
|
||||
)
|
||||
})
|
||||
|
@ -190,7 +190,8 @@ test.describe('Text-to-CAD tests', () => {
|
||||
await expect(prompt.first()).toBeVisible()
|
||||
|
||||
// Type the prompt.
|
||||
await page.keyboard.type('akjsndladf ghgsssswefiuwq22262664')
|
||||
const randomPrompt = `aslkdfja;` + Date.now() + `FFFFEIWJF`
|
||||
await page.keyboard.type(randomPrompt)
|
||||
await page.waitForTimeout(1000)
|
||||
await page.keyboard.press('Enter')
|
||||
|
||||
|
@ -153,33 +153,34 @@ export const FileMachineProvider = ({
|
||||
event: EventFrom<typeof fileMachine, 'Rename file'>
|
||||
) => {
|
||||
const { oldName, newName, isDir } = event.data
|
||||
const name = newName ? newName : DEFAULT_FILE_NAME
|
||||
const name = newName
|
||||
? newName.endsWith(FILE_EXT) || isDir
|
||||
? newName
|
||||
: newName + FILE_EXT
|
||||
: DEFAULT_FILE_NAME
|
||||
const oldPath = window.electron.path.join(
|
||||
context.selectedDirectory.path,
|
||||
oldName
|
||||
)
|
||||
const newDirPath = window.electron.path.join(
|
||||
const newPath = window.electron.path.join(
|
||||
context.selectedDirectory.path,
|
||||
name
|
||||
)
|
||||
const newPath =
|
||||
newDirPath + (name.endsWith(FILE_EXT) || isDir ? '' : FILE_EXT)
|
||||
|
||||
await window.electron.rename(oldPath, newPath)
|
||||
window.electron.rename(oldPath, newPath)
|
||||
|
||||
if (!file) {
|
||||
return Promise.reject(new Error('file is not defined'))
|
||||
}
|
||||
|
||||
const currentFilePath = window.electron.path.join(file.path, file.name)
|
||||
if (oldPath === currentFilePath && project?.path) {
|
||||
if (oldPath === file.path && project?.path) {
|
||||
// If we just renamed the current file, navigate to the new path
|
||||
navigate(`..${PATHS.FILE}/${encodeURIComponent(newPath)}`)
|
||||
} else if (file?.path.includes(oldPath)) {
|
||||
// If we just renamed a directory that the current file is in, navigate to the new path
|
||||
navigate(
|
||||
`..${PATHS.FILE}/${encodeURIComponent(
|
||||
file.path.replace(oldPath, newDirPath)
|
||||
file.path.replace(oldPath, newPath)
|
||||
)}`
|
||||
)
|
||||
}
|
||||
|
@ -19,11 +19,6 @@ import init, {
|
||||
parse_project_route,
|
||||
base64_decode,
|
||||
} from '../wasm-lib/pkg/wasm_lib'
|
||||
import {
|
||||
configurationToSettingsPayload,
|
||||
projectConfigurationToSettingsPayload,
|
||||
} from 'lib/settings/settingsUtils'
|
||||
import { SaveSettingsPayload } from 'lib/settings/settingsTypes'
|
||||
import { KCLError } from './errors'
|
||||
import { KclError as RustKclError } from '../wasm-lib/kcl/bindings/KclError'
|
||||
import { EngineCommandManager } from './std/engineConnection'
|
||||
@ -40,6 +35,9 @@ import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes'
|
||||
import { TEST } from 'env'
|
||||
import { ProjectRoute } from 'wasm-lib/kcl/bindings/ProjectRoute'
|
||||
import { err } from 'lib/trap'
|
||||
import { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
|
||||
import { DeepPartial } from 'lib/types'
|
||||
import { ProjectConfiguration } from 'wasm-lib/kcl/bindings/ProjectConfiguration'
|
||||
|
||||
export type { Program } from '../wasm-lib/kcl/bindings/Program'
|
||||
export type { Expr } from '../wasm-lib/kcl/bindings/Expr'
|
||||
@ -570,31 +568,30 @@ export function tomlStringify(toml: any): string | Error {
|
||||
return toml_stringify(JSON.stringify(toml))
|
||||
}
|
||||
|
||||
export function defaultAppSettings(): Partial<SaveSettingsPayload> {
|
||||
// Immediately go from Configuration -> Partial<SaveSettingsPayload>
|
||||
// The returned Rust type is Configuration but it's a lie. Every
|
||||
// property in that returned object is optional. The Partial<T> essentially
|
||||
// brings that type in-line with that definition.
|
||||
return configurationToSettingsPayload(default_app_settings())
|
||||
export function defaultAppSettings(): DeepPartial<Configuration> | Error {
|
||||
return default_app_settings()
|
||||
}
|
||||
|
||||
export function parseAppSettings(toml: string): Partial<SaveSettingsPayload> {
|
||||
const parsed = parse_app_settings(toml)
|
||||
return configurationToSettingsPayload(parsed)
|
||||
export function parseAppSettings(
|
||||
toml: string
|
||||
): DeepPartial<Configuration> | Error {
|
||||
return parse_app_settings(toml)
|
||||
}
|
||||
|
||||
export function defaultProjectSettings(): Partial<SaveSettingsPayload> {
|
||||
return projectConfigurationToSettingsPayload(default_project_settings())
|
||||
export function defaultProjectSettings():
|
||||
| DeepPartial<ProjectConfiguration>
|
||||
| Error {
|
||||
return default_project_settings()
|
||||
}
|
||||
|
||||
export function parseProjectSettings(
|
||||
toml: string
|
||||
): Partial<SaveSettingsPayload> {
|
||||
return projectConfigurationToSettingsPayload(parse_project_settings(toml))
|
||||
): DeepPartial<ProjectConfiguration> | Error {
|
||||
return parse_project_settings(toml)
|
||||
}
|
||||
|
||||
export function parseProjectRoute(
|
||||
configuration: Partial<SaveSettingsPayload>,
|
||||
configuration: DeepPartial<Configuration>,
|
||||
route_str: string
|
||||
): ProjectRoute | Error {
|
||||
return parse_project_route(JSON.stringify(configuration), route_str)
|
||||
|
@ -3,11 +3,9 @@ import { Models } from '@kittycad/lib'
|
||||
import { Project } from 'wasm-lib/kcl/bindings/Project'
|
||||
import { ProjectState } from 'wasm-lib/kcl/bindings/ProjectState'
|
||||
import { FileEntry } from 'wasm-lib/kcl/bindings/FileEntry'
|
||||
import { SaveSettingsPayload } from 'lib/settings/settingsTypes'
|
||||
|
||||
import {
|
||||
defaultAppSettings,
|
||||
tomlStringify,
|
||||
parseAppSettings,
|
||||
parseProjectSettings,
|
||||
} from 'lang/wasm'
|
||||
@ -18,6 +16,9 @@ import {
|
||||
PROJECT_SETTINGS_FILE_NAME,
|
||||
SETTINGS_FILE_NAME,
|
||||
} from './constants'
|
||||
import { DeepPartial } from './types'
|
||||
import { ProjectConfiguration } from 'wasm-lib/kcl/bindings/ProjectConfiguration'
|
||||
import { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
|
||||
export { parseProjectRoute } from 'lang/wasm'
|
||||
|
||||
export async function renameProjectDirectory(
|
||||
@ -61,10 +62,13 @@ export async function renameProjectDirectory(
|
||||
}
|
||||
|
||||
export async function ensureProjectDirectoryExists(
|
||||
config: Partial<SaveSettingsPayload>
|
||||
config: DeepPartial<Configuration>
|
||||
): Promise<string | undefined> {
|
||||
const projectDir = config.app?.projectDirectory
|
||||
const projectDir =
|
||||
config.settings?.app?.project_directory ||
|
||||
config.settings?.project?.directory
|
||||
if (!projectDir) {
|
||||
console.error('projectDir is falsey', config)
|
||||
return Promise.reject(new Error('projectDir is falsey'))
|
||||
}
|
||||
try {
|
||||
@ -81,12 +85,13 @@ export async function ensureProjectDirectoryExists(
|
||||
export async function createNewProjectDirectory(
|
||||
projectName: string,
|
||||
initialCode?: string,
|
||||
configuration?: Partial<SaveSettingsPayload>
|
||||
configuration?: DeepPartial<Configuration> | Error
|
||||
): Promise<Project> {
|
||||
if (!configuration) {
|
||||
configuration = await readAppSettingsFile()
|
||||
}
|
||||
|
||||
if (err(configuration)) return Promise.reject(configuration)
|
||||
const mainDir = await ensureProjectDirectoryExists(configuration)
|
||||
|
||||
if (!projectName) {
|
||||
@ -124,11 +129,13 @@ export async function createNewProjectDirectory(
|
||||
}
|
||||
|
||||
export async function listProjects(
|
||||
configuration?: Partial<SaveSettingsPayload>
|
||||
configuration?: DeepPartial<Configuration> | Error
|
||||
): Promise<Project[]> {
|
||||
if (configuration === undefined) {
|
||||
configuration = await readAppSettingsFile()
|
||||
}
|
||||
|
||||
if (err(configuration)) return Promise.reject(configuration)
|
||||
const projectDir = await ensureProjectDirectoryExists(configuration)
|
||||
const projects = []
|
||||
if (!projectDir) return Promise.reject(new Error('projectDir was falsey'))
|
||||
@ -179,7 +186,7 @@ const collectAllFilesRecursiveFrom = async (path: string) => {
|
||||
return Promise.reject(new Error(`Path ${path} is not a directory`))
|
||||
}
|
||||
|
||||
const pathParts = path.split('/')
|
||||
const pathParts = path.split(window.electron.path.sep)
|
||||
let entry: FileEntry = {
|
||||
name: pathParts.slice(-1)[0],
|
||||
path,
|
||||
@ -358,10 +365,9 @@ export async function getProjectInfo(projectPath: string): Promise<Project> {
|
||||
// Write project settings file.
|
||||
export async function writeProjectSettingsFile(
|
||||
projectPath: string,
|
||||
configuration: Partial<SaveSettingsPayload>
|
||||
tomlStr: string
|
||||
): Promise<void> {
|
||||
const projectSettingsFilePath = await getProjectSettingsFilePath(projectPath)
|
||||
const tomlStr = tomlStringify({ settings: configuration })
|
||||
if (err(tomlStr)) return Promise.reject(tomlStr)
|
||||
return window.electron.writeFile(projectSettingsFilePath, tomlStr)
|
||||
}
|
||||
@ -416,7 +422,7 @@ export const getInitialDefaultDir = async () => {
|
||||
|
||||
export const readProjectSettingsFile = async (
|
||||
projectPath: string
|
||||
): Promise<Partial<SaveSettingsPayload>> => {
|
||||
): Promise<DeepPartial<ProjectConfiguration>> => {
|
||||
let settingsPath = await getProjectSettingsFilePath(projectPath)
|
||||
|
||||
// Check if this file exists.
|
||||
@ -431,6 +437,9 @@ export const readProjectSettingsFile = async (
|
||||
|
||||
const configToml = await window.electron.readFile(settingsPath)
|
||||
const configObj = parseProjectSettings(configToml)
|
||||
if (err(configObj)) {
|
||||
return Promise.reject(configObj)
|
||||
}
|
||||
return configObj
|
||||
}
|
||||
|
||||
@ -441,29 +450,25 @@ export const readAppSettingsFile = async () => {
|
||||
} catch (e) {
|
||||
if (e === 'ENOENT') {
|
||||
const config = defaultAppSettings()
|
||||
if (!config.app) {
|
||||
if (err(config)) return Promise.reject(config)
|
||||
if (!config.settings?.app)
|
||||
return Promise.reject(new Error('config.app is falsey'))
|
||||
}
|
||||
config.app.projectDirectory = await getInitialDefaultDir()
|
||||
|
||||
config.settings.app.project_directory = await getInitialDefaultDir()
|
||||
return config
|
||||
}
|
||||
}
|
||||
const configToml = await window.electron.readFile(settingsPath)
|
||||
const configObj = parseAppSettings(configToml)
|
||||
if (!configObj.app) {
|
||||
return Promise.reject(new Error('config.app is falsey'))
|
||||
}
|
||||
if (!configObj.app.projectDirectory) {
|
||||
configObj.app.projectDirectory = await getInitialDefaultDir()
|
||||
if (err(configObj)) {
|
||||
return Promise.reject(configObj)
|
||||
}
|
||||
|
||||
return configObj
|
||||
}
|
||||
|
||||
export const writeAppSettingsFile = async (
|
||||
config: Partial<SaveSettingsPayload>
|
||||
) => {
|
||||
export const writeAppSettingsFile = async (tomlStr: string) => {
|
||||
const appSettingsFilePath = await getAppSettingsFilePath()
|
||||
const tomlStr = tomlStringify({ settings: config })
|
||||
if (err(tomlStr)) return Promise.reject(tomlStr)
|
||||
return window.electron.writeFile(appSettingsFilePath, tomlStr)
|
||||
}
|
||||
|
@ -4,9 +4,10 @@ import { isDesktop } from './isDesktop'
|
||||
import { ProjectRoute } from 'wasm-lib/kcl/bindings/ProjectRoute'
|
||||
import { parseProjectRoute, readAppSettingsFile } from './desktop'
|
||||
import { readLocalStorageAppSettingsFile } from './settings/settingsUtils'
|
||||
import { SaveSettingsPayload } from './settings/settingsTypes'
|
||||
import { err } from 'lib/trap'
|
||||
import { IS_PLAYWRIGHT_KEY } from '../../e2e/playwright/storageStates'
|
||||
import { DeepPartial } from './types'
|
||||
import { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
|
||||
|
||||
const prependRoutes =
|
||||
(routesObject: Record<string, string>) => (prepend: string) => {
|
||||
@ -39,7 +40,7 @@ export const BROWSER_PATH = `%2F${BROWSER_PROJECT_NAME}%2F${BROWSER_FILE_NAME}${
|
||||
|
||||
export async function getProjectMetaByRouteId(
|
||||
id?: string,
|
||||
configuration?: Partial<SaveSettingsPayload> | Error
|
||||
configuration?: DeepPartial<Configuration> | Error
|
||||
): Promise<ProjectRoute | undefined> {
|
||||
if (!id) return undefined
|
||||
|
||||
|
@ -10,7 +10,7 @@ import {
|
||||
} from 'lib/constants'
|
||||
import { loadAndValidateSettings } from './settings/settingsUtils'
|
||||
import makeUrlPathRelative from './makeUrlPathRelative'
|
||||
import { codeManager, kclManager } from 'lib/singletons'
|
||||
import { codeManager } from 'lib/singletons'
|
||||
import { fileSystemManager } from 'lang/std/fileSystemManager'
|
||||
import {
|
||||
getProjectInfo,
|
||||
@ -107,8 +107,6 @@ export const fileLoader: LoaderFunction = async (
|
||||
// the file system and not the editor.
|
||||
codeManager.updateCurrentFilePath(current_file_path)
|
||||
codeManager.updateCodeStateEditor(code)
|
||||
// We don't want to call await on execute code since we don't want to block the UI
|
||||
kclManager.executeCode(true)
|
||||
}
|
||||
|
||||
// Set the file system manager to the project path
|
||||
@ -125,14 +123,22 @@ export const fileLoader: LoaderFunction = async (
|
||||
default_file: project_path,
|
||||
}
|
||||
|
||||
const maybeProjectInfo = isDesktop()
|
||||
? await getProjectInfo(project_path)
|
||||
: null
|
||||
|
||||
console.log('maybeProjectInfo', {
|
||||
maybeProjectInfo,
|
||||
defaultProjectData,
|
||||
projectPathData,
|
||||
})
|
||||
|
||||
const projectData: IndexLoaderData = {
|
||||
code,
|
||||
project: isDesktop()
|
||||
? (await getProjectInfo(project_path)) ?? defaultProjectData
|
||||
: defaultProjectData,
|
||||
project: maybeProjectInfo ?? defaultProjectData,
|
||||
file: {
|
||||
name: current_file_name || '',
|
||||
path: current_file_path?.split('/').slice(0, -1).join('/') ?? '',
|
||||
path: current_file_path || '',
|
||||
children: [],
|
||||
},
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ import {
|
||||
} from 'lib/desktop'
|
||||
import { ProjectConfiguration } from 'wasm-lib/kcl/bindings/ProjectConfiguration'
|
||||
import { BROWSER_PROJECT_NAME } from 'lib/constants'
|
||||
import { DeepPartial } from 'lib/types'
|
||||
|
||||
/**
|
||||
* Convert from a rust settings struct into the JS settings struct.
|
||||
@ -28,8 +29,8 @@ import { BROWSER_PROJECT_NAME } from 'lib/constants'
|
||||
* for hiding and showing settings.
|
||||
**/
|
||||
export function configurationToSettingsPayload(
|
||||
configuration: Configuration
|
||||
): Partial<SaveSettingsPayload> {
|
||||
configuration: DeepPartial<Configuration>
|
||||
): DeepPartial<SaveSettingsPayload> {
|
||||
return {
|
||||
app: {
|
||||
theme: appThemeToTheme(configuration?.settings?.app?.appearance?.theme),
|
||||
@ -66,8 +67,8 @@ export function configurationToSettingsPayload(
|
||||
}
|
||||
|
||||
export function projectConfigurationToSettingsPayload(
|
||||
configuration: ProjectConfiguration
|
||||
): Partial<SaveSettingsPayload> {
|
||||
configuration: DeepPartial<ProjectConfiguration>
|
||||
): DeepPartial<SaveSettingsPayload> {
|
||||
return {
|
||||
app: {
|
||||
theme: appThemeToTheme(configuration?.settings?.app?.appearance?.theme),
|
||||
@ -106,7 +107,7 @@ function localStorageProjectSettingsPath() {
|
||||
}
|
||||
|
||||
export function readLocalStorageAppSettingsFile():
|
||||
| Partial<SaveSettingsPayload>
|
||||
| DeepPartial<Configuration>
|
||||
| Error {
|
||||
// TODO: Remove backwards compatibility after a few releases.
|
||||
let stored =
|
||||
@ -132,7 +133,7 @@ export function readLocalStorageAppSettingsFile():
|
||||
}
|
||||
|
||||
function readLocalStorageProjectSettingsFile():
|
||||
| Partial<SaveSettingsPayload>
|
||||
| DeepPartial<ProjectConfiguration>
|
||||
| Error {
|
||||
// TODO: Remove backwards compatibility after a few releases.
|
||||
let stored = localStorage.getItem(localStorageProjectSettingsPath()) ?? ''
|
||||
@ -156,7 +157,7 @@ function readLocalStorageProjectSettingsFile():
|
||||
|
||||
export interface AppSettings {
|
||||
settings: ReturnType<typeof createSettings>
|
||||
configuration: Partial<SaveSettingsPayload>
|
||||
configuration: DeepPartial<Configuration>
|
||||
}
|
||||
|
||||
export async function loadAndValidateSettings(
|
||||
@ -175,7 +176,11 @@ export async function loadAndValidateSettings(
|
||||
if (err(appSettingsPayload)) return Promise.reject(appSettingsPayload)
|
||||
|
||||
const settings = createSettings()
|
||||
setSettingsAtLevel(settings, 'user', appSettingsPayload)
|
||||
setSettingsAtLevel(
|
||||
settings,
|
||||
'user',
|
||||
configurationToSettingsPayload(appSettingsPayload)
|
||||
)
|
||||
|
||||
// Load the project settings if they exist
|
||||
if (projectPath) {
|
||||
@ -187,11 +192,18 @@ export async function loadAndValidateSettings(
|
||||
return Promise.reject(new Error('Invalid project settings'))
|
||||
|
||||
const projectSettingsPayload = projectSettings
|
||||
setSettingsAtLevel(settings, 'project', projectSettingsPayload)
|
||||
setSettingsAtLevel(
|
||||
settings,
|
||||
'project',
|
||||
projectConfigurationToSettingsPayload(projectSettingsPayload)
|
||||
)
|
||||
}
|
||||
|
||||
// Return the settings object
|
||||
return { settings, configuration: appSettingsPayload }
|
||||
return {
|
||||
settings,
|
||||
configuration: appSettingsPayload,
|
||||
}
|
||||
}
|
||||
|
||||
export async function saveSettings(
|
||||
@ -204,21 +216,14 @@ export async function saveSettings(
|
||||
|
||||
// Get the user settings.
|
||||
const jsAppSettings = getChangedSettingsAtLevel(allSettings, 'user')
|
||||
const tomlString = tomlStringify({ settings: jsAppSettings })
|
||||
if (err(tomlString)) return
|
||||
|
||||
// Parse this as a Configuration.
|
||||
const appSettings = parseAppSettings(tomlString)
|
||||
if (err(appSettings)) return
|
||||
|
||||
const tomlString2 = tomlStringify({ settings: appSettings })
|
||||
if (err(tomlString2)) return
|
||||
const appTomlString = tomlStringify({ settings: jsAppSettings })
|
||||
if (err(appTomlString)) return
|
||||
|
||||
// Write the app settings.
|
||||
if (onDesktop) {
|
||||
await writeAppSettingsFile(appSettings)
|
||||
await writeAppSettingsFile(appTomlString)
|
||||
} else {
|
||||
localStorage.setItem(localStorageAppSettingsPath(), tomlString2)
|
||||
localStorage.setItem(localStorageAppSettingsPath(), appTomlString)
|
||||
}
|
||||
|
||||
if (!projectPath) {
|
||||
@ -231,19 +236,11 @@ export async function saveSettings(
|
||||
const projectTomlString = tomlStringify({ settings: jsProjectSettings })
|
||||
if (err(projectTomlString)) return
|
||||
|
||||
// Parse this as a Configuration.
|
||||
const projectSettings = parseProjectSettings(projectTomlString)
|
||||
if (err(projectSettings)) return
|
||||
|
||||
const tomlStr = tomlStringify(projectSettings)
|
||||
|
||||
if (err(tomlStr)) return
|
||||
|
||||
// Write the project settings.
|
||||
if (onDesktop) {
|
||||
await writeProjectSettingsFile(projectPath, projectSettings)
|
||||
await writeProjectSettingsFile(projectPath, projectTomlString)
|
||||
} else {
|
||||
localStorage.setItem(localStorageProjectSettingsPath(), tomlStr)
|
||||
localStorage.setItem(localStorageProjectSettingsPath(), projectTomlString)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -95,3 +95,9 @@ export function isEnumMember<T extends Record<string, unknown>>(
|
||||
) {
|
||||
return Object.values(e).includes(v)
|
||||
}
|
||||
|
||||
// utility type to make all *nested* object properties optional
|
||||
// https://www.geodev.me/blog/deeppartial-in-typescript
|
||||
export type DeepPartial<T> = {
|
||||
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P]
|
||||
}
|
||||
|
@ -145,7 +145,7 @@ function OnboardingIntroductionInner() {
|
||||
<div className="max-w-3xl p-8 rounded bg-chalkboard-10 dark:bg-chalkboard-90">
|
||||
<h1 className="flex flex-wrap items-center gap-4 text-3xl font-bold">
|
||||
<img
|
||||
src={`./zma-logomark${getLogoTheme()}.svg`}
|
||||
src={`${isDesktop() ? '.' : ''}/zma-logomark${getLogoTheme()}.svg`}
|
||||
alt={APP_NAME}
|
||||
className="h-20 max-w-full"
|
||||
/>
|
||||
|
Reference in New Issue
Block a user