Assemblies: Load outside files into project via point-and-click (#6217)
* WIP: Add point-and-click Import for geometry Will eventually fix #6120 Right now the whole loop is there but the codemod doesn't work yet * Better pathToNOde, log on non-working cm dispatch call * Add workaround to updateModelingState not working * Back to updateModelingState with a skip flag * Better todo * Change working from Import to Insert, cleanups * Sister command in kclCommands to populate file options * Improve path selector * Unsure: move importAstMod to kclCommands onSubmit 😶 * Add e2e test * Clean up for review * Add native file menu entry and test * No await yo lint said so * WIP: UX improvements around foreign file imports Fixes #6152 * @lrev-Dev's suggestion to remove a comment Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch> * Update to scene.settled(cmdBar) * Add partNNN default name for alias * Lint * Lint * Fix unit tests * Add sad path insert test Thanks @Irev-Dev for the suggestion * Add step insert test * Lint * Add test for second foreign import thru file tree click * WIP: Add point-and-click Load to copy files from outside the project into the project Towards #6210 * Move Insert button to modeling toolbar, update menus and toolbars * Add default value for local name alias * Aligning tests * Fix tests * Add padding for filenames starting with a digit * Lint * Lint * Update snapshots * Merge branch 'main' into pierremtb/issue6210-Add-point-and-click-Load-to-copy-files-from-outside-the-project-into-the-project * Add disabled transform subbutton * Merge kcl-samples and local disk load into one 'Load external model' command * Fix em tests * Fix test * Add test for file pick import, better input * Fix non .kcl loading * Lint * Update snapshots * Fix issue leading to test failure * Fix clone test * Add note * Fix nested clone issue * Clean up for review * Add valueSummary for path * Fix test after path change * Clean up for review * Update src/lib/kclCommands.ts Thanks @franknoirot! Co-authored-by: Frank Noirot <frank@zoo.dev> * Improve path input arg * Fix tests * Merge branch 'main' into pierremtb/issue6210-Add-point-and-click-Load-to-copy-files-from-outside-the-project-into-the-project * Fix path header not showing and improve tests * Clean up --------- Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch> Co-authored-by: Frank Noirot <frank@zoo.dev>
@ -3,7 +3,7 @@ import { expect } from '@playwright/test'
|
|||||||
import * as fs from 'fs'
|
import * as fs from 'fs'
|
||||||
import * as path from 'path'
|
import * as path from 'path'
|
||||||
|
|
||||||
type CmdBarSerialised =
|
export type CmdBarSerialised =
|
||||||
| {
|
| {
|
||||||
stage: 'commandBarClosed'
|
stage: 'commandBarClosed'
|
||||||
}
|
}
|
||||||
|
@ -27,6 +27,7 @@ export class ToolbarFixture {
|
|||||||
offsetPlaneButton!: Locator
|
offsetPlaneButton!: Locator
|
||||||
helixButton!: Locator
|
helixButton!: Locator
|
||||||
startSketchBtn!: Locator
|
startSketchBtn!: Locator
|
||||||
|
insertButton!: Locator
|
||||||
lineBtn!: Locator
|
lineBtn!: Locator
|
||||||
tangentialArcBtn!: Locator
|
tangentialArcBtn!: Locator
|
||||||
circleBtn!: Locator
|
circleBtn!: Locator
|
||||||
@ -44,7 +45,7 @@ export class ToolbarFixture {
|
|||||||
featureTreePane!: Locator
|
featureTreePane!: Locator
|
||||||
gizmo!: Locator
|
gizmo!: Locator
|
||||||
gizmoDisabled!: Locator
|
gizmoDisabled!: Locator
|
||||||
insertButton!: Locator
|
loadButton!: Locator
|
||||||
|
|
||||||
constructor(page: Page) {
|
constructor(page: Page) {
|
||||||
this.page = page
|
this.page = page
|
||||||
@ -59,6 +60,7 @@ export class ToolbarFixture {
|
|||||||
this.offsetPlaneButton = page.getByTestId('plane-offset')
|
this.offsetPlaneButton = page.getByTestId('plane-offset')
|
||||||
this.helixButton = page.getByTestId('helix')
|
this.helixButton = page.getByTestId('helix')
|
||||||
this.startSketchBtn = page.getByTestId('sketch')
|
this.startSketchBtn = page.getByTestId('sketch')
|
||||||
|
this.insertButton = page.getByTestId('insert')
|
||||||
this.lineBtn = page.getByTestId('line')
|
this.lineBtn = page.getByTestId('line')
|
||||||
this.tangentialArcBtn = page.getByTestId('tangential-arc')
|
this.tangentialArcBtn = page.getByTestId('tangential-arc')
|
||||||
this.circleBtn = page.getByTestId('circle-center')
|
this.circleBtn = page.getByTestId('circle-center')
|
||||||
@ -68,6 +70,7 @@ export class ToolbarFixture {
|
|||||||
this.fileTreeBtn = page.locator('[id="files-button-holder"]')
|
this.fileTreeBtn = page.locator('[id="files-button-holder"]')
|
||||||
this.createFileBtn = page.getByTestId('create-file-button')
|
this.createFileBtn = page.getByTestId('create-file-button')
|
||||||
this.treeInputField = page.getByTestId('tree-input-field')
|
this.treeInputField = page.getByTestId('tree-input-field')
|
||||||
|
this.loadButton = page.getByTestId('load-external-model-pane-button')
|
||||||
|
|
||||||
this.filePane = page.locator('#files-pane')
|
this.filePane = page.locator('#files-pane')
|
||||||
this.featureTreePane = page.locator('#feature-tree-pane')
|
this.featureTreePane = page.locator('#feature-tree-pane')
|
||||||
@ -79,8 +82,6 @@ export class ToolbarFixture {
|
|||||||
// element or two different elements can represent these states.
|
// element or two different elements can represent these states.
|
||||||
this.gizmo = page.getByTestId('gizmo')
|
this.gizmo = page.getByTestId('gizmo')
|
||||||
this.gizmoDisabled = page.getByTestId('gizmo-disabled')
|
this.gizmoDisabled = page.getByTestId('gizmo-disabled')
|
||||||
|
|
||||||
this.insertButton = page.getByTestId('insert-pane-button')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get logoLink() {
|
get logoLink() {
|
||||||
|
@ -534,7 +534,7 @@ test.describe('Native file menu', { tag: ['@electron'] }, () => {
|
|||||||
const expected = 'Open project'
|
const expected = 'Open project'
|
||||||
expect(actual).toBe(expected)
|
expect(actual).toBe(expected)
|
||||||
})
|
})
|
||||||
test('Modeling.File.Load a sample model', async ({
|
test('Modeling.File.Load external model', async ({
|
||||||
tronApp,
|
tronApp,
|
||||||
cmdBar,
|
cmdBar,
|
||||||
page,
|
page,
|
||||||
@ -555,10 +555,10 @@ test.describe('Native file menu', { tag: ['@electron'] }, () => {
|
|||||||
throw new Error('app or app.applicationMenu is missing')
|
throw new Error('app or app.applicationMenu is missing')
|
||||||
}
|
}
|
||||||
const openProject = app.applicationMenu.getMenuItemById(
|
const openProject = app.applicationMenu.getMenuItemById(
|
||||||
'File.Load a sample model'
|
'File.Load external model'
|
||||||
)
|
)
|
||||||
if (!openProject) {
|
if (!openProject) {
|
||||||
throw new Error('File.Load a sample model')
|
throw new Error('File.Load external model')
|
||||||
}
|
}
|
||||||
openProject.click()
|
openProject.click()
|
||||||
})
|
})
|
||||||
@ -568,44 +568,7 @@ test.describe('Native file menu', { tag: ['@electron'] }, () => {
|
|||||||
const actual = await cmdBar.cmdBarElement
|
const actual = await cmdBar.cmdBarElement
|
||||||
.getByTestId('command-name')
|
.getByTestId('command-name')
|
||||||
.textContent()
|
.textContent()
|
||||||
const expected = 'Open sample'
|
const expected = 'Load external model'
|
||||||
expect(actual).toBe(expected)
|
|
||||||
})
|
|
||||||
test('Modeling.File.Insert from project file', async ({
|
|
||||||
tronApp,
|
|
||||||
cmdBar,
|
|
||||||
page,
|
|
||||||
homePage,
|
|
||||||
scene,
|
|
||||||
}) => {
|
|
||||||
if (!tronApp) {
|
|
||||||
throwTronAppMissing()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
await homePage.goToModelingScene()
|
|
||||||
await scene.settled(cmdBar)
|
|
||||||
|
|
||||||
// Run electron snippet to find the Menu!
|
|
||||||
await page.waitForTimeout(100) // wait for createModelingPageMenu() to run
|
|
||||||
await tronApp.electron.evaluate(async ({ app }) => {
|
|
||||||
if (!app || !app.applicationMenu) {
|
|
||||||
throw new Error('app or app.applicationMenu is missing')
|
|
||||||
}
|
|
||||||
const openProject = app.applicationMenu.getMenuItemById(
|
|
||||||
'File.Insert from project file'
|
|
||||||
)
|
|
||||||
if (!openProject) {
|
|
||||||
throw new Error('File.Insert from project file')
|
|
||||||
}
|
|
||||||
openProject.click()
|
|
||||||
})
|
|
||||||
// Check that the command bar is opened
|
|
||||||
await expect(cmdBar.cmdBarElement).toBeVisible()
|
|
||||||
// Check the placeholder project name exists
|
|
||||||
const actual = await cmdBar.cmdBarElement
|
|
||||||
.getByTestId('command-name')
|
|
||||||
.textContent()
|
|
||||||
const expected = 'Insert'
|
|
||||||
expect(actual).toBe(expected)
|
expect(actual).toBe(expected)
|
||||||
})
|
})
|
||||||
test('Modeling.File.Export current part', async ({
|
test('Modeling.File.Export current part', async ({
|
||||||
@ -2159,6 +2122,44 @@ test.describe('Native file menu', { tag: ['@electron'] }, () => {
|
|||||||
expect(actual).toBe(expected)
|
expect(actual).toBe(expected)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('Modeling.Design.Insert from project file', async ({
|
||||||
|
tronApp,
|
||||||
|
cmdBar,
|
||||||
|
page,
|
||||||
|
homePage,
|
||||||
|
scene,
|
||||||
|
}) => {
|
||||||
|
if (!tronApp) {
|
||||||
|
throwTronAppMissing()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await homePage.goToModelingScene()
|
||||||
|
await scene.settled(cmdBar)
|
||||||
|
|
||||||
|
// Run electron snippet to find the Menu!
|
||||||
|
await page.waitForTimeout(100) // wait for createModelingPageMenu() to run
|
||||||
|
await tronApp.electron.evaluate(async ({ app }) => {
|
||||||
|
if (!app || !app.applicationMenu) {
|
||||||
|
throw new Error('app or app.applicationMenu is missing')
|
||||||
|
}
|
||||||
|
const openProject = app.applicationMenu.getMenuItemById(
|
||||||
|
'Design.Insert from project file'
|
||||||
|
)
|
||||||
|
if (!openProject) {
|
||||||
|
throw new Error('Design.Insert from project file')
|
||||||
|
}
|
||||||
|
openProject.click()
|
||||||
|
})
|
||||||
|
// Check that the command bar is opened
|
||||||
|
await expect(cmdBar.cmdBarElement).toBeVisible()
|
||||||
|
// Check the placeholder project name exists
|
||||||
|
const actual = await cmdBar.cmdBarElement
|
||||||
|
.getByTestId('command-name')
|
||||||
|
.textContent()
|
||||||
|
const expected = 'Insert'
|
||||||
|
expect(actual).toBe(expected)
|
||||||
|
})
|
||||||
|
|
||||||
test('Modeling.Design.Create with Zoo Text-To-CAD', async ({
|
test('Modeling.Design.Create with Zoo Text-To-CAD', async ({
|
||||||
tronApp,
|
tronApp,
|
||||||
cmdBar,
|
cmdBar,
|
||||||
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 52 KiB |
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 55 KiB |
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 43 KiB |
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 51 KiB |
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 57 KiB |
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 62 KiB |
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 51 KiB |
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 74 KiB |
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 58 KiB |
Before Width: | Height: | Size: 140 KiB After Width: | Height: | Size: 138 KiB |
Before Width: | Height: | Size: 124 KiB After Width: | Height: | Size: 121 KiB |
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 65 KiB |
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 68 KiB |
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 64 KiB |
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 67 KiB |
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 67 KiB |
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 63 KiB |
@ -3,14 +3,18 @@ import { FILE_EXT } from '@src/lib/constants'
|
|||||||
import * as fsp from 'fs/promises'
|
import * as fsp from 'fs/promises'
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
|
|
||||||
|
import type { CmdBarSerialised } from '@e2e/playwright/fixtures/cmdBarFixture'
|
||||||
|
import type { ElectronZoo } from '@e2e/playwright/fixtures/fixtureSetup'
|
||||||
import {
|
import {
|
||||||
|
executorInputPath,
|
||||||
getUtils,
|
getUtils,
|
||||||
orRunWhenFullSuiteEnabled,
|
orRunWhenFullSuiteEnabled,
|
||||||
runningOnWindows,
|
runningOnWindows,
|
||||||
|
testsInputPath,
|
||||||
} from '@e2e/playwright/test-utils'
|
} from '@e2e/playwright/test-utils'
|
||||||
import { expect, test } from '@e2e/playwright/zoo-test'
|
import { expect, test } from '@e2e/playwright/zoo-test'
|
||||||
|
|
||||||
test.describe('Testing in-app sample loading', () => {
|
test.describe('Testing loading external models', () => {
|
||||||
/**
|
/**
|
||||||
* Note this test implicitly depends on the KCL sample "parametric-bearing-pillow-block",
|
* Note this test implicitly depends on the KCL sample "parametric-bearing-pillow-block",
|
||||||
* its title, and its units settings. https://github.com/KittyCAD/kcl-samples/blob/main/parametric-bearing-pillow-block/main.kcl
|
* its title, and its units settings. https://github.com/KittyCAD/kcl-samples/blob/main/parametric-bearing-pillow-block/main.kcl
|
||||||
@ -39,7 +43,7 @@ test.describe('Testing in-app sample loading', () => {
|
|||||||
}
|
}
|
||||||
const commandBarButton = page.getByRole('button', { name: 'Commands' })
|
const commandBarButton = page.getByRole('button', { name: 'Commands' })
|
||||||
const samplesCommandOption = page.getByRole('option', {
|
const samplesCommandOption = page.getByRole('option', {
|
||||||
name: 'Open Sample',
|
name: 'Load external model',
|
||||||
})
|
})
|
||||||
const commandSampleOption = page.getByRole('option', {
|
const commandSampleOption = page.getByRole('option', {
|
||||||
name: newSample.title,
|
name: newSample.title,
|
||||||
@ -83,7 +87,7 @@ test.describe('Testing in-app sample loading', () => {
|
|||||||
test(
|
test(
|
||||||
'Desktop: should create new file by default, optionally overwrite',
|
'Desktop: should create new file by default, optionally overwrite',
|
||||||
{ tag: '@electron' },
|
{ tag: '@electron' },
|
||||||
async ({ editor, context, page, scene, cmdBar }, testInfo) => {
|
async ({ editor, context, page, scene, cmdBar, toolbar }) => {
|
||||||
if (runningOnWindows()) {
|
if (runningOnWindows()) {
|
||||||
test.fixme(orRunWhenFullSuiteEnabled())
|
test.fixme(orRunWhenFullSuiteEnabled())
|
||||||
}
|
}
|
||||||
@ -106,20 +110,12 @@ test.describe('Testing in-app sample loading', () => {
|
|||||||
title: '100mm Gear Rack',
|
title: '100mm Gear Rack',
|
||||||
}
|
}
|
||||||
const projectCard = page.getByRole('link', { name: 'bracket' })
|
const projectCard = page.getByRole('link', { name: 'bracket' })
|
||||||
const commandBarButton = page.getByRole('button', { name: 'Commands' })
|
|
||||||
const commandOption = page.getByRole('option', { name: 'Open Sample' })
|
|
||||||
const commandSampleOption = (name: string) =>
|
|
||||||
page.getByRole('option', {
|
|
||||||
name,
|
|
||||||
exact: true,
|
|
||||||
})
|
|
||||||
const commandMethodArgButton = page.getByRole('button', {
|
const commandMethodArgButton = page.getByRole('button', {
|
||||||
name: 'Method',
|
name: 'Method',
|
||||||
})
|
})
|
||||||
const commandMethodOption = page.getByRole('option', {
|
const commandMethodOption = page.getByRole('option', {
|
||||||
name: 'Overwrite',
|
name: 'Overwrite',
|
||||||
})
|
})
|
||||||
const newFileWarning = page.getByText('Create a new file from sample?')
|
|
||||||
const overwriteWarning = page.getByText(
|
const overwriteWarning = page.getByText(
|
||||||
'Overwrite current file with sample?'
|
'Overwrite current file with sample?'
|
||||||
)
|
)
|
||||||
@ -129,6 +125,18 @@ test.describe('Testing in-app sample loading', () => {
|
|||||||
page.getByRole('listitem').filter({
|
page.getByRole('listitem').filter({
|
||||||
has: page.getByRole('button', { name }),
|
has: page.getByRole('button', { name }),
|
||||||
})
|
})
|
||||||
|
const defaultLoadCmdBarState: CmdBarSerialised = {
|
||||||
|
commandName: 'Load external model',
|
||||||
|
currentArgKey: 'source',
|
||||||
|
currentArgValue: '',
|
||||||
|
headerArguments: {
|
||||||
|
Method: 'newFile',
|
||||||
|
Sample: '',
|
||||||
|
Source: '',
|
||||||
|
},
|
||||||
|
highlightedHeaderArg: 'source',
|
||||||
|
stage: 'arguments',
|
||||||
|
}
|
||||||
|
|
||||||
await test.step(`Test setup`, async () => {
|
await test.step(`Test setup`, async () => {
|
||||||
await page.setBodyDimensions({ width: 1200, height: 500 })
|
await page.setBodyDimensions({ width: 1200, height: 500 })
|
||||||
@ -147,14 +155,12 @@ test.describe('Testing in-app sample loading', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
await test.step(`Load a KCL sample with the command palette`, async () => {
|
await test.step(`Load a KCL sample with the command palette`, async () => {
|
||||||
await commandBarButton.click()
|
await toolbar.loadButton.click()
|
||||||
await page.waitForTimeout(1000)
|
await cmdBar.expectState(defaultLoadCmdBarState)
|
||||||
await commandOption.click()
|
await cmdBar.progressCmdBar()
|
||||||
await page.waitForTimeout(1000)
|
await cmdBar.selectOption({ name: sampleOne.title }).click()
|
||||||
await commandSampleOption(sampleOne.title).click()
|
|
||||||
await expect(overwriteWarning).not.toBeVisible()
|
await expect(overwriteWarning).not.toBeVisible()
|
||||||
await expect(newFileWarning).toBeVisible()
|
await cmdBar.progressCmdBar()
|
||||||
await confirmButton.click()
|
|
||||||
await page.waitForTimeout(1000)
|
await page.waitForTimeout(1000)
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -165,21 +171,15 @@ test.describe('Testing in-app sample loading', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
await test.step(`Now overwrite the current file`, async () => {
|
await test.step(`Now overwrite the current file`, async () => {
|
||||||
await commandBarButton.click()
|
await toolbar.loadButton.click()
|
||||||
await page.waitForTimeout(1000)
|
await cmdBar.expectState(defaultLoadCmdBarState)
|
||||||
await commandOption.click()
|
await cmdBar.progressCmdBar()
|
||||||
await page.waitForTimeout(1000)
|
await cmdBar.selectOption({ name: sampleTwo.title }).click()
|
||||||
await commandSampleOption(sampleTwo.title).click()
|
|
||||||
await page.waitForTimeout(1000)
|
|
||||||
await commandMethodArgButton.click()
|
await commandMethodArgButton.click()
|
||||||
await page.waitForTimeout(1000)
|
|
||||||
await commandMethodOption.click()
|
await commandMethodOption.click()
|
||||||
await page.waitForTimeout(1000)
|
|
||||||
await expect(commandMethodArgButton).toContainText('overwrite')
|
await expect(commandMethodArgButton).toContainText('overwrite')
|
||||||
await expect(newFileWarning).not.toBeVisible()
|
|
||||||
await expect(overwriteWarning).toBeVisible()
|
await expect(overwriteWarning).toBeVisible()
|
||||||
await confirmButton.click()
|
await confirmButton.click()
|
||||||
await page.waitForTimeout(1000)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
await test.step(`Ensure we overwrote the current file without navigating`, async () => {
|
await test.step(`Ensure we overwrote the current file without navigating`, async () => {
|
||||||
@ -200,4 +200,96 @@ test.describe('Testing in-app sample loading', () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const externalModelCases = [
|
||||||
|
{
|
||||||
|
modelName: 'cylinder.kcl',
|
||||||
|
deconflictedModelName: 'cylinder-1.kcl',
|
||||||
|
modelPath: executorInputPath('cylinder.kcl'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
modelName: 'cube.step',
|
||||||
|
deconflictedModelName: 'cube-1.step',
|
||||||
|
modelPath: testsInputPath('cube.step'),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
externalModelCases.map(({ modelName, deconflictedModelName, modelPath }) => {
|
||||||
|
test(
|
||||||
|
`Load external models from local drive - ${modelName}`,
|
||||||
|
{ tag: ['@electron'] },
|
||||||
|
async ({ page, homePage, scene, toolbar, cmdBar, tronApp }) => {
|
||||||
|
if (!tronApp) {
|
||||||
|
fail()
|
||||||
|
}
|
||||||
|
|
||||||
|
await page.setBodyDimensions({ width: 1000, height: 500 })
|
||||||
|
await homePage.goToModelingScene()
|
||||||
|
await scene.settled(cmdBar)
|
||||||
|
const modelFileContent = await fsp.readFile(modelPath, 'utf-8')
|
||||||
|
const { editorTextMatches } = await getUtils(page, test)
|
||||||
|
|
||||||
|
async function loadExternalFileThroughCommandBar(tronApp: ElectronZoo) {
|
||||||
|
await toolbar.loadButton.click()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
commandName: 'Load external model',
|
||||||
|
currentArgKey: 'source',
|
||||||
|
currentArgValue: '',
|
||||||
|
headerArguments: {
|
||||||
|
Method: 'newFile',
|
||||||
|
Sample: '',
|
||||||
|
Source: '',
|
||||||
|
},
|
||||||
|
highlightedHeaderArg: 'source',
|
||||||
|
stage: 'arguments',
|
||||||
|
})
|
||||||
|
await cmdBar.selectOption({ name: 'Local Drive' }).click()
|
||||||
|
|
||||||
|
// Mock the file picker selection
|
||||||
|
const handleFile = tronApp.electron.evaluate(
|
||||||
|
async ({ dialog }, filePaths) => {
|
||||||
|
dialog.showOpenDialog = () =>
|
||||||
|
Promise.resolve({ canceled: false, filePaths })
|
||||||
|
},
|
||||||
|
[modelPath]
|
||||||
|
)
|
||||||
|
await page.getByTestId('cmd-bar-arg-file-button').click()
|
||||||
|
await handleFile
|
||||||
|
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
commandName: 'Load external model',
|
||||||
|
headerArguments: {
|
||||||
|
Source: 'local',
|
||||||
|
Path: modelName,
|
||||||
|
},
|
||||||
|
stage: 'review',
|
||||||
|
})
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
}
|
||||||
|
|
||||||
|
await test.step('Load the external model from local drive', async () => {
|
||||||
|
await loadExternalFileThroughCommandBar(tronApp)
|
||||||
|
// TODO: I think the files pane should auto open?
|
||||||
|
await toolbar.openPane('files')
|
||||||
|
await toolbar.expectFileTreeState([modelName, 'main.kcl'])
|
||||||
|
if (modelName.endsWith('.kcl')) {
|
||||||
|
await editorTextMatches(modelFileContent)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
await test.step('Load the same external model, except deconflicted name', async () => {
|
||||||
|
await loadExternalFileThroughCommandBar(tronApp)
|
||||||
|
await toolbar.openPane('files')
|
||||||
|
await toolbar.expectFileTreeState([
|
||||||
|
deconflictedModelName,
|
||||||
|
modelName,
|
||||||
|
'main.kcl',
|
||||||
|
])
|
||||||
|
if (modelName.endsWith('.kcl')) {
|
||||||
|
await editorTextMatches(modelFileContent)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -2,6 +2,7 @@ import CommandArgOptionInput from '@src/components/CommandBar/CommandArgOptionIn
|
|||||||
import CommandBarBasicInput from '@src/components/CommandBar/CommandBarBasicInput'
|
import CommandBarBasicInput from '@src/components/CommandBar/CommandBarBasicInput'
|
||||||
import CommandBarHeader from '@src/components/CommandBar/CommandBarHeader'
|
import CommandBarHeader from '@src/components/CommandBar/CommandBarHeader'
|
||||||
import CommandBarKclInput from '@src/components/CommandBar/CommandBarKclInput'
|
import CommandBarKclInput from '@src/components/CommandBar/CommandBarKclInput'
|
||||||
|
import CommandBarPathInput from '@src/components/CommandBar/CommandBarPathInput'
|
||||||
import CommandBarSelectionInput from '@src/components/CommandBar/CommandBarSelectionInput'
|
import CommandBarSelectionInput from '@src/components/CommandBar/CommandBarSelectionInput'
|
||||||
import CommandBarSelectionMixedInput from '@src/components/CommandBar/CommandBarSelectionMixedInput'
|
import CommandBarSelectionMixedInput from '@src/components/CommandBar/CommandBarSelectionMixedInput'
|
||||||
import CommandBarTextareaInput from '@src/components/CommandBar/CommandBarTextareaInput'
|
import CommandBarTextareaInput from '@src/components/CommandBar/CommandBarTextareaInput'
|
||||||
@ -108,6 +109,14 @@ function ArgumentInput({
|
|||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
case 'path':
|
||||||
|
return (
|
||||||
|
<CommandBarPathInput
|
||||||
|
arg={arg}
|
||||||
|
stepBack={stepBack}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
/>
|
||||||
|
)
|
||||||
default:
|
default:
|
||||||
return (
|
return (
|
||||||
<CommandBarBasicInput
|
<CommandBarBasicInput
|
||||||
|
120
src/components/CommandBar/CommandBarPathInput.tsx
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
import { useEffect, useMemo, useRef } from 'react'
|
||||||
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
|
|
||||||
|
import { ActionButton } from '@src/components/ActionButton'
|
||||||
|
import type { CommandArgument } from '@src/lib/commandTypes'
|
||||||
|
import { reportRejection } from '@src/lib/trap'
|
||||||
|
import { isArray, toSync } from '@src/lib/utils'
|
||||||
|
import {
|
||||||
|
commandBarActor,
|
||||||
|
useCommandBarState,
|
||||||
|
} from '@src/machines/commandBarMachine'
|
||||||
|
import { useSelector } from '@xstate/react'
|
||||||
|
import type { AnyStateMachine, SnapshotFrom } from 'xstate'
|
||||||
|
|
||||||
|
// TODO: remove the need for this selector once we decouple all actors from React
|
||||||
|
const machineContextSelector = (snapshot?: SnapshotFrom<AnyStateMachine>) =>
|
||||||
|
snapshot?.context
|
||||||
|
|
||||||
|
function CommandBarPathInput({
|
||||||
|
arg,
|
||||||
|
stepBack,
|
||||||
|
onSubmit,
|
||||||
|
}: {
|
||||||
|
arg: CommandArgument<unknown> & {
|
||||||
|
inputType: 'path'
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
stepBack: () => void
|
||||||
|
onSubmit: (event: unknown) => void
|
||||||
|
}) {
|
||||||
|
const commandBarState = useCommandBarState()
|
||||||
|
useHotkeys('mod + k, mod + /', () => commandBarActor.send({ type: 'Close' }))
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null)
|
||||||
|
const argMachineContext = useSelector(
|
||||||
|
arg.machineActor,
|
||||||
|
machineContextSelector
|
||||||
|
)
|
||||||
|
const defaultValue = useMemo(
|
||||||
|
() =>
|
||||||
|
arg.defaultValue
|
||||||
|
? arg.defaultValue instanceof Function
|
||||||
|
? arg.defaultValue(commandBarState.context, argMachineContext)
|
||||||
|
: arg.defaultValue
|
||||||
|
: '',
|
||||||
|
[arg.defaultValue, commandBarState.context, argMachineContext]
|
||||||
|
)
|
||||||
|
|
||||||
|
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
|
||||||
|
e.preventDefault()
|
||||||
|
onSubmit(inputRef.current?.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pickFileThroughNativeDialog() {
|
||||||
|
// In desktop end-to-end tests we can't control the file picker,
|
||||||
|
// so we seed the new directory value in the element's dataset
|
||||||
|
const inputRefVal = inputRef.current?.dataset.testValue
|
||||||
|
if (inputRef.current && inputRefVal && !isArray(inputRefVal)) {
|
||||||
|
inputRef.current.value = inputRefVal
|
||||||
|
} else if (inputRef.current) {
|
||||||
|
const newPath = await window.electron.open({
|
||||||
|
properties: ['openFile'],
|
||||||
|
title: 'Pick a file to load into the current project',
|
||||||
|
})
|
||||||
|
if (newPath.canceled) return
|
||||||
|
inputRef.current.value = newPath.filePaths[0]
|
||||||
|
} else {
|
||||||
|
return new Error("Couldn't find inputRef")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fire on component mount, if outside of e2e test context
|
||||||
|
useEffect(() => {
|
||||||
|
window.electron.process.env.IS_PLAYWRIGHT !== 'true' &&
|
||||||
|
toSync(pickFileThroughNativeDialog, reportRejection)()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form id="arg-form" onSubmit={handleSubmit}>
|
||||||
|
<label
|
||||||
|
data-testid="cmd-bar-arg-name"
|
||||||
|
className="flex items-center mx-4 my-4 border-b border-b-chalkboard-100 dark:border-b-chalkboard-80"
|
||||||
|
>
|
||||||
|
<span className="capitalize px-2 py-1 bg-chalkboard-100 dark:bg-chalkboard-80 text-chalkboard-10">
|
||||||
|
{arg.displayName || arg.name}
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
data-testid="cmd-bar-arg-value"
|
||||||
|
id="arg-form"
|
||||||
|
name={arg.inputType}
|
||||||
|
ref={inputRef}
|
||||||
|
required
|
||||||
|
className="flex-grow px-2 py-1 !bg-transparent focus:outline-none"
|
||||||
|
placeholder="Enter a path"
|
||||||
|
defaultValue={defaultValue}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key === 'Backspace' && event.shiftKey) {
|
||||||
|
stepBack()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<ActionButton
|
||||||
|
Element="button"
|
||||||
|
onClick={toSync(pickFileThroughNativeDialog, reportRejection)}
|
||||||
|
className="p-0 m-0 border-none hover:bg-primary/10 focus:bg-primary/10 dark:hover:bg-primary/20 dark:focus::bg-primary/20"
|
||||||
|
data-testid="cmd-bar-arg-file-button"
|
||||||
|
iconEnd={{
|
||||||
|
icon: 'file',
|
||||||
|
size: 'sm',
|
||||||
|
className: 'p-1',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Open file
|
||||||
|
</ActionButton>
|
||||||
|
</label>
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CommandBarPathInput
|
@ -619,6 +619,22 @@ const CustomIconMap = {
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
),
|
),
|
||||||
|
importFile: (
|
||||||
|
<svg
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
d="M4 3H4.5H11H11.2071L11.3536 3.14645L15.8536 7.64646L16 7.7929V8.00001V11.3773C15.6992 11.1362 15.3628 10.9376 15 10.7908V8.50001H11H10.5V8.00001V4H5V16H9.79076C9.93763 16.3628 10.1362 16.6992 10.3773 17H4.5H4V16.5V3.5V3ZM11.5 4.70711L14.2929 7.50001H11.5V4.70711ZM13.8123 17.3904L16.3123 15.3904L15.6877 14.6096L14 15.9597V12H13V15.9597L11.3123 14.6096L10.6877 15.3904L13.1877 17.3904L13.5 17.6403L13.8123 17.3904Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
'intersection-offset': (
|
'intersection-offset': (
|
||||||
<svg
|
<svg
|
||||||
viewBox="0 0 20 20"
|
viewBox="0 0 20 20"
|
||||||
|
@ -228,16 +228,30 @@ export const FileMachineProvider = ({
|
|||||||
createdPath = path
|
createdPath = path
|
||||||
await window.electron.mkdir(createdPath)
|
await window.electron.mkdir(createdPath)
|
||||||
} else {
|
} else {
|
||||||
const { name, path } = getNextFileName({
|
const isTargetPathToCloneASubPath =
|
||||||
entryName: input.targetPathToClone
|
input.targetPathToClone &&
|
||||||
? window.electron.path.basename(input.targetPathToClone)
|
input.selectedDirectory.path.indexOf(input.targetPathToClone) > -1
|
||||||
: createdName,
|
if (isTargetPathToCloneASubPath) {
|
||||||
baseDir: input.targetPathToClone
|
const { name, path } = getNextFileName({
|
||||||
? window.electron.path.dirname(input.targetPathToClone)
|
entryName: input.targetPathToClone
|
||||||
: input.selectedDirectory.path,
|
? window.electron.path.basename(input.targetPathToClone)
|
||||||
})
|
: createdName,
|
||||||
createdName = name
|
baseDir: input.targetPathToClone
|
||||||
createdPath = path
|
? window.electron.path.dirname(input.targetPathToClone)
|
||||||
|
: input.selectedDirectory.path,
|
||||||
|
})
|
||||||
|
createdName = name
|
||||||
|
createdPath = path
|
||||||
|
} else {
|
||||||
|
const { name, path } = getNextFileName({
|
||||||
|
entryName: input.targetPathToClone
|
||||||
|
? window.electron.path.basename(input.targetPathToClone)
|
||||||
|
: createdName,
|
||||||
|
baseDir: input.selectedDirectory.path,
|
||||||
|
})
|
||||||
|
createdName = name
|
||||||
|
createdPath = path
|
||||||
|
}
|
||||||
if (input.targetPathToClone) {
|
if (input.targetPathToClone) {
|
||||||
await window.electron.copyFile(
|
await window.electron.copyFile(
|
||||||
input.targetPathToClone,
|
input.targetPathToClone,
|
||||||
@ -437,19 +451,19 @@ export const FileMachineProvider = ({
|
|||||||
settings.modeling.defaultUnit.current ??
|
settings.modeling.defaultUnit.current ??
|
||||||
DEFAULT_DEFAULT_LENGTH_UNIT,
|
DEFAULT_DEFAULT_LENGTH_UNIT,
|
||||||
},
|
},
|
||||||
specialPropsForSampleCommand: {
|
specialPropsForLoadCommand: {
|
||||||
onSubmit: async (data) => {
|
onSubmit: async (data) => {
|
||||||
if (data.method === 'overwrite') {
|
if (data.method === 'overwrite' && data.content) {
|
||||||
codeManager.updateCodeStateEditor(data.code)
|
codeManager.updateCodeStateEditor(data.content)
|
||||||
await kclManager.executeCode()
|
await kclManager.executeCode()
|
||||||
await codeManager.writeToFile()
|
await codeManager.writeToFile()
|
||||||
} else if (data.method === 'newFile' && isDesktop()) {
|
} else if (data.method === 'newFile' && isDesktop()) {
|
||||||
send({
|
send({
|
||||||
type: 'Create file',
|
type: 'Create file',
|
||||||
data: {
|
data: {
|
||||||
name: data.sampleName,
|
...data,
|
||||||
content: data.code,
|
|
||||||
makeDir: false,
|
makeDir: false,
|
||||||
|
shouldSetToRename: false,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -480,7 +494,7 @@ export const FileMachineProvider = ({
|
|||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
}).filter(
|
}).filter(
|
||||||
(command) => kclSamples.length || command.name !== 'open-kcl-example'
|
(command) => kclSamples.length || command.name !== 'load-external-model'
|
||||||
),
|
),
|
||||||
[codeManager, kclManager, send, kclSamples, project, file]
|
[codeManager, kclManager, send, kclSamples, project, file]
|
||||||
)
|
)
|
||||||
|
@ -90,13 +90,13 @@ export const KclEditorMenu = ({ children }: PropsWithChildren) => {
|
|||||||
type: 'Find and select command',
|
type: 'Find and select command',
|
||||||
data: {
|
data: {
|
||||||
groupId: 'code',
|
groupId: 'code',
|
||||||
name: 'open-kcl-example',
|
name: 'load-external-model',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
className={styles.button}
|
className={styles.button}
|
||||||
>
|
>
|
||||||
<span>Load a sample model</span>
|
<span>Load external model</span>
|
||||||
</button>
|
</button>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<Menu.Item>
|
<Menu.Item>
|
||||||
|
@ -15,7 +15,6 @@ import type {
|
|||||||
} from '@src/components/ModelingSidebar/ModelingPanes'
|
} from '@src/components/ModelingSidebar/ModelingPanes'
|
||||||
import { sidebarPanes } from '@src/components/ModelingSidebar/ModelingPanes'
|
import { sidebarPanes } from '@src/components/ModelingSidebar/ModelingPanes'
|
||||||
import Tooltip from '@src/components/Tooltip'
|
import Tooltip from '@src/components/Tooltip'
|
||||||
import { DEV } from '@src/env'
|
|
||||||
import { useModelingContext } from '@src/hooks/useModelingContext'
|
import { useModelingContext } from '@src/hooks/useModelingContext'
|
||||||
import { useNetworkContext } from '@src/hooks/useNetworkContext'
|
import { useNetworkContext } from '@src/hooks/useNetworkContext'
|
||||||
import { NetworkHealthState } from '@src/hooks/useNetworkStatus'
|
import { NetworkHealthState } from '@src/hooks/useNetworkStatus'
|
||||||
@ -26,7 +25,6 @@ import { isDesktop } from '@src/lib/isDesktop'
|
|||||||
import { useSettings } from '@src/machines/appMachine'
|
import { useSettings } from '@src/machines/appMachine'
|
||||||
import { commandBarActor } from '@src/machines/commandBarMachine'
|
import { commandBarActor } from '@src/machines/commandBarMachine'
|
||||||
import { onboardingPaths } from '@src/routes/Onboarding/paths'
|
import { onboardingPaths } from '@src/routes/Onboarding/paths'
|
||||||
import { IS_NIGHTLY_OR_DEBUG } from '@src/routes/utils'
|
|
||||||
|
|
||||||
interface ModelingSidebarProps {
|
interface ModelingSidebarProps {
|
||||||
paneOpacity: '' | 'opacity-20' | 'opacity-40'
|
paneOpacity: '' | 'opacity-20' | 'opacity-40'
|
||||||
@ -77,16 +75,15 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
|
|||||||
|
|
||||||
const sidebarActions: SidebarAction[] = [
|
const sidebarActions: SidebarAction[] = [
|
||||||
{
|
{
|
||||||
id: 'insert',
|
id: 'load-external-model',
|
||||||
title: 'Insert from project file',
|
title: 'Load external model',
|
||||||
sidebarName: 'Insert from project file',
|
sidebarName: 'Load external model',
|
||||||
icon: 'import',
|
icon: 'importFile',
|
||||||
keybinding: 'Ctrl + Shift + I',
|
keybinding: 'Ctrl + Shift + I',
|
||||||
hide: (a) => a.platform === 'web' || !(DEV || IS_NIGHTLY_OR_DEBUG),
|
|
||||||
action: () =>
|
action: () =>
|
||||||
commandBarActor.send({
|
commandBarActor.send({
|
||||||
type: 'Find and select command',
|
type: 'Find and select command',
|
||||||
data: { name: 'Insert', groupId: 'code' },
|
data: { name: 'load-external-model', groupId: 'code' },
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -344,6 +344,15 @@ export type CommandArgument<
|
|||||||
machineContext?: ContextFrom<T>
|
machineContext?: ContextFrom<T>
|
||||||
) => OutputType)
|
) => OutputType)
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
inputType: 'path'
|
||||||
|
defaultValue?:
|
||||||
|
| OutputType
|
||||||
|
| ((
|
||||||
|
commandBarContext: ContextFrom<typeof commandBarMachine>,
|
||||||
|
machineContext?: ContextFrom<T>
|
||||||
|
) => OutputType)
|
||||||
|
}
|
||||||
| {
|
| {
|
||||||
inputType: 'text'
|
inputType: 'text'
|
||||||
defaultValue?:
|
defaultValue?:
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { relevantFileExtensions } from '@src/lang/wasmUtils'
|
||||||
import {
|
import {
|
||||||
FILE_EXT,
|
FILE_EXT,
|
||||||
INDEX_IDENTIFIER,
|
INDEX_IDENTIFIER,
|
||||||
@ -200,14 +201,20 @@ export function getNextFileName({
|
|||||||
entryName: string
|
entryName: string
|
||||||
baseDir: string
|
baseDir: string
|
||||||
}) {
|
}) {
|
||||||
|
// Preserve the extension in case of a relevant but foreign file
|
||||||
|
let extension = window.electron.path.extname(entryName)
|
||||||
|
if (!relevantFileExtensions().includes(extension.replace('.', ''))) {
|
||||||
|
extension = FILE_EXT
|
||||||
|
}
|
||||||
|
|
||||||
// Remove any existing index from the name before adding a new one
|
// Remove any existing index from the name before adding a new one
|
||||||
let createdName = entryName.replace(FILE_EXT, '') + FILE_EXT
|
let createdName = entryName.replace(extension, '') + extension
|
||||||
let createdPath = window.electron.path.join(baseDir, createdName)
|
let createdPath = window.electron.path.join(baseDir, createdName)
|
||||||
let i = 1
|
let i = 1
|
||||||
while (window.electron.exists(createdPath)) {
|
while (window.electron.exists(createdPath)) {
|
||||||
const matchOnIndexAndExtension = new RegExp(`(-\\d+)?(${FILE_EXT})?$`)
|
const matchOnIndexAndExtension = new RegExp(`(-\\d+)?(${extension})?$`)
|
||||||
createdName =
|
createdName =
|
||||||
entryName.replace(matchOnIndexAndExtension, '') + `-${i}` + FILE_EXT
|
entryName.replace(matchOnIndexAndExtension, '') + `-${i}` + extension
|
||||||
createdPath = window.electron.path.join(baseDir, createdName)
|
createdPath = window.electron.path.join(baseDir, createdName)
|
||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
|
@ -28,16 +28,17 @@ import type { CommandBarContext } from '@src/machines/commandBarMachine'
|
|||||||
import { IS_NIGHTLY_OR_DEBUG } from '@src/routes/utils'
|
import { IS_NIGHTLY_OR_DEBUG } from '@src/routes/utils'
|
||||||
|
|
||||||
interface OnSubmitProps {
|
interface OnSubmitProps {
|
||||||
sampleName: string
|
name: string
|
||||||
code: string
|
content?: string
|
||||||
sampleUnits?: UnitLength_type
|
targetPathToClone?: string
|
||||||
method: 'overwrite' | 'newFile'
|
method: 'overwrite' | 'newFile'
|
||||||
|
source: 'kcl-samples' | 'local'
|
||||||
}
|
}
|
||||||
|
|
||||||
interface KclCommandConfig {
|
interface KclCommandConfig {
|
||||||
// TODO: find a different approach that doesn't require
|
// TODO: find a different approach that doesn't require
|
||||||
// special props for a single command
|
// special props for a single command
|
||||||
specialPropsForSampleCommand: {
|
specialPropsForLoadCommand: {
|
||||||
onSubmit: (p: OnSubmitProps) => Promise<void>
|
onSubmit: (p: OnSubmitProps) => Promise<void>
|
||||||
providedOptions: CommandArgumentOption<string>[]
|
providedOptions: CommandArgumentOption<string>[]
|
||||||
}
|
}
|
||||||
@ -170,69 +171,108 @@ export function kclCommands(commandProps: KclCommandConfig): Command[] {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'open-kcl-example',
|
name: 'load-external-model',
|
||||||
displayName: 'Open sample',
|
displayName: 'Load external model',
|
||||||
description: 'Imports an example KCL program into the editor.',
|
description:
|
||||||
|
'Loads a model from an external source into the current project.',
|
||||||
needsReview: true,
|
needsReview: true,
|
||||||
icon: 'code',
|
icon: 'importFile',
|
||||||
reviewMessage: ({ argumentsToSubmit }) =>
|
reviewMessage: ({ argumentsToSubmit }) =>
|
||||||
CommandBarOverwriteWarning({
|
argumentsToSubmit['method'] === 'overwrite'
|
||||||
heading:
|
? CommandBarOverwriteWarning({
|
||||||
'method' in argumentsToSubmit &&
|
heading: 'Overwrite current file with sample?',
|
||||||
argumentsToSubmit.method === 'newFile'
|
message:
|
||||||
? 'Create a new file from sample?'
|
'This will erase your current file and load the sample part.',
|
||||||
: 'Overwrite current file with sample?',
|
})
|
||||||
message:
|
: 'This will create a new file in the current project and open it.',
|
||||||
'method' in argumentsToSubmit &&
|
|
||||||
argumentsToSubmit.method === 'newFile'
|
|
||||||
? 'This will create a new file in the current project and open it.'
|
|
||||||
: 'This will erase your current file and load the sample part.',
|
|
||||||
}),
|
|
||||||
groupId: 'code',
|
groupId: 'code',
|
||||||
onSubmit(data) {
|
onSubmit(data) {
|
||||||
if (!data?.sample) {
|
if (!data) {
|
||||||
return
|
return new Error('No input data')
|
||||||
}
|
}
|
||||||
const pathParts = data.sample.split('/')
|
|
||||||
const projectPathPart = pathParts[0]
|
|
||||||
const primaryKclFile = pathParts[1]
|
|
||||||
// local only
|
|
||||||
const sampleCodeUrl =
|
|
||||||
(isDesktop() ? '.' : '') +
|
|
||||||
`/kcl-samples/${encodeURIComponent(
|
|
||||||
projectPathPart
|
|
||||||
)}/${encodeURIComponent(primaryKclFile)}`
|
|
||||||
|
|
||||||
fetch(sampleCodeUrl)
|
const { method, source, sample, path } = data
|
||||||
.then(async (codeResponse): Promise<OnSubmitProps> => {
|
if (source === 'local' && path) {
|
||||||
if (!codeResponse.ok) {
|
commandProps.specialPropsForLoadCommand
|
||||||
console.error(
|
.onSubmit({
|
||||||
'Failed to fetch sample code:',
|
name: '',
|
||||||
codeResponse.statusText
|
targetPathToClone: path,
|
||||||
)
|
method,
|
||||||
return Promise.reject(new Error('Failed to fetch sample code'))
|
source,
|
||||||
}
|
})
|
||||||
const code = await codeResponse.text()
|
.catch(reportError)
|
||||||
return {
|
} else if (source === 'kcl-samples' && sample) {
|
||||||
sampleName: data.sample.split('/')[0] + FILE_EXT,
|
const pathParts = sample.split('/')
|
||||||
code,
|
const projectPathPart = pathParts[0]
|
||||||
method: data.method,
|
const primaryKclFile = pathParts[1]
|
||||||
}
|
// local only
|
||||||
})
|
const sampleCodeUrl =
|
||||||
.then((props) => {
|
(isDesktop() ? '.' : '') +
|
||||||
if (props?.code) {
|
`/kcl-samples/${encodeURIComponent(
|
||||||
commandProps.specialPropsForSampleCommand
|
projectPathPart
|
||||||
.onSubmit(props)
|
)}/${encodeURIComponent(primaryKclFile)}`
|
||||||
|
|
||||||
|
fetch(sampleCodeUrl)
|
||||||
|
.then(async (codeResponse) => {
|
||||||
|
if (!codeResponse.ok) {
|
||||||
|
console.error(
|
||||||
|
'Failed to fetch sample code:',
|
||||||
|
codeResponse.statusText
|
||||||
|
)
|
||||||
|
return Promise.reject(new Error('Failed to fetch sample code'))
|
||||||
|
}
|
||||||
|
const code = await codeResponse.text()
|
||||||
|
commandProps.specialPropsForLoadCommand
|
||||||
|
.onSubmit({
|
||||||
|
name: data.sample.split('/')[0] + FILE_EXT,
|
||||||
|
content: code,
|
||||||
|
source,
|
||||||
|
method,
|
||||||
|
})
|
||||||
.catch(reportError)
|
.catch(reportError)
|
||||||
}
|
})
|
||||||
})
|
.catch(reportError)
|
||||||
.catch(reportError)
|
} else {
|
||||||
|
toast.error("The command couldn't be submitted, check the arguments.")
|
||||||
|
}
|
||||||
},
|
},
|
||||||
args: {
|
args: {
|
||||||
method: {
|
source: {
|
||||||
inputType: 'options',
|
inputType: 'options',
|
||||||
required: true,
|
required: true,
|
||||||
|
skip: false,
|
||||||
|
defaultValue: 'local',
|
||||||
|
hidden: !isDesktop(),
|
||||||
|
options() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
value: 'kcl-samples',
|
||||||
|
name: 'KCL Samples',
|
||||||
|
isCurrent: true,
|
||||||
|
},
|
||||||
|
...(isDesktop()
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
value: 'local',
|
||||||
|
name: 'Local Drive',
|
||||||
|
isCurrent: false,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
method: {
|
||||||
|
inputType: 'options',
|
||||||
skip: true,
|
skip: true,
|
||||||
|
required: (commandContext) =>
|
||||||
|
!['local'].includes(
|
||||||
|
commandContext.argumentsToSubmit.source as string
|
||||||
|
),
|
||||||
|
hidden: (commandContext) =>
|
||||||
|
['local'].includes(
|
||||||
|
commandContext.argumentsToSubmit.source as string
|
||||||
|
),
|
||||||
defaultValue: isDesktop() ? 'newFile' : 'overwrite',
|
defaultValue: isDesktop() ? 'newFile' : 'overwrite',
|
||||||
options() {
|
options() {
|
||||||
return [
|
return [
|
||||||
@ -255,7 +295,14 @@ export function kclCommands(commandProps: KclCommandConfig): Command[] {
|
|||||||
},
|
},
|
||||||
sample: {
|
sample: {
|
||||||
inputType: 'options',
|
inputType: 'options',
|
||||||
required: true,
|
required: (commandContext) =>
|
||||||
|
!['local'].includes(
|
||||||
|
commandContext.argumentsToSubmit.source as string
|
||||||
|
),
|
||||||
|
hidden: (commandContext) =>
|
||||||
|
['local'].includes(
|
||||||
|
commandContext.argumentsToSubmit.source as string
|
||||||
|
),
|
||||||
valueSummary(value) {
|
valueSummary(value) {
|
||||||
const MAX_LENGTH = 12
|
const MAX_LENGTH = 12
|
||||||
if (typeof value === 'string') {
|
if (typeof value === 'string') {
|
||||||
@ -265,7 +312,15 @@ export function kclCommands(commandProps: KclCommandConfig): Command[] {
|
|||||||
}
|
}
|
||||||
return value
|
return value
|
||||||
},
|
},
|
||||||
options: commandProps.specialPropsForSampleCommand.providedOptions,
|
options: commandProps.specialPropsForLoadCommand.providedOptions,
|
||||||
|
},
|
||||||
|
path: {
|
||||||
|
inputType: 'path',
|
||||||
|
valueSummary: (value) => window.electron.path.basename(value),
|
||||||
|
required: (commandContext) =>
|
||||||
|
['local'].includes(
|
||||||
|
commandContext.argumentsToSubmit.source as string
|
||||||
|
),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -3,6 +3,7 @@ import type { EventFrom, StateFrom } from 'xstate'
|
|||||||
|
|
||||||
import type { CustomIconName } from '@src/components/CustomIcon'
|
import type { CustomIconName } from '@src/components/CustomIcon'
|
||||||
import { createLiteral } from '@src/lang/create'
|
import { createLiteral } from '@src/lang/create'
|
||||||
|
import { isDesktop } from '@src/lib/isDesktop'
|
||||||
import { commandBarActor } from '@src/machines/commandBarMachine'
|
import { commandBarActor } from '@src/machines/commandBarMachine'
|
||||||
import type { modelingMachine } from '@src/machines/modelingMachine'
|
import type { modelingMachine } from '@src/machines/modelingMachine'
|
||||||
import {
|
import {
|
||||||
@ -337,6 +338,50 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
|||||||
links: [{ label: 'KCL docs', url: 'https://zoo.dev/docs/kcl/helix' }],
|
links: [{ label: 'KCL docs', url: 'https://zoo.dev/docs/kcl/helix' }],
|
||||||
},
|
},
|
||||||
'break',
|
'break',
|
||||||
|
{
|
||||||
|
id: 'modules',
|
||||||
|
array: [
|
||||||
|
{
|
||||||
|
id: 'insert',
|
||||||
|
onClick: () =>
|
||||||
|
commandBarActor.send({
|
||||||
|
type: 'Find and select command',
|
||||||
|
data: { name: 'Insert', groupId: 'code' },
|
||||||
|
}),
|
||||||
|
hotkey: 'I',
|
||||||
|
icon: 'import',
|
||||||
|
status: DEV || IS_NIGHTLY_OR_DEBUG ? 'available' : 'kcl-only',
|
||||||
|
disabled: () => !isDesktop(),
|
||||||
|
title: 'Insert',
|
||||||
|
description: 'Insert from a file in the current project directory',
|
||||||
|
links: [
|
||||||
|
{
|
||||||
|
label: 'API docs',
|
||||||
|
url: 'https://zoo.dev/docs/kcl/import',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'transform',
|
||||||
|
icon: 'angle',
|
||||||
|
status: 'kcl-only',
|
||||||
|
title: 'Transform',
|
||||||
|
description: 'Apply a translation and/or rotation to a module',
|
||||||
|
onClick: () => undefined,
|
||||||
|
links: [
|
||||||
|
{
|
||||||
|
label: 'API docs',
|
||||||
|
url: 'https://zoo.dev/docs/kcl/translate',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'API docs',
|
||||||
|
url: 'https://zoo.dev/docs/kcl/rotate',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
'break',
|
||||||
{
|
{
|
||||||
id: 'ai',
|
id: 'ai',
|
||||||
array: [
|
array: [
|
||||||
|
@ -24,8 +24,7 @@ export type MenuLabels =
|
|||||||
| 'File.Sign out'
|
| 'File.Sign out'
|
||||||
| 'File.Create new file'
|
| 'File.Create new file'
|
||||||
| 'File.Create new folder'
|
| 'File.Create new folder'
|
||||||
| 'File.Load a sample model'
|
| 'File.Load external model'
|
||||||
| 'File.Insert from project file'
|
|
||||||
| 'File.Export current part'
|
| 'File.Export current part'
|
||||||
| 'File.Share current part (via Zoo link)'
|
| 'File.Share current part (via Zoo link)'
|
||||||
| 'File.Preferences.Project settings'
|
| 'File.Preferences.Project settings'
|
||||||
@ -40,6 +39,7 @@ export type MenuLabels =
|
|||||||
| 'Design.Apply modification feature.Fillet'
|
| 'Design.Apply modification feature.Fillet'
|
||||||
| 'Design.Apply modification feature.Chamfer'
|
| 'Design.Apply modification feature.Chamfer'
|
||||||
| 'Design.Apply modification feature.Shell'
|
| 'Design.Apply modification feature.Shell'
|
||||||
|
| 'Design.Insert from project file'
|
||||||
| 'Design.Create with Zoo Text-To-CAD'
|
| 'Design.Create with Zoo Text-To-CAD'
|
||||||
| 'Design.Modify with Zoo Text-To-CAD'
|
| 'Design.Modify with Zoo Text-To-CAD'
|
||||||
| 'View.Command Palette...'
|
| 'View.Command Palette...'
|
||||||
|
@ -122,6 +122,16 @@ export const modelingDesignRole = (
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
|
{
|
||||||
|
label: 'Insert from project file',
|
||||||
|
id: 'Design.Insert from project file',
|
||||||
|
click: () => {
|
||||||
|
typeSafeWebContentsSend(mainWindow, 'menu-action-clicked', {
|
||||||
|
menuLabel: 'Design.Insert from project file',
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ type: 'separator' },
|
||||||
{
|
{
|
||||||
label: 'Create with Zoo Text-To-CAD',
|
label: 'Create with Zoo Text-To-CAD',
|
||||||
id: 'Design.Create with Zoo Text-To-CAD',
|
id: 'Design.Create with Zoo Text-To-CAD',
|
||||||
|
@ -148,21 +148,11 @@ export const modelingFileRole = (
|
|||||||
// Appears to be only Windows and Mac OS specific. Linux does not have support
|
// Appears to be only Windows and Mac OS specific. Linux does not have support
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
{
|
{
|
||||||
label: 'Load a sample model',
|
label: 'Load external model',
|
||||||
id: 'File.Load a sample model',
|
id: 'File.Load external model',
|
||||||
click: () => {
|
click: () => {
|
||||||
typeSafeWebContentsSend(mainWindow, 'menu-action-clicked', {
|
typeSafeWebContentsSend(mainWindow, 'menu-action-clicked', {
|
||||||
menuLabel: 'File.Load a sample model',
|
menuLabel: 'File.Load external model',
|
||||||
})
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ type: 'separator' },
|
|
||||||
{
|
|
||||||
label: 'Insert from project file',
|
|
||||||
id: 'File.Insert from project file',
|
|
||||||
click: () => {
|
|
||||||
typeSafeWebContentsSend(mainWindow, 'menu-action-clicked', {
|
|
||||||
menuLabel: 'File.Insert from project file',
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -92,12 +92,12 @@ export function modelingMenuCallbackMostActions(
|
|||||||
}).catch(reportRejection)
|
}).catch(reportRejection)
|
||||||
} else if (data.menuLabel === 'File.Preferences.User default units') {
|
} else if (data.menuLabel === 'File.Preferences.User default units') {
|
||||||
navigate(filePath + PATHS.SETTINGS_USER + '#defaultUnit')
|
navigate(filePath + PATHS.SETTINGS_USER + '#defaultUnit')
|
||||||
} else if (data.menuLabel === 'File.Insert from project file') {
|
} else if (data.menuLabel === 'File.Load external model') {
|
||||||
commandBarActor.send({
|
commandBarActor.send({
|
||||||
type: 'Find and select command',
|
type: 'Find and select command',
|
||||||
data: {
|
data: {
|
||||||
groupId: 'code',
|
groupId: 'code',
|
||||||
name: 'Insert',
|
name: 'load-external-model',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
} else if (data.menuLabel === 'File.Export current part') {
|
} else if (data.menuLabel === 'File.Export current part') {
|
||||||
@ -108,14 +108,6 @@ export function modelingMenuCallbackMostActions(
|
|||||||
name: 'Export',
|
name: 'Export',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
} else if (data.menuLabel === 'File.Load a sample model') {
|
|
||||||
commandBarActor.send({
|
|
||||||
type: 'Find and select command',
|
|
||||||
data: {
|
|
||||||
groupId: 'code',
|
|
||||||
name: 'open-kcl-example',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} else if (data.menuLabel === 'File.Create new file') {
|
} else if (data.menuLabel === 'File.Create new file') {
|
||||||
// NO OP. A safe command bar create new file is not implemented yet.
|
// NO OP. A safe command bar create new file is not implemented yet.
|
||||||
} else if (data.menuLabel === 'Edit.Modify with Zoo Text-To-CAD') {
|
} else if (data.menuLabel === 'Edit.Modify with Zoo Text-To-CAD') {
|
||||||
@ -256,6 +248,14 @@ export function modelingMenuCallbackMostActions(
|
|||||||
type: 'Find and select command',
|
type: 'Find and select command',
|
||||||
data: { name: 'Shell', groupId: 'modeling' },
|
data: { name: 'Shell', groupId: 'modeling' },
|
||||||
})
|
})
|
||||||
|
} else if (data.menuLabel === 'Design.Insert from project file') {
|
||||||
|
commandBarActor.send({
|
||||||
|
type: 'Find and select command',
|
||||||
|
data: {
|
||||||
|
groupId: 'code',
|
||||||
|
name: 'Insert',
|
||||||
|
},
|
||||||
|
})
|
||||||
} else if (data.menuLabel === 'Design.Create with Zoo Text-To-CAD') {
|
} else if (data.menuLabel === 'Design.Create with Zoo Text-To-CAD') {
|
||||||
commandBarActor.send({
|
commandBarActor.send({
|
||||||
type: 'Find and select command',
|
type: 'Find and select command',
|
||||||
|
@ -21,13 +21,12 @@ type FileRoleLabel =
|
|||||||
| 'Sign out'
|
| 'Sign out'
|
||||||
| 'Theme'
|
| 'Theme'
|
||||||
| 'Theme color'
|
| 'Theme color'
|
||||||
| 'Insert from project file'
|
|
||||||
| 'Export current part'
|
| 'Export current part'
|
||||||
| 'Create new file'
|
| 'Create new file'
|
||||||
| 'Create new folder'
|
| 'Create new folder'
|
||||||
| 'Share current part (via Zoo link)'
|
| 'Share current part (via Zoo link)'
|
||||||
| 'Project settings'
|
| 'Project settings'
|
||||||
| 'Load a sample model'
|
| 'Load external model'
|
||||||
| 'User default units'
|
| 'User default units'
|
||||||
|
|
||||||
type EditRoleLabel =
|
type EditRoleLabel =
|
||||||
@ -82,6 +81,7 @@ type ViewRoleLabel =
|
|||||||
type DesignRoleLabel =
|
type DesignRoleLabel =
|
||||||
| 'Design'
|
| 'Design'
|
||||||
| 'Create a parameter'
|
| 'Create a parameter'
|
||||||
|
| 'Insert from project file'
|
||||||
| 'Create with Zoo Text-To-CAD'
|
| 'Create with Zoo Text-To-CAD'
|
||||||
| 'Start sketch'
|
| 'Start sketch'
|
||||||
| 'Create an offset plane'
|
| 'Create an offset plane'
|
||||||
|