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:
Frank Noirot
2024-08-20 22:16:44 -04:00
committed by GitHub
parent d14b8f5443
commit c09775f5eb
10 changed files with 472 additions and 91 deletions

View File

@ -10,6 +10,7 @@ import {
import fsp from 'fs/promises' import fsp from 'fs/promises'
import fs from 'fs' import fs from 'fs'
import { join } from 'path' import { join } from 'path'
import { FILE_EXT } from 'lib/constants'
test.afterEach(async ({ page }, testInfo) => { test.afterEach(async ({ page }, testInfo) => {
await tearDown(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()
}
)
})

View File

@ -190,7 +190,8 @@ test.describe('Text-to-CAD tests', () => {
await expect(prompt.first()).toBeVisible() await expect(prompt.first()).toBeVisible()
// Type the prompt. // 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.waitForTimeout(1000)
await page.keyboard.press('Enter') await page.keyboard.press('Enter')

View File

@ -153,33 +153,34 @@ export const FileMachineProvider = ({
event: EventFrom<typeof fileMachine, 'Rename file'> event: EventFrom<typeof fileMachine, 'Rename file'>
) => { ) => {
const { oldName, newName, isDir } = event.data 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( const oldPath = window.electron.path.join(
context.selectedDirectory.path, context.selectedDirectory.path,
oldName oldName
) )
const newDirPath = window.electron.path.join( const newPath = window.electron.path.join(
context.selectedDirectory.path, context.selectedDirectory.path,
name name
) )
const newPath =
newDirPath + (name.endsWith(FILE_EXT) || isDir ? '' : FILE_EXT)
await window.electron.rename(oldPath, newPath) window.electron.rename(oldPath, newPath)
if (!file) { if (!file) {
return Promise.reject(new Error('file is not defined')) return Promise.reject(new Error('file is not defined'))
} }
const currentFilePath = window.electron.path.join(file.path, file.name) if (oldPath === file.path && project?.path) {
if (oldPath === currentFilePath && project?.path) {
// If we just renamed the current file, navigate to the new path // If we just renamed the current file, navigate to the new path
navigate(`..${PATHS.FILE}/${encodeURIComponent(newPath)}`) navigate(`..${PATHS.FILE}/${encodeURIComponent(newPath)}`)
} else if (file?.path.includes(oldPath)) { } else if (file?.path.includes(oldPath)) {
// If we just renamed a directory that the current file is in, navigate to the new path // If we just renamed a directory that the current file is in, navigate to the new path
navigate( navigate(
`..${PATHS.FILE}/${encodeURIComponent( `..${PATHS.FILE}/${encodeURIComponent(
file.path.replace(oldPath, newDirPath) file.path.replace(oldPath, newPath)
)}` )}`
) )
} }

View File

@ -19,11 +19,6 @@ import init, {
parse_project_route, parse_project_route,
base64_decode, base64_decode,
} from '../wasm-lib/pkg/wasm_lib' } 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 } from './errors'
import { KclError as RustKclError } from '../wasm-lib/kcl/bindings/KclError' import { KclError as RustKclError } from '../wasm-lib/kcl/bindings/KclError'
import { EngineCommandManager } from './std/engineConnection' import { EngineCommandManager } from './std/engineConnection'
@ -40,6 +35,9 @@ import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes'
import { TEST } from 'env' import { TEST } from 'env'
import { ProjectRoute } from 'wasm-lib/kcl/bindings/ProjectRoute' import { ProjectRoute } from 'wasm-lib/kcl/bindings/ProjectRoute'
import { err } from 'lib/trap' 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 { Program } from '../wasm-lib/kcl/bindings/Program'
export type { Expr } from '../wasm-lib/kcl/bindings/Expr' 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)) return toml_stringify(JSON.stringify(toml))
} }
export function defaultAppSettings(): Partial<SaveSettingsPayload> { export function defaultAppSettings(): DeepPartial<Configuration> | Error {
// Immediately go from Configuration -> Partial<SaveSettingsPayload> return default_app_settings()
// 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 parseAppSettings(toml: string): Partial<SaveSettingsPayload> { export function parseAppSettings(
const parsed = parse_app_settings(toml) toml: string
return configurationToSettingsPayload(parsed) ): DeepPartial<Configuration> | Error {
return parse_app_settings(toml)
} }
export function defaultProjectSettings(): Partial<SaveSettingsPayload> { export function defaultProjectSettings():
return projectConfigurationToSettingsPayload(default_project_settings()) | DeepPartial<ProjectConfiguration>
| Error {
return default_project_settings()
} }
export function parseProjectSettings( export function parseProjectSettings(
toml: string toml: string
): Partial<SaveSettingsPayload> { ): DeepPartial<ProjectConfiguration> | Error {
return projectConfigurationToSettingsPayload(parse_project_settings(toml)) return parse_project_settings(toml)
} }
export function parseProjectRoute( export function parseProjectRoute(
configuration: Partial<SaveSettingsPayload>, configuration: DeepPartial<Configuration>,
route_str: string route_str: string
): ProjectRoute | Error { ): ProjectRoute | Error {
return parse_project_route(JSON.stringify(configuration), route_str) return parse_project_route(JSON.stringify(configuration), route_str)

View File

@ -3,11 +3,9 @@ import { Models } from '@kittycad/lib'
import { Project } from 'wasm-lib/kcl/bindings/Project' import { Project } from 'wasm-lib/kcl/bindings/Project'
import { ProjectState } from 'wasm-lib/kcl/bindings/ProjectState' import { ProjectState } from 'wasm-lib/kcl/bindings/ProjectState'
import { FileEntry } from 'wasm-lib/kcl/bindings/FileEntry' import { FileEntry } from 'wasm-lib/kcl/bindings/FileEntry'
import { SaveSettingsPayload } from 'lib/settings/settingsTypes'
import { import {
defaultAppSettings, defaultAppSettings,
tomlStringify,
parseAppSettings, parseAppSettings,
parseProjectSettings, parseProjectSettings,
} from 'lang/wasm' } from 'lang/wasm'
@ -18,6 +16,9 @@ import {
PROJECT_SETTINGS_FILE_NAME, PROJECT_SETTINGS_FILE_NAME,
SETTINGS_FILE_NAME, SETTINGS_FILE_NAME,
} from './constants' } 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 { parseProjectRoute } from 'lang/wasm'
export async function renameProjectDirectory( export async function renameProjectDirectory(
@ -61,10 +62,13 @@ export async function renameProjectDirectory(
} }
export async function ensureProjectDirectoryExists( export async function ensureProjectDirectoryExists(
config: Partial<SaveSettingsPayload> config: DeepPartial<Configuration>
): Promise<string | undefined> { ): Promise<string | undefined> {
const projectDir = config.app?.projectDirectory const projectDir =
config.settings?.app?.project_directory ||
config.settings?.project?.directory
if (!projectDir) { if (!projectDir) {
console.error('projectDir is falsey', config)
return Promise.reject(new Error('projectDir is falsey')) return Promise.reject(new Error('projectDir is falsey'))
} }
try { try {
@ -81,12 +85,13 @@ export async function ensureProjectDirectoryExists(
export async function createNewProjectDirectory( export async function createNewProjectDirectory(
projectName: string, projectName: string,
initialCode?: string, initialCode?: string,
configuration?: Partial<SaveSettingsPayload> configuration?: DeepPartial<Configuration> | Error
): Promise<Project> { ): Promise<Project> {
if (!configuration) { if (!configuration) {
configuration = await readAppSettingsFile() configuration = await readAppSettingsFile()
} }
if (err(configuration)) return Promise.reject(configuration)
const mainDir = await ensureProjectDirectoryExists(configuration) const mainDir = await ensureProjectDirectoryExists(configuration)
if (!projectName) { if (!projectName) {
@ -124,11 +129,13 @@ export async function createNewProjectDirectory(
} }
export async function listProjects( export async function listProjects(
configuration?: Partial<SaveSettingsPayload> configuration?: DeepPartial<Configuration> | Error
): Promise<Project[]> { ): Promise<Project[]> {
if (configuration === undefined) { if (configuration === undefined) {
configuration = await readAppSettingsFile() configuration = await readAppSettingsFile()
} }
if (err(configuration)) return Promise.reject(configuration)
const projectDir = await ensureProjectDirectoryExists(configuration) const projectDir = await ensureProjectDirectoryExists(configuration)
const projects = [] const projects = []
if (!projectDir) return Promise.reject(new Error('projectDir was falsey')) 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`)) 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 = { let entry: FileEntry = {
name: pathParts.slice(-1)[0], name: pathParts.slice(-1)[0],
path, path,
@ -358,10 +365,9 @@ export async function getProjectInfo(projectPath: string): Promise<Project> {
// Write project settings file. // Write project settings file.
export async function writeProjectSettingsFile( export async function writeProjectSettingsFile(
projectPath: string, projectPath: string,
configuration: Partial<SaveSettingsPayload> tomlStr: string
): Promise<void> { ): Promise<void> {
const projectSettingsFilePath = await getProjectSettingsFilePath(projectPath) const projectSettingsFilePath = await getProjectSettingsFilePath(projectPath)
const tomlStr = tomlStringify({ settings: configuration })
if (err(tomlStr)) return Promise.reject(tomlStr) if (err(tomlStr)) return Promise.reject(tomlStr)
return window.electron.writeFile(projectSettingsFilePath, tomlStr) return window.electron.writeFile(projectSettingsFilePath, tomlStr)
} }
@ -416,7 +422,7 @@ export const getInitialDefaultDir = async () => {
export const readProjectSettingsFile = async ( export const readProjectSettingsFile = async (
projectPath: string projectPath: string
): Promise<Partial<SaveSettingsPayload>> => { ): Promise<DeepPartial<ProjectConfiguration>> => {
let settingsPath = await getProjectSettingsFilePath(projectPath) let settingsPath = await getProjectSettingsFilePath(projectPath)
// Check if this file exists. // Check if this file exists.
@ -431,6 +437,9 @@ export const readProjectSettingsFile = async (
const configToml = await window.electron.readFile(settingsPath) const configToml = await window.electron.readFile(settingsPath)
const configObj = parseProjectSettings(configToml) const configObj = parseProjectSettings(configToml)
if (err(configObj)) {
return Promise.reject(configObj)
}
return configObj return configObj
} }
@ -441,29 +450,25 @@ export const readAppSettingsFile = async () => {
} catch (e) { } catch (e) {
if (e === 'ENOENT') { if (e === 'ENOENT') {
const config = defaultAppSettings() 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')) return Promise.reject(new Error('config.app is falsey'))
}
config.app.projectDirectory = await getInitialDefaultDir() config.settings.app.project_directory = await getInitialDefaultDir()
return config return config
} }
} }
const configToml = await window.electron.readFile(settingsPath) const configToml = await window.electron.readFile(settingsPath)
const configObj = parseAppSettings(configToml) const configObj = parseAppSettings(configToml)
if (!configObj.app) { if (err(configObj)) {
return Promise.reject(new Error('config.app is falsey')) return Promise.reject(configObj)
}
if (!configObj.app.projectDirectory) {
configObj.app.projectDirectory = await getInitialDefaultDir()
} }
return configObj return configObj
} }
export const writeAppSettingsFile = async ( export const writeAppSettingsFile = async (tomlStr: string) => {
config: Partial<SaveSettingsPayload>
) => {
const appSettingsFilePath = await getAppSettingsFilePath() const appSettingsFilePath = await getAppSettingsFilePath()
const tomlStr = tomlStringify({ settings: config })
if (err(tomlStr)) return Promise.reject(tomlStr) if (err(tomlStr)) return Promise.reject(tomlStr)
return window.electron.writeFile(appSettingsFilePath, tomlStr) return window.electron.writeFile(appSettingsFilePath, tomlStr)
} }

View File

@ -4,9 +4,10 @@ import { isDesktop } from './isDesktop'
import { ProjectRoute } from 'wasm-lib/kcl/bindings/ProjectRoute' import { ProjectRoute } from 'wasm-lib/kcl/bindings/ProjectRoute'
import { parseProjectRoute, readAppSettingsFile } from './desktop' import { parseProjectRoute, readAppSettingsFile } from './desktop'
import { readLocalStorageAppSettingsFile } from './settings/settingsUtils' import { readLocalStorageAppSettingsFile } from './settings/settingsUtils'
import { SaveSettingsPayload } from './settings/settingsTypes'
import { err } from 'lib/trap' import { err } from 'lib/trap'
import { IS_PLAYWRIGHT_KEY } from '../../e2e/playwright/storageStates' import { IS_PLAYWRIGHT_KEY } from '../../e2e/playwright/storageStates'
import { DeepPartial } from './types'
import { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
const prependRoutes = const prependRoutes =
(routesObject: Record<string, string>) => (prepend: string) => { (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( export async function getProjectMetaByRouteId(
id?: string, id?: string,
configuration?: Partial<SaveSettingsPayload> | Error configuration?: DeepPartial<Configuration> | Error
): Promise<ProjectRoute | undefined> { ): Promise<ProjectRoute | undefined> {
if (!id) return undefined if (!id) return undefined

View File

@ -10,7 +10,7 @@ import {
} from 'lib/constants' } from 'lib/constants'
import { loadAndValidateSettings } from './settings/settingsUtils' import { loadAndValidateSettings } from './settings/settingsUtils'
import makeUrlPathRelative from './makeUrlPathRelative' import makeUrlPathRelative from './makeUrlPathRelative'
import { codeManager, kclManager } from 'lib/singletons' import { codeManager } from 'lib/singletons'
import { fileSystemManager } from 'lang/std/fileSystemManager' import { fileSystemManager } from 'lang/std/fileSystemManager'
import { import {
getProjectInfo, getProjectInfo,
@ -107,8 +107,6 @@ export const fileLoader: LoaderFunction = async (
// the file system and not the editor. // the file system and not the editor.
codeManager.updateCurrentFilePath(current_file_path) codeManager.updateCurrentFilePath(current_file_path)
codeManager.updateCodeStateEditor(code) 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 // Set the file system manager to the project path
@ -125,14 +123,22 @@ export const fileLoader: LoaderFunction = async (
default_file: project_path, default_file: project_path,
} }
const maybeProjectInfo = isDesktop()
? await getProjectInfo(project_path)
: null
console.log('maybeProjectInfo', {
maybeProjectInfo,
defaultProjectData,
projectPathData,
})
const projectData: IndexLoaderData = { const projectData: IndexLoaderData = {
code, code,
project: isDesktop() project: maybeProjectInfo ?? defaultProjectData,
? (await getProjectInfo(project_path)) ?? defaultProjectData
: defaultProjectData,
file: { file: {
name: current_file_name || '', name: current_file_name || '',
path: current_file_path?.split('/').slice(0, -1).join('/') ?? '', path: current_file_path || '',
children: [], children: [],
}, },
} }

View File

@ -21,6 +21,7 @@ import {
} from 'lib/desktop' } from 'lib/desktop'
import { ProjectConfiguration } from 'wasm-lib/kcl/bindings/ProjectConfiguration' import { ProjectConfiguration } from 'wasm-lib/kcl/bindings/ProjectConfiguration'
import { BROWSER_PROJECT_NAME } from 'lib/constants' import { BROWSER_PROJECT_NAME } from 'lib/constants'
import { DeepPartial } from 'lib/types'
/** /**
* Convert from a rust settings struct into the JS settings struct. * 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. * for hiding and showing settings.
**/ **/
export function configurationToSettingsPayload( export function configurationToSettingsPayload(
configuration: Configuration configuration: DeepPartial<Configuration>
): Partial<SaveSettingsPayload> { ): DeepPartial<SaveSettingsPayload> {
return { return {
app: { app: {
theme: appThemeToTheme(configuration?.settings?.app?.appearance?.theme), theme: appThemeToTheme(configuration?.settings?.app?.appearance?.theme),
@ -66,8 +67,8 @@ export function configurationToSettingsPayload(
} }
export function projectConfigurationToSettingsPayload( export function projectConfigurationToSettingsPayload(
configuration: ProjectConfiguration configuration: DeepPartial<ProjectConfiguration>
): Partial<SaveSettingsPayload> { ): DeepPartial<SaveSettingsPayload> {
return { return {
app: { app: {
theme: appThemeToTheme(configuration?.settings?.app?.appearance?.theme), theme: appThemeToTheme(configuration?.settings?.app?.appearance?.theme),
@ -106,7 +107,7 @@ function localStorageProjectSettingsPath() {
} }
export function readLocalStorageAppSettingsFile(): export function readLocalStorageAppSettingsFile():
| Partial<SaveSettingsPayload> | DeepPartial<Configuration>
| Error { | Error {
// TODO: Remove backwards compatibility after a few releases. // TODO: Remove backwards compatibility after a few releases.
let stored = let stored =
@ -132,7 +133,7 @@ export function readLocalStorageAppSettingsFile():
} }
function readLocalStorageProjectSettingsFile(): function readLocalStorageProjectSettingsFile():
| Partial<SaveSettingsPayload> | DeepPartial<ProjectConfiguration>
| Error { | Error {
// TODO: Remove backwards compatibility after a few releases. // TODO: Remove backwards compatibility after a few releases.
let stored = localStorage.getItem(localStorageProjectSettingsPath()) ?? '' let stored = localStorage.getItem(localStorageProjectSettingsPath()) ?? ''
@ -156,7 +157,7 @@ function readLocalStorageProjectSettingsFile():
export interface AppSettings { export interface AppSettings {
settings: ReturnType<typeof createSettings> settings: ReturnType<typeof createSettings>
configuration: Partial<SaveSettingsPayload> configuration: DeepPartial<Configuration>
} }
export async function loadAndValidateSettings( export async function loadAndValidateSettings(
@ -175,7 +176,11 @@ export async function loadAndValidateSettings(
if (err(appSettingsPayload)) return Promise.reject(appSettingsPayload) if (err(appSettingsPayload)) return Promise.reject(appSettingsPayload)
const settings = createSettings() const settings = createSettings()
setSettingsAtLevel(settings, 'user', appSettingsPayload) setSettingsAtLevel(
settings,
'user',
configurationToSettingsPayload(appSettingsPayload)
)
// Load the project settings if they exist // Load the project settings if they exist
if (projectPath) { if (projectPath) {
@ -187,11 +192,18 @@ export async function loadAndValidateSettings(
return Promise.reject(new Error('Invalid project settings')) return Promise.reject(new Error('Invalid project settings'))
const projectSettingsPayload = projectSettings const projectSettingsPayload = projectSettings
setSettingsAtLevel(settings, 'project', projectSettingsPayload) setSettingsAtLevel(
settings,
'project',
projectConfigurationToSettingsPayload(projectSettingsPayload)
)
} }
// Return the settings object // Return the settings object
return { settings, configuration: appSettingsPayload } return {
settings,
configuration: appSettingsPayload,
}
} }
export async function saveSettings( export async function saveSettings(
@ -204,21 +216,14 @@ export async function saveSettings(
// Get the user settings. // Get the user settings.
const jsAppSettings = getChangedSettingsAtLevel(allSettings, 'user') const jsAppSettings = getChangedSettingsAtLevel(allSettings, 'user')
const tomlString = tomlStringify({ settings: jsAppSettings }) const appTomlString = tomlStringify({ settings: jsAppSettings })
if (err(tomlString)) return if (err(appTomlString)) return
// Parse this as a Configuration.
const appSettings = parseAppSettings(tomlString)
if (err(appSettings)) return
const tomlString2 = tomlStringify({ settings: appSettings })
if (err(tomlString2)) return
// Write the app settings. // Write the app settings.
if (onDesktop) { if (onDesktop) {
await writeAppSettingsFile(appSettings) await writeAppSettingsFile(appTomlString)
} else { } else {
localStorage.setItem(localStorageAppSettingsPath(), tomlString2) localStorage.setItem(localStorageAppSettingsPath(), appTomlString)
} }
if (!projectPath) { if (!projectPath) {
@ -231,19 +236,11 @@ export async function saveSettings(
const projectTomlString = tomlStringify({ settings: jsProjectSettings }) const projectTomlString = tomlStringify({ settings: jsProjectSettings })
if (err(projectTomlString)) return 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. // Write the project settings.
if (onDesktop) { if (onDesktop) {
await writeProjectSettingsFile(projectPath, projectSettings) await writeProjectSettingsFile(projectPath, projectTomlString)
} else { } else {
localStorage.setItem(localStorageProjectSettingsPath(), tomlStr) localStorage.setItem(localStorageProjectSettingsPath(), projectTomlString)
} }
} }

View File

@ -95,3 +95,9 @@ export function isEnumMember<T extends Record<string, unknown>>(
) { ) {
return Object.values(e).includes(v) 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]
}

View File

@ -145,7 +145,7 @@ function OnboardingIntroductionInner() {
<div className="max-w-3xl p-8 rounded bg-chalkboard-10 dark:bg-chalkboard-90"> <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"> <h1 className="flex flex-wrap items-center gap-4 text-3xl font-bold">
<img <img
src={`./zma-logomark${getLogoTheme()}.svg`} src={`${isDesktop() ? '.' : ''}/zma-logomark${getLogoTheme()}.svg`}
alt={APP_NAME} alt={APP_NAME}
className="h-20 max-w-full" className="h-20 max-w-full"
/> />