Compare commits

...

13 Commits
v1.0.1 ... ok

Author SHA1 Message Date
6af475cf91 Merge branch 'main' into ok 2024-08-27 10:42:09 -04:00
2b5fd32699 Merge branch 'main' into ok 2024-08-26 21:23:44 -04:00
743aa606f9 Merge branch 'main' into ok 2024-08-26 16:19:12 -04:00
9a4e061be3 Merge branch 'main' into ok 2024-08-26 13:09:46 -04:00
42028798e4 Merge branch 'main' into ok 2024-08-26 12:36:49 -04:00
319251f98c Remove debugger statement 2024-08-26 12:17:23 -04:00
c545834a52 Merge branch 'main' into ok 2024-08-26 12:04:24 -04:00
bc8cf38936 tsc 2024-08-26 10:50:22 -04:00
535911fc0a Fix navigating to deleted file 2024-08-26 10:10:48 -04:00
19575a16d7 I've been lied to 2024-08-26 09:45:16 -04:00
2ae92d5341 Make tsc happy 2024-08-26 09:45:16 -04:00
fca1f02d88 fmt
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-08-26 09:45:16 -04:00
f752d47dd2 Fix and test file tree operations 2024-08-26 09:45:16 -04:00
7 changed files with 324 additions and 14 deletions

View File

@ -0,0 +1,204 @@
import { test, expect } from '@playwright/test'
import * as fsp from 'fs/promises'
import { getUtils, setup, setupElectron, tearDown } from './test-utils'
test.beforeEach(async ({ context, page }) => {
await setup(context, page)
})
test.afterEach(async ({ page }, testInfo) => {
await tearDown(page, testInfo)
})
test.describe('when using the file tree to', () => {
const fromFile = 'main.kcl'
const toFile = 'hello.kcl'
test(
`rename ${fromFile} to ${toFile}, and doesn't crash on reload and settings load`,
{ tag: '@electron' },
async ({ browser: _ }, testInfo) => {
const { electronApp, page } = await setupElectron({
testInfo,
folderSetupFn: async () => {},
})
const {
panesOpen,
createAndSelectProject,
pasteCodeInEditor,
renameFile,
editorTextMatches,
} = await getUtils(page, test)
await page.setViewportSize({ width: 1200, height: 500 })
page.on('console', console.log)
await panesOpen(['files', 'code'])
await createAndSelectProject('project-000')
// File the main.kcl with contents
const kclCube = await fsp.readFile(
'src/wasm-lib/tests/executor/inputs/cube.kcl',
'utf-8'
)
await pasteCodeInEditor(kclCube)
await renameFile(fromFile, toFile)
await page.reload()
await test.step('Postcondition: editor has same content as before the rename', async () => {
await editorTextMatches(kclCube)
})
await test.step('Postcondition: opening and closing settings works', async () => {
const settingsOpenButton = page.getByRole('link', {
name: 'settings Settings',
})
const settingsCloseButton = page.getByTestId('settings-close-button')
await settingsOpenButton.click()
await settingsCloseButton.click()
})
await electronApp.close()
}
)
test(
`create many new untitled files they increment their names`,
{ tag: '@electron' },
async ({ browser: _ }, testInfo) => {
const { electronApp, page } = await setupElectron({
testInfo,
folderSetupFn: async () => {},
})
const { panesOpen, createAndSelectProject, createNewFile } =
await getUtils(page, test)
await page.setViewportSize({ width: 1200, height: 500 })
page.on('console', console.log)
await panesOpen(['files'])
await createAndSelectProject('project-000')
await createNewFile('')
await createNewFile('')
await createNewFile('')
await createNewFile('')
await createNewFile('')
await test.step('Postcondition: there are 5 new Untitled-*.kcl files', async () => {
await expect(
page
.locator('[data-testid="file-pane-scroll-container"] button')
.filter({ hasText: /Untitled[-]?[0-5]?/ })
).toHaveCount(5)
})
await electronApp.close()
}
)
test(
'create a new file with the same name as an existing file cancels the operation',
{ tag: '@electron' },
async ({ browser: _ }, testInfo) => {
const { electronApp, page } = await setupElectron({
testInfo,
folderSetupFn: async () => {},
})
const {
panesOpen,
createAndSelectProject,
pasteCodeInEditor,
createNewFileAndSelect,
renameFile,
selectFile,
editorTextMatches,
} = await getUtils(page, test)
await page.setViewportSize({ width: 1200, height: 500 })
page.on('console', console.log)
await panesOpen(['files', 'code'])
await createAndSelectProject('project-000')
// File the main.kcl with contents
const kclCube = await fsp.readFile(
'src/wasm-lib/tests/executor/inputs/cube.kcl',
'utf-8'
)
await pasteCodeInEditor(kclCube)
const kcl1 = 'main.kcl'
const kcl2 = '2.kcl'
await createNewFileAndSelect(kcl2)
const kclCylinder = await fsp.readFile(
'src/wasm-lib/tests/executor/inputs/cylinder.kcl',
'utf-8'
)
await pasteCodeInEditor(kclCylinder)
await renameFile(kcl2, kcl1)
await test.step(`Postcondition: ${kcl1} still has the original content`, async () => {
await selectFile(kcl1)
await editorTextMatches(kclCube)
})
await test.step(`Postcondition: ${kcl2} still exists with the original content`, async () => {
await selectFile(kcl2)
await editorTextMatches(kclCylinder)
})
await electronApp.close()
}
)
test(
'deleting all files recreates a default main.kcl with no code',
{ tag: '@electron' },
async ({ browser: _ }, testInfo) => {
const { electronApp, page } = await setupElectron({
testInfo,
folderSetupFn: async () => {},
})
const {
panesOpen,
createAndSelectProject,
pasteCodeInEditor,
deleteFile,
editorTextMatches,
} = await getUtils(page, test)
await page.setViewportSize({ width: 1200, height: 500 })
page.on('console', console.log)
await panesOpen(['files', 'code'])
await createAndSelectProject('project-000')
// File the main.kcl with contents
const kclCube = await fsp.readFile(
'src/wasm-lib/tests/executor/inputs/cube.kcl',
'utf-8'
)
await pasteCodeInEditor(kclCube)
const kcl1 = 'main.kcl'
await deleteFile(kcl1)
await test.step(`Postcondition: ${kcl1} is recreated but has no content`, async () => {
await editorTextMatches('')
})
await electronApp.close()
}
)
})

View File

@ -532,18 +532,62 @@ export async function getUtils(page: Page, test_?: typeof test) {
})
},
createNewFile: async (name: string) => {
return test?.step(`Create a file named ${name}`, async () => {
await page.getByTestId('create-file-button').click()
await page.getByTestId('file-rename-field').fill(name)
await page.keyboard.press('Enter')
})
},
selectFile: async (name: string) => {
return test?.step(`Select ${name}`, async () => {
await page
.locator('[data-testid="file-pane-scroll-container"] button')
.filter({ hasText: name })
.click()
})
},
createNewFileAndSelect: async (name: string) => {
return test?.step(`Create a file named ${name}, select it`, async () => {
await page.getByTestId('create-file-button').click()
await page.getByTestId('file-rename-field').fill(name)
await page.keyboard.press('Enter')
await page
.getByTestId('file-pane-scroll-container')
.locator('[data-testid="file-pane-scroll-container"] button')
.filter({ hasText: name })
.click()
})
},
renameFile: async (fromName: string, toName: string) => {
return test?.step(`Rename ${fromName} to ${toName}`, async () => {
await page
.locator('[data-testid="file-pane-scroll-container"] button')
.filter({ hasText: fromName })
.click({ button: 'right' })
await page.getByTestId('context-menu-rename').click()
await page.getByTestId('file-rename-field').fill(toName)
await page.keyboard.press('Enter')
await page
.locator('[data-testid="file-pane-scroll-container"] button')
.filter({ hasText: toName })
.click()
})
},
deleteFile: async (name: string) => {
return test?.step(`Delete ${name}`, async () => {
await page
.locator('[data-testid="file-pane-scroll-container"] button')
.filter({ hasText: name })
.click({ button: 'right' })
await page.getByTestId('context-menu-delete').click()
await page.getByTestId('delete-confirmation').click()
})
},
panesOpen: async (paneIds: PaneId[]) => {
return test?.step(`Setting ${paneIds} panes to be open`, async () => {
await page.addInitScript(

View File

@ -135,16 +135,15 @@ interface ContextMenuItemProps {
icon?: ActionIconProps['icon']
onClick?: () => void
hotkey?: string
'data-testid'?: string
}
export function ContextMenuItem({
children,
icon,
onClick,
hotkey,
}: ContextMenuItemProps) {
export function ContextMenuItem(props: ContextMenuItemProps) {
const { children, icon, onClick, hotkey } = props
return (
<button
data-testid={props['data-testid']}
className="flex items-center gap-2 py-1 px-2 cursor-pointer hover:bg-chalkboard-20 dark:hover:bg-chalkboard-80 border-none text-left"
onClick={onClick}
>

View File

@ -16,7 +16,11 @@ import {
import { useCommandsContext } from 'hooks/useCommandsContext'
import { fileMachine } from 'machines/fileMachine'
import { isDesktop } from 'lib/isDesktop'
import { DEFAULT_FILE_NAME, FILE_EXT } from 'lib/constants'
import {
DEFAULT_FILE_NAME,
DEFAULT_PROJECT_KCL_FILE,
FILE_EXT,
} from 'lib/constants'
import { getProjectInfo } from 'lib/desktop'
import { getNextDirName, getNextFileName } from 'lib/desktopFS'
@ -167,6 +171,25 @@ export const FileMachineProvider = ({
name
)
// no-op
if (oldPath === newPath) {
return {
message: `Old is the same as new.`,
newPath,
oldPath,
}
}
// if there are any siblings with the same name, report error.
const entries = await window.electron.readdir(
window.electron.path.dirname(newPath)
)
for (let entry of entries) {
if (entry === newName) {
return Promise.reject(new Error('Filename already exists.'))
}
}
window.electron.rename(oldPath, newPath)
if (!file) {
@ -209,6 +232,22 @@ export const FileMachineProvider = ({
.catch((e) => console.error('Error deleting file', e))
}
// If there are no more files at all in the project, create a main.kcl
// for when we navigate to the root.
if (!project?.path) {
return Promise.reject(new Error('Project path not set.'))
}
const entries = await window.electron.readdir(project.path)
const hasKclEntries =
entries.filter((e: string) => e.endsWith('.kcl')).length !== 0
if (!hasKclEntries) {
await window.electron.writeFile(
window.electron.path.join(project.path, DEFAULT_PROJECT_KCL_FILE),
''
)
}
// If we just deleted the current file or one of its parent directories,
// navigate to the project root
if (
@ -219,6 +258,11 @@ export const FileMachineProvider = ({
navigate(`../${PATHS.FILE}/${encodeURIComponent(project.path)}`)
}
// Refresh the route selected above because it's possible we're on
// the same path on the navigate, which doesn't cause anything to
// refresh, leaving a stale execution state.
navigate(0)
return `Successfully deleted ${isDir ? 'folder' : 'file'} "${
event.data.name
}"`

View File

@ -358,10 +358,18 @@ function FileTreeContextMenu({
<ContextMenu
menuTargetElement={itemRef}
items={[
<ContextMenuItem onClick={onRename} hotkey="Enter">
<ContextMenuItem
data-testid="context-menu-rename"
onClick={onRename}
hotkey="Enter"
>
Rename
</ContextMenuItem>,
<ContextMenuItem onClick={onDelete} hotkey={metaKey + ' + Del'}>
<ContextMenuItem
data-testid="context-menu-delete"
onClick={onDelete}
hotkey={metaKey + ' + Del'}
>
Delete
</ContextMenuItem>,
]}

View File

@ -8,6 +8,7 @@ export const MAX_PADDING = 7
* This is available for users to edit as a setting.
*/
export const DEFAULT_PROJECT_NAME = 'project-$nnn'
export const DEFAULT_PROJECT_KCL_FILE = 'main.kcl'
/** Name given the temporary "project" in the browser version of the app */
export const BROWSER_PROJECT_NAME = 'browser'
/** Name given the temporary file in the browser version of the app */

View File

@ -90,12 +90,22 @@ export const fileLoader: LoaderFunction = async (
let code = ''
if (!urlObj.pathname.endsWith('/settings')) {
if (!currentFileName || !currentFilePath || !projectName) {
const fallbackFile = (await getProjectInfo(projectPath)).default_file
let fileExists = true
if (currentFilePath) {
try {
await window.electron.stat(currentFilePath)
} catch (e) {
if (e === 'ENOENT') {
fileExists = false
}
}
}
if (!fileExists || !currentFileName || !currentFilePath || !projectName) {
return redirect(
`${PATHS.FILE}/${encodeURIComponent(
isDesktop()
? (await getProjectInfo(projectPath)).default_file
: params.id + '/' + PROJECT_ENTRYPOINT
isDesktop() ? fallbackFile : params.id + '/' + PROJECT_ENTRYPOINT
)}`
)
}