diff --git a/e2e/playwright/fixtures/toolbarFixture.ts b/e2e/playwright/fixtures/toolbarFixture.ts
index 37bee58c7..ed7bffe79 100644
--- a/e2e/playwright/fixtures/toolbarFixture.ts
+++ b/e2e/playwright/fixtures/toolbarFixture.ts
@@ -73,7 +73,7 @@ export class ToolbarFixture {
this.fileTreeBtn = page.locator('[id="files-button-holder"]')
this.createFileBtn = page.getByTestId('create-file-button')
this.treeInputField = page.getByTestId('tree-input-field')
- this.loadButton = page.getByTestId('load-external-model-pane-button')
+ this.loadButton = page.getByTestId('add-file-to-project-pane-button')
this.filePane = page.locator('#files-pane')
this.featureTreePane = page.locator('#feature-tree-pane')
diff --git a/e2e/playwright/native-file-menu.spec.ts b/e2e/playwright/native-file-menu.spec.ts
index 61aa216d4..d9967f06e 100644
--- a/e2e/playwright/native-file-menu.spec.ts
+++ b/e2e/playwright/native-file-menu.spec.ts
@@ -550,7 +550,7 @@ test.describe(
const expected = 'Open project'
expect(actual).toBe(expected)
})
- test('Modeling.File.Load external model', async ({
+ test('Modeling.File.Add file to project', async ({
tronApp,
cmdBar,
page,
@@ -571,10 +571,10 @@ test.describe(
throw new Error('app or app.applicationMenu is missing')
}
const openProject = app.applicationMenu.getMenuItemById(
- 'File.Load external model'
+ 'File.Add file to project'
)
if (!openProject) {
- throw new Error('File.Load external model')
+ throw new Error('File.Add file to project')
}
openProject.click()
})
@@ -584,7 +584,7 @@ test.describe(
const actual = await cmdBar.cmdBarElement
.getByTestId('command-name')
.textContent()
- const expected = 'Load external model'
+ const expected = 'Add file to project'
expect(actual).toBe(expected)
})
test('Modeling.File.Export current part', async ({
diff --git a/e2e/playwright/testing-samples-loading.spec.ts b/e2e/playwright/testing-samples-loading.spec.ts
index 7e33d179b..8ab73afc9 100644
--- a/e2e/playwright/testing-samples-loading.spec.ts
+++ b/e2e/playwright/testing-samples-loading.spec.ts
@@ -23,7 +23,6 @@ test.describe('Testing loading external models', () => {
'Web: should overwrite current code, cannot create new file',
async ({ editor, context, page, homePage }) => {
const u = await getUtils(page)
-
await test.step(`Test setup`, async () => {
await context.addInitScript((code) => {
window.localStorage.setItem('persistCode', code)
@@ -82,12 +81,13 @@ test.describe('Testing loading external models', () => {
* "gear-rack": https://github.com/KittyCAD/kcl-samples/blob/main/gear-rack/main.kcl
*/
test(
- 'Desktop: should create new file by default, optionally overwrite',
+ 'Desktop: should create new file by default, creates a second file with automatic unique name',
{ tag: '@electron' },
async ({ editor, context, page, scene, cmdBar, toolbar }) => {
if (runningOnWindows()) {
}
- const { dir } = await context.folderSetupFn(async (dir) => {
+
+ await context.folderSetupFn(async (dir) => {
const bracketDir = join(dir, 'bracket')
await fsp.mkdir(bracketDir, { recursive: true })
await fsp.writeFile(join(bracketDir, 'main.kcl'), bracket, {
@@ -100,37 +100,28 @@ test.describe('Testing loading external models', () => {
const sampleOne = {
file: 'parametric-bearing-pillow-block' + FILE_EXT,
title: 'Parametric Bearing Pillow Block',
- }
- const sampleTwo = {
- file: 'gear-rack' + FILE_EXT,
- title: '100mm Gear Rack',
+ file1: 'parametric-bearing-pillow-block-1' + FILE_EXT,
}
const projectCard = page.getByRole('link', { name: 'bracket' })
- const commandMethodArgButton = page.getByRole('button', {
- name: 'Method',
- })
- const commandMethodOption = page.getByRole('option', {
- name: 'Overwrite',
- })
const overwriteWarning = page.getByText(
'Overwrite current file with sample?'
)
- const confirmButton = page.getByRole('button', { name: 'Submit command' })
const projectMenuButton = page.getByTestId('project-sidebar-toggle')
const newlyCreatedFile = (name: string) =>
page.getByRole('listitem').filter({
has: page.getByRole('button', { name }),
})
const defaultLoadCmdBarState: CmdBarSerialised = {
- commandName: 'Load external model',
- currentArgKey: 'source',
+ commandName: 'Add file to project',
+ currentArgKey: 'sample',
currentArgValue: '',
headerArguments: {
- Method: 'newFile',
+ Method: 'Existing project',
Sample: '',
- Source: '',
+ Source: 'kcl-samples',
+ ProjectName: 'bracket',
},
- highlightedHeaderArg: 'source',
+ highlightedHeaderArg: 'sample',
stage: 'arguments',
}
@@ -152,11 +143,10 @@ test.describe('Testing loading external models', () => {
await test.step(`Load a KCL sample with the command palette`, async () => {
await toolbar.loadButton.click()
+ await cmdBar.selectOption({ name: 'KCL Samples' }).click()
await cmdBar.expectState(defaultLoadCmdBarState)
- await cmdBar.progressCmdBar()
await cmdBar.selectOption({ name: sampleOne.title }).click()
await expect(overwriteWarning).not.toBeVisible()
- await cmdBar.progressCmdBar()
await page.waitForTimeout(1000)
})
@@ -166,33 +156,19 @@ test.describe('Testing loading external models', () => {
await expect(projectMenuButton).toContainText(sampleOne.file)
})
- await test.step(`Now overwrite the current file`, async () => {
+ await test.step(`Load a KCL sample with the command palette`, async () => {
await toolbar.loadButton.click()
+ await cmdBar.selectOption({ name: 'KCL Samples' }).click()
await cmdBar.expectState(defaultLoadCmdBarState)
- await cmdBar.progressCmdBar()
- await cmdBar.selectOption({ name: sampleTwo.title }).click()
- await commandMethodArgButton.click()
- await commandMethodOption.click()
- await expect(commandMethodArgButton).toContainText('overwrite')
- await expect(overwriteWarning).toBeVisible()
- await confirmButton.click()
+ await cmdBar.selectOption({ name: sampleOne.title }).click()
+ await expect(overwriteWarning).not.toBeVisible()
+ await page.waitForTimeout(1000)
})
- await test.step(`Ensure we overwrote the current file without navigating`, async () => {
- await editor.expectEditor.toContain('// ' + sampleTwo.title)
- await test.step(`Check actual file contents`, async () => {
- await expect
- .poll(async () => {
- return await fsp.readFile(
- join(dir, 'bracket', sampleOne.file),
- 'utf-8'
- )
- })
- .toContain('// ' + sampleTwo.title)
- })
- await expect(newlyCreatedFile(sampleOne.file)).toBeVisible()
- await expect(newlyCreatedFile(sampleTwo.file)).not.toBeVisible()
- await expect(projectMenuButton).toContainText(sampleOne.file)
+ await test.step(`Ensure we made and opened a new file with a unique name`, async () => {
+ await editor.expectEditor.toContain('// ' + sampleOne.title)
+ await expect(newlyCreatedFile(sampleOne.file1)).toBeVisible()
+ await expect(projectMenuButton).toContainText(sampleOne.file1)
})
}
)
@@ -226,19 +202,20 @@ test.describe('Testing loading external models', () => {
async function loadExternalFileThroughCommandBar(tronApp: ElectronZoo) {
await toolbar.loadButton.click()
+ await cmdBar.selectOption({ name: 'Local Drive' }).click()
await cmdBar.expectState({
- commandName: 'Load external model',
- currentArgKey: 'source',
+ commandName: 'Add file to project',
+ currentArgKey: 'pathOpen file',
currentArgValue: '',
headerArguments: {
- Method: 'newFile',
- Sample: '',
- Source: '',
+ Method: 'Existing project',
+ Path: '',
+ Source: 'local',
+ ProjectName: 'testDefault',
},
- highlightedHeaderArg: 'source',
+ highlightedHeaderArg: 'path',
stage: 'arguments',
})
- await cmdBar.selectOption({ name: 'Local Drive' }).click()
// Mock the file picker selection
const handleFile = tronApp.electron.evaluate(
@@ -251,14 +228,18 @@ test.describe('Testing loading external models', () => {
await page.getByTestId('cmd-bar-arg-file-button').click()
await handleFile
- await cmdBar.progressCmdBar()
await cmdBar.expectState({
- commandName: 'Load external model',
+ commandName: 'Add file to project',
+ currentArgKey: 'pathOpen file',
+ currentArgValue: '',
headerArguments: {
+ Method: 'Existing project',
+ Path: '',
Source: 'local',
- Path: modelName,
+ ProjectName: 'testDefault',
},
- stage: 'review',
+ highlightedHeaderArg: 'path',
+ stage: 'arguments',
})
await cmdBar.progressCmdBar()
}
diff --git a/src/components/CommandBar/CommandBar.tsx b/src/components/CommandBar/CommandBar.tsx
index 5bc3efc6f..90ec70615 100644
--- a/src/components/CommandBar/CommandBar.tsx
+++ b/src/components/CommandBar/CommandBar.tsx
@@ -144,7 +144,16 @@ export const CommandBar = () => {
data-testid="command-bar"
>
{commandBarState.matches('Selecting command') ? (
-
+ {
+ return (
+ // By default everything is undefined
+ // If marked explicitly as false hide
+ command.hideFromSearch === undefined ||
+ command.hideFromSearch === false
+ )
+ })}
+ />
) : commandBarState.matches('Gathering arguments') ? (
) : (
diff --git a/src/components/CommandBar/CommandBarPathInput.tsx b/src/components/CommandBar/CommandBarPathInput.tsx
index a90f02c9a..58238afce 100644
--- a/src/components/CommandBar/CommandBarPathInput.tsx
+++ b/src/components/CommandBar/CommandBarPathInput.tsx
@@ -8,6 +8,7 @@ import { isArray, toSync } from '@src/lib/utils'
import { commandBarActor, useCommandBarState } from '@src/lib/singletons'
import { useSelector } from '@xstate/react'
import type { AnyStateMachine, SnapshotFrom } from 'xstate'
+import type { OpenDialogOptions } from 'electron'
// TODO: remove the need for this selector once we decouple all actors from React
const machineContextSelector = (snapshot?: SnapshotFrom) =>
@@ -54,10 +55,16 @@ function CommandBarPathInput({
if (inputRef.current && inputRefVal && !isArray(inputRefVal)) {
inputRef.current.value = inputRefVal
} else if (inputRef.current) {
- const newPath = await window.electron.open({
+ const configuration: OpenDialogOptions = {
properties: ['openFile'],
title: 'Pick a file to load into the current project',
- })
+ }
+
+ if (arg.filters) {
+ configuration.filters = arg.filters
+ }
+
+ const newPath = await window.electron.open(configuration)
if (newPath.canceled) return
inputRef.current.value = newPath.filePaths[0]
} else {
diff --git a/src/components/FileMachineProvider.tsx b/src/components/FileMachineProvider.tsx
index a7ef49deb..57d8d256c 100644
--- a/src/components/FileMachineProvider.tsx
+++ b/src/components/FileMachineProvider.tsx
@@ -24,8 +24,6 @@ import {
} from '@src/lib/constants'
import { getProjectInfo } from '@src/lib/desktop'
import { getNextDirName, getNextFileName } from '@src/lib/desktopFS'
-import type { KclSamplesManifestItem } from '@src/lib/getKclSamplesManifest'
-import { getKclSamplesManifest } from '@src/lib/getKclSamplesManifest'
import { isDesktop } from '@src/lib/isDesktop'
import { kclCommands } from '@src/lib/kclCommands'
import { BROWSER_PATH, PATHS } from '@src/lib/paths'
@@ -59,9 +57,6 @@ export const FileMachineProvider = ({
const settings = useSettings()
const projectData = useRouteLoaderData(PATHS.FILE) as IndexLoaderData
const { project, file } = projectData
- const [kclSamples, setKclSamples] = React.useState(
- []
- )
const filePath = useAbsoluteFilePath()
// Only create the native file menus on desktop
@@ -102,12 +97,6 @@ export const FileMachineProvider = ({
useEffect(() => {
markOnce('code/didLoadFile')
- async function fetchKclSamples() {
- const manifest = await getKclSamplesManifest()
- const filteredFiles = manifest.filter((file) => !file.multipleFiles)
- setKclSamples(filteredFiles)
- }
- fetchKclSamples().catch(reportError)
}, [])
const [state, send] = useMachine(
@@ -468,28 +457,6 @@ export const FileMachineProvider = ({
settings.modeling.defaultUnit.current ??
DEFAULT_DEFAULT_LENGTH_UNIT,
},
- specialPropsForLoadCommand: {
- onSubmit: async (data) => {
- if (data.method === 'overwrite' && data.content) {
- codeManager.updateCodeStateEditor(data.content)
- await kclManager.executeCode()
- await codeManager.writeToFile()
- } else if (data.method === 'newFile' && isDesktop()) {
- send({
- type: 'Create file',
- data: {
- ...data,
- makeDir: false,
- shouldSetToRename: false,
- },
- })
- }
- },
- providedOptions: kclSamples.map((sample) => ({
- value: sample.pathFromProjectDirectoryToFirstFile,
- name: sample.title,
- })),
- },
specialPropsForInsertCommand: {
providedOptions: (isDesktop() && project?.children
? project.children
@@ -510,10 +477,8 @@ export const FileMachineProvider = ({
}
}),
},
- }).filter(
- (command) => kclSamples.length || command.name !== 'load-external-model'
- ),
- [codeManager, kclManager, send, kclSamples, project, file]
+ }),
+ [codeManager, kclManager, send, project, file]
)
useEffect(() => {
diff --git a/src/components/ModelingMachineProvider.tsx b/src/components/ModelingMachineProvider.tsx
index eea287752..e332012e4 100644
--- a/src/components/ModelingMachineProvider.tsx
+++ b/src/components/ModelingMachineProvider.tsx
@@ -1872,11 +1872,6 @@ export const ModelingMachineProvider = ({
commandName: 'Shell',
groupId: 'modeling',
},
- {
- menuLabel: 'Design.Create with Zoo Text-To-CAD',
- commandName: 'Text-to-CAD',
- groupId: 'modeling',
- },
{
menuLabel: 'Design.Modify with Zoo Text-To-CAD',
commandName: 'Prompt-to-edit',
diff --git a/src/components/ModelingSidebar/ModelingPanes/KclEditorMenu.tsx b/src/components/ModelingSidebar/ModelingPanes/KclEditorMenu.tsx
index c21e4db13..e0ffe489f 100644
--- a/src/components/ModelingSidebar/ModelingPanes/KclEditorMenu.tsx
+++ b/src/components/ModelingSidebar/ModelingPanes/KclEditorMenu.tsx
@@ -9,7 +9,7 @@ import { useConvertToVariable } from '@src/hooks/useToolbarGuards'
import { openExternalBrowserIfDesktop } from '@src/lib/openWindow'
import { kclManager } from '@src/lib/singletons'
import { reportRejection } from '@src/lib/trap'
-import { commandBarActor } from '@src/lib/singletons'
+import { commandBarActor, settingsActor } from '@src/lib/singletons'
import styles from './KclEditorMenu.module.css'
@@ -86,17 +86,23 @@ export const KclEditorMenu = ({ children }: PropsWithChildren) => {
diff --git a/src/components/ModelingSidebar/ModelingSidebar.tsx b/src/components/ModelingSidebar/ModelingSidebar.tsx
index 146e4c366..6e5eb2f68 100644
--- a/src/components/ModelingSidebar/ModelingSidebar.tsx
+++ b/src/components/ModelingSidebar/ModelingSidebar.tsx
@@ -29,6 +29,7 @@ import { reportRejection } from '@src/lib/trap'
import { refreshPage } from '@src/lib/utils'
import { hotkeyDisplay } from '@src/lib/hotkeyWrapper'
import usePlatform from '@src/hooks/usePlatform'
+import { settingsActor } from '@src/lib/singletons'
interface ModelingSidebarProps {
paneOpacity: '' | 'opacity-20' | 'opacity-40'
@@ -79,16 +80,27 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
const sidebarActions: SidebarAction[] = [
{
- id: 'load-external-model',
- title: 'Load external model',
- sidebarName: 'Load external model',
+ id: 'add-file-to-project',
+ title: 'Add file to project',
+ sidebarName: 'Add file to project',
icon: 'importFile',
keybinding: 'Mod + Alt + L',
- action: () =>
+ action: () => {
+ const currentProject =
+ settingsActor.getSnapshot().context.currentProject
commandBarActor.send({
type: 'Find and select command',
- data: { name: 'load-external-model', groupId: 'code' },
- }),
+ data: {
+ name: 'add-kcl-file-to-project',
+ groupId: 'application',
+ argDefaultValues: {
+ method: 'existingProject',
+ projectName: currentProject?.name,
+ ...(!isDesktop() ? { source: 'kcl-samples' } : {}),
+ },
+ },
+ })
+ },
},
{
id: 'export',
diff --git a/src/index.tsx b/src/index.tsx
index 1ae0620a2..84b016d8b 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -13,8 +13,9 @@ import { initializeWindowExceptionHandler } from '@src/lib/exceptions'
import { isDesktop } from '@src/lib/isDesktop'
import { markOnce } from '@src/lib/performance'
import { reportRejection } from '@src/lib/trap'
-import { appActor } from '@src/lib/singletons'
+import { appActor, systemIOActor, commandBarActor } from '@src/lib/singletons'
import reportWebVitals from '@src/reportWebVitals'
+import { createApplicationCommands } from '@src/lib/commandBarConfigs/applicationCommandConfig'
markOnce('code/willAuth')
initializeWindowExceptionHandler()
@@ -32,6 +33,14 @@ initializeWindowExceptionHandler()
initPromise
.then(() => {
appActor.start()
+ // Application commands must be created after the initPromise because
+ // it calls WASM functions to file extensions, this dependency is not available during initialization, it is an async dependency
+ commandBarActor.send({
+ type: 'Add commands',
+ data: {
+ commands: [...createApplicationCommands({ systemIOActor })],
+ },
+ })
})
.catch(reportRejection)
diff --git a/src/lib/commandBarConfigs/applicationCommandConfig.ts b/src/lib/commandBarConfigs/applicationCommandConfig.ts
index e5c744305..8352a4646 100644
--- a/src/lib/commandBarConfigs/applicationCommandConfig.ts
+++ b/src/lib/commandBarConfigs/applicationCommandConfig.ts
@@ -3,6 +3,12 @@ import type { ActorRefFrom } from 'xstate'
import type { Command, CommandArgumentOption } from '@src/lib/commandTypes'
import { SystemIOMachineEvents } from '@src/machines/systemIO/utils'
import { isDesktop } from '@src/lib/isDesktop'
+import { kclSamplesManifestWithNoMultipleFiles } from '@src/lib/kclSamples'
+import { getUniqueProjectName } from '@src/lib/desktopFS'
+import { FILE_EXT } from '@src/lib/constants'
+import toast from 'react-hot-toast'
+import { reportRejection } from '@src/lib/trap'
+import { relevantFileExtensions } from '@src/lang/wasmUtils'
export function createApplicationCommands({
systemIOActor,
@@ -79,5 +85,196 @@ export function createApplicationCommands({
},
}
- return isDesktop() ? [textToCADCommand] : [textToCADCommand]
+ const addKCLFileToProject: Command = {
+ name: 'add-kcl-file-to-project',
+ displayName: 'Add file to project',
+ description:
+ 'Add KCL file, Zoo sample, or 3D model to new or existing project.',
+ needsReview: false,
+ icon: 'importFile',
+ groupId: 'application',
+ onSubmit(data) {
+ if (data) {
+ /** TODO: Make a new machine for models. This is only a temporary location
+ * to move it to the global application level. To reduce its footprint
+ * and complexity the implementation lives here with systemIOMachine. Not
+ * inside the systemIOMachine. We can have a fancy model machine that loads
+ * KCL samples
+ */
+ const folders = systemIOActor.getSnapshot().context.folders
+ const isProjectNew = !!data.newProjectName
+ const requestedProjectName = data.newProjectName || data.projectName
+ const uniqueNameIfNeeded = isProjectNew
+ ? getUniqueProjectName(requestedProjectName, folders)
+ : requestedProjectName
+
+ if (data.source === 'kcl-samples' && data.sample) {
+ const pathParts = data.sample.split('/')
+ const projectPathPart = pathParts[0]
+ const primaryKclFile = pathParts[1]
+ const folderNameBecomesKCLFileName = projectPathPart + FILE_EXT
+
+ const sampleCodeUrl =
+ (isDesktop() ? '.' : '') +
+ `/kcl-samples/${encodeURIComponent(
+ projectPathPart
+ )}/${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()
+ systemIOActor.send({
+ type: SystemIOMachineEvents.importFileFromURL,
+ data: {
+ requestedProjectName: uniqueNameIfNeeded,
+ requestedFileName: folderNameBecomesKCLFileName,
+ requestedCode: code,
+ },
+ })
+ })
+ .catch(reportError)
+ } else if (data.source === 'local' && data.path) {
+ const clonePath = data.path
+ const fileWithExtension = clonePath.split('/').pop()
+ const readFileContentsAndCreateNewFile = async () => {
+ const text = await window.electron.readFile(clonePath, 'utf8')
+ systemIOActor.send({
+ type: SystemIOMachineEvents.importFileFromURL,
+ data: {
+ requestedProjectName: uniqueNameIfNeeded,
+ requestedFileName: fileWithExtension,
+ requestedCode: text,
+ },
+ })
+ }
+ readFileContentsAndCreateNewFile().catch(reportRejection)
+ } else {
+ toast.error("The command couldn't be submitted, check the arguments.")
+ }
+ }
+ },
+ args: {
+ source: {
+ inputType: 'options',
+ required: true,
+ skip: false,
+ defaultValue: isDesktop() ? 'local' : 'kcl-samples',
+ options() {
+ return [
+ {
+ value: 'kcl-samples',
+ name: 'KCL Samples',
+ isCurrent: true,
+ },
+ ...(isDesktop()
+ ? [
+ {
+ value: 'local',
+ name: 'Local Drive',
+ isCurrent: false,
+ },
+ ]
+ : []),
+ ]
+ },
+ },
+ method: {
+ inputType: 'options',
+ required: true,
+ skip: true,
+ options: isDesktop()
+ ? [
+ { name: 'New project', value: 'newProject', isCurrent: true },
+ { name: 'Existing project', value: 'existingProject' },
+ ]
+ : [{ name: 'Overwrite', value: 'existingProject' }],
+ valueSummary(value) {
+ return isDesktop()
+ ? value === 'newProject'
+ ? 'New project'
+ : 'Existing project'
+ : 'Overwrite'
+ },
+ },
+ projectName: {
+ inputType: 'options',
+ required: (commandsContext) =>
+ isDesktop() &&
+ commandsContext.argumentsToSubmit.method === 'existingProject',
+ skip: true,
+ options: (_, context) => {
+ const { folders } = systemIOActor.getSnapshot().context
+ const options: CommandArgumentOption[] = []
+ folders.forEach((folder) => {
+ options.push({
+ name: folder.name,
+ value: folder.name,
+ isCurrent: false,
+ })
+ })
+ return options
+ },
+ },
+ newProjectName: {
+ inputType: 'text',
+ required: (commandsContext) =>
+ isDesktop() &&
+ commandsContext.argumentsToSubmit.method === 'newProject',
+ skip: true,
+ },
+ sample: {
+ inputType: 'options',
+ required: (commandContext) =>
+ !['local'].includes(
+ commandContext.argumentsToSubmit.source as string
+ ),
+ hidden: (commandContext) =>
+ ['local'].includes(commandContext.argumentsToSubmit.source as string),
+ valueSummary(value) {
+ const MAX_LENGTH = 12
+ if (typeof value === 'string') {
+ return value.length > MAX_LENGTH
+ ? value.substring(0, MAX_LENGTH) + '...'
+ : value
+ }
+ return value
+ },
+ options: kclSamplesManifestWithNoMultipleFiles.map((sample) => {
+ return {
+ value: sample.pathFromProjectDirectoryToFirstFile,
+ name: sample.title,
+ }
+ }),
+ },
+ path: {
+ inputType: 'path',
+ skip: true,
+ hidden: !isDesktop(),
+ defaultValue: '',
+ valueSummary: (value) => {
+ return isDesktop() ? window.electron.path.basename(value) : ''
+ },
+ required: (commandContext) =>
+ isDesktop() &&
+ ['local'].includes(commandContext.argumentsToSubmit.source as string),
+ filters: [
+ {
+ name: `Import ${relevantFileExtensions().map((f) => ` .${f}`)}`,
+ extensions: relevantFileExtensions(),
+ },
+ ],
+ },
+ },
+ }
+
+ return isDesktop()
+ ? [textToCADCommand, addKCLFileToProject]
+ : [textToCADCommand, addKCLFileToProject]
}
diff --git a/src/lib/commandBarConfigs/projectsCommandConfig.ts b/src/lib/commandBarConfigs/projectsCommandConfig.ts
index 5e8907b89..d44703153 100644
--- a/src/lib/commandBarConfigs/projectsCommandConfig.ts
+++ b/src/lib/commandBarConfigs/projectsCommandConfig.ts
@@ -182,6 +182,7 @@ export function createProjectCommands({
icon: 'file',
description: 'Create a file',
needsReview: true,
+ hideFromSearch: true,
onSubmit: (record) => {
if (record) {
systemIOActor.send({
diff --git a/src/lib/commandTypes.ts b/src/lib/commandTypes.ts
index 0943b2d76..6b58f1d85 100644
--- a/src/lib/commandTypes.ts
+++ b/src/lib/commandTypes.ts
@@ -41,6 +41,12 @@ export interface KclExpressionWithVariable extends KclExpression {
export type KclCommandValue = KclExpression | KclExpressionWithVariable
export type CommandInputType = INPUT_TYPE[number]
+export type FileFilter = {
+ name: string
+ extensions: string[]
+}
+export type FiltersConfig = FileFilter[]
+
export type StateMachineCommandSetSchema = Partial<{
[EventType in EventFrom['type']]: Record
}>
@@ -96,6 +102,7 @@ export type Command<
description?: string
icon?: Icon
hide?: PLATFORM[number]
+ hideFromSearch?: boolean
}
export type CommandConfig<
@@ -373,6 +380,7 @@ export type CommandArgument<
commandBarContext: ContextFrom,
machineContext?: ContextFrom
) => OutputType)
+ filters: FiltersConfig
}
| {
inputType: 'text'
diff --git a/src/lib/kclCommands.ts b/src/lib/kclCommands.ts
index df9058e9b..751b15612 100644
--- a/src/lib/kclCommands.ts
+++ b/src/lib/kclCommands.ts
@@ -1,7 +1,6 @@
import type { UnitLength_type } from '@kittycad/lib/dist/types/src/models'
import toast from 'react-hot-toast'
-import { CommandBarOverwriteWarning } from '@src/components/CommandBarOverwriteWarning'
import { updateModelingState } from '@src/lang/modelingWorkflows'
import { addImportAndInsert } from '@src/lang/modifyAst'
import {
@@ -14,10 +13,8 @@ import {
DEFAULT_DEFAULT_ANGLE_UNIT,
DEFAULT_DEFAULT_LENGTH_UNIT,
EXECUTION_TYPE_REAL,
- FILE_EXT,
} from '@src/lib/constants'
import { getPathFilenameInVariableCase } from '@src/lib/desktop'
-import { isDesktop } from '@src/lib/isDesktop'
import { copyFileShareLink } from '@src/lib/links'
import { baseUnitsUnion } from '@src/lib/settings/settingsTypes'
import { codeManager, editorManager, kclManager } from '@src/lib/singletons'
@@ -25,21 +22,9 @@ import { err, reportRejection } from '@src/lib/trap'
import type { IndexLoaderData } from '@src/lib/types'
import type { CommandBarContext } from '@src/machines/commandBarMachine'
-interface OnSubmitProps {
- name: string
- content?: string
- targetPathToClone?: string
- method: 'overwrite' | 'newFile'
- source: 'kcl-samples' | 'local'
-}
-
interface KclCommandConfig {
// TODO: find a different approach that doesn't require
// special props for a single command
- specialPropsForLoadCommand: {
- onSubmit: (p: OnSubmitProps) => Promise
- providedOptions: CommandArgumentOption[]
- }
specialPropsForInsertCommand: {
providedOptions: CommandArgumentOption[]
}
@@ -189,160 +174,6 @@ export function kclCommands(commandProps: KclCommandConfig): Command[] {
kclManager.format().catch(reportRejection)
},
},
- {
- name: 'load-external-model',
- displayName: 'Load external model',
- description:
- 'Loads a model from an external source into the current project.',
- needsReview: true,
- icon: 'importFile',
- reviewMessage: ({ argumentsToSubmit }) =>
- argumentsToSubmit['method'] === 'overwrite'
- ? CommandBarOverwriteWarning({
- heading: 'Overwrite current file with sample?',
- message:
- 'This will erase your current file and load the sample part.',
- })
- : 'This will create a new file in the current project and open it.',
- groupId: 'code',
- onSubmit(data) {
- if (!data) {
- return new Error('No input data')
- }
-
- const { method, source, sample, path } = data
- if (source === 'local' && path) {
- commandProps.specialPropsForLoadCommand
- .onSubmit({
- name: '',
- targetPathToClone: path,
- method,
- source,
- })
- .catch(reportError)
- } else if (source === 'kcl-samples' && sample) {
- const pathParts = sample.split('/')
- const projectPathPart = pathParts[0]
- const primaryKclFile = pathParts[1]
- // local only
- const sampleCodeUrl =
- (isDesktop() ? '.' : '') +
- `/kcl-samples/${encodeURIComponent(
- projectPathPart
- )}/${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)
- } else {
- toast.error("The command couldn't be submitted, check the arguments.")
- }
- },
- args: {
- source: {
- inputType: 'options',
- 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,
- required: (commandContext) =>
- !['local'].includes(
- commandContext.argumentsToSubmit.source as string
- ),
- hidden: (commandContext) =>
- ['local'].includes(
- commandContext.argumentsToSubmit.source as string
- ),
- defaultValue: isDesktop() ? 'newFile' : 'overwrite',
- options() {
- return [
- {
- value: 'overwrite',
- name: 'Overwrite current code',
- isCurrent: !isDesktop(),
- },
- ...(isDesktop()
- ? [
- {
- value: 'newFile',
- name: 'Create a new file',
- isCurrent: true,
- },
- ]
- : []),
- ]
- },
- },
- sample: {
- inputType: 'options',
- required: (commandContext) =>
- !['local'].includes(
- commandContext.argumentsToSubmit.source as string
- ),
- hidden: (commandContext) =>
- ['local'].includes(
- commandContext.argumentsToSubmit.source as string
- ),
- valueSummary(value) {
- const MAX_LENGTH = 12
- if (typeof value === 'string') {
- return value.length > MAX_LENGTH
- ? value.substring(0, MAX_LENGTH) + '...'
- : value
- }
- return value
- },
- options: commandProps.specialPropsForLoadCommand.providedOptions,
- },
- path: {
- inputType: 'path',
- valueSummary: (value) => window.electron.path.basename(value),
- required: (commandContext) =>
- ['local'].includes(
- commandContext.argumentsToSubmit.source as string
- ),
- },
- },
- },
{
name: 'share-file-link',
displayName: 'Share part via Zoo link',
diff --git a/src/lib/kclSamples.ts b/src/lib/kclSamples.ts
new file mode 100644
index 000000000..d9c36f181
--- /dev/null
+++ b/src/lib/kclSamples.ts
@@ -0,0 +1,7 @@
+import kclSamplesManifest from '@public/kcl-samples/manifest.json'
+
+const kclSamplesManifestWithNoMultipleFiles = kclSamplesManifest.filter(
+ (file) => !file.multipleFiles
+)
+
+export { kclSamplesManifest, kclSamplesManifestWithNoMultipleFiles }
diff --git a/src/lib/singletons.ts b/src/lib/singletons.ts
index 5c73092da..c81c331a0 100644
--- a/src/lib/singletons.ts
+++ b/src/lib/singletons.ts
@@ -28,7 +28,6 @@ import type { AppMachineContext } from '@src/lib/types'
import { createAuthCommands } from '@src/lib/commandBarConfigs/authCommandConfig'
import { commandBarMachine } from '@src/machines/commandBarMachine'
import { createProjectCommands } from '@src/lib/commandBarConfigs/projectsCommandConfig'
-import { createApplicationCommands } from '@src/lib/commandBarConfigs/applicationCommandConfig'
export const codeManager = new CodeManager()
export const engineCommandManager = new EngineCommandManager()
@@ -227,7 +226,6 @@ commandBarActor.send({
commands: [
...createAuthCommands({ authActor }),
...createProjectCommands({ systemIOActor }),
- ...createApplicationCommands({ systemIOActor }),
],
},
})
diff --git a/src/machines/systemIO/systemIOMachine.ts b/src/machines/systemIO/systemIOMachine.ts
index 45e943a90..57e175082 100644
--- a/src/machines/systemIO/systemIOMachine.ts
+++ b/src/machines/systemIO/systemIOMachine.ts
@@ -469,10 +469,13 @@ export const systemIOMachine = setup({
assign({
requestedFileName: ({ context, event }) => {
assertEvent(event, SystemIOMachineEvents.done_importFileFromURL)
- // Not the entire path
+ // Gotcha: file could have an ending of .kcl...
+ const file = event.output.fileName.endsWith('.kcl')
+ ? event.output.fileName
+ : event.output.fileName + '.kcl'
return {
project: event.output.projectName,
- file: event.output.fileName + '.kcl',
+ file,
}
},
}),
diff --git a/src/machines/systemIO/systemIOMachineDesktop.ts b/src/machines/systemIO/systemIOMachineDesktop.ts
index b7dc456c2..bc42af2e5 100644
--- a/src/machines/systemIO/systemIOMachineDesktop.ts
+++ b/src/machines/systemIO/systemIOMachineDesktop.ts
@@ -204,7 +204,7 @@ export const systemIOMachineDesktop = systemIOMachine.provide({
return {
message: 'File created successfully',
- fileName: input.requestedFileName,
+ fileName: newFileName,
projectName: newProjectName,
}
}
diff --git a/src/menu/channels.ts b/src/menu/channels.ts
index bba93d874..f9ba9f5d8 100644
--- a/src/menu/channels.ts
+++ b/src/menu/channels.ts
@@ -24,7 +24,7 @@ export type MenuLabels =
| 'File.Sign out'
| 'File.Create new file'
| 'File.Create new folder'
- | 'File.Load external model'
+ | 'File.Add file to project'
| 'File.Export current part'
| 'File.Share part via Zoo link'
| 'File.Preferences.Project settings'
diff --git a/src/menu/fileRole.ts b/src/menu/fileRole.ts
index cd1e39769..781524139 100644
--- a/src/menu/fileRole.ts
+++ b/src/menu/fileRole.ts
@@ -35,6 +35,25 @@ export const projectFileRole = (
// TODO https://www.electronjs.org/docs/latest/tutorial/recent-documents
// Appears to be only Windows and Mac OS specific. Linux does not have support
{ type: 'separator' },
+ {
+ label: 'Add file to project',
+ id: 'File.Add file to project',
+ click: () => {
+ typeSafeWebContentsSend(mainWindow, 'menu-action-clicked', {
+ menuLabel: 'File.Add file to project',
+ })
+ },
+ },
+ {
+ label: 'Create with Zoo Text-To-CAD',
+ id: 'Design.Create with Zoo Text-To-CAD',
+ click: () => {
+ typeSafeWebContentsSend(mainWindow, 'menu-action-clicked', {
+ menuLabel: 'Design.Create with Zoo Text-To-CAD',
+ })
+ },
+ },
+ { type: 'separator' },
{
label: 'Preferences',
submenu: [
@@ -148,11 +167,11 @@ export const modelingFileRole = (
// Appears to be only Windows and Mac OS specific. Linux does not have support
{ type: 'separator' },
{
- label: 'Load external model',
- id: 'File.Load external model',
+ label: 'Add file to project',
+ id: 'File.Add file to project',
click: () => {
typeSafeWebContentsSend(mainWindow, 'menu-action-clicked', {
- menuLabel: 'File.Load external model',
+ menuLabel: 'File.Add file to project',
})
},
},
diff --git a/src/menu/register.ts b/src/menu/register.ts
index 8fb1dd78c..eae3a35e2 100644
--- a/src/menu/register.ts
+++ b/src/menu/register.ts
@@ -92,12 +92,17 @@ export function modelingMenuCallbackMostActions(
}).catch(reportRejection)
} else if (data.menuLabel === 'File.Preferences.User default units') {
navigate(filePath + PATHS.SETTINGS_USER + '#defaultUnit')
- } else if (data.menuLabel === 'File.Load external model') {
+ } else if (data.menuLabel === 'File.Add file to project') {
+ const currentProject = settingsActor.getSnapshot().context.currentProject
commandBarActor.send({
type: 'Find and select command',
data: {
- groupId: 'code',
- name: 'load-external-model',
+ name: 'add-kcl-file-to-project',
+ groupId: 'application',
+ argDefaultValues: {
+ method: 'existingProject',
+ projectName: currentProject?.name,
+ },
},
})
} else if (data.menuLabel === 'File.Export current part') {
@@ -257,9 +262,17 @@ export function modelingMenuCallbackMostActions(
},
})
} else if (data.menuLabel === 'Design.Create with Zoo Text-To-CAD') {
+ const currentProject = settingsActor.getSnapshot().context.currentProject
commandBarActor.send({
type: 'Find and select command',
- data: { name: 'Text-to-CAD', groupId: 'modeling' },
+ data: {
+ name: 'Text-to-CAD',
+ groupId: 'application',
+ argDefaultValues: {
+ method: 'existingProject',
+ projectName: currentProject?.name,
+ },
+ },
})
} else if (data.menuLabel === 'Design.Modify with Zoo Text-To-CAD') {
commandBarActor.send({
diff --git a/src/menu/roles.ts b/src/menu/roles.ts
index 0a6d85253..8b51ad1da 100644
--- a/src/menu/roles.ts
+++ b/src/menu/roles.ts
@@ -26,7 +26,7 @@ type FileRoleLabel =
| 'Create new folder'
| 'Share part via Zoo link'
| 'Project settings'
- | 'Load external model'
+ | 'Add file to project'
| 'User default units'
type EditRoleLabel =
diff --git a/src/routes/Home.tsx b/src/routes/Home.tsx
index 6c108b4c1..5e85e597f 100644
--- a/src/routes/Home.tsx
+++ b/src/routes/Home.tsx
@@ -142,6 +142,26 @@ const Home = () => {
})
} else if (data.menuLabel === 'File.Preferences.Theme color') {
navigate(`${PATHS.HOME}${PATHS.SETTINGS_USER}#themeColor`)
+ } else if (data.menuLabel === 'File.Add file to project') {
+ commandBarActor.send({
+ type: 'Find and select command',
+ data: {
+ name: 'add-kcl-file-to-project',
+ groupId: 'application',
+ },
+ })
+ } else if (data.menuLabel === 'Design.Create with Zoo Text-To-CAD') {
+ commandBarActor.send({
+ type: 'Find and select command',
+ data: {
+ name: 'Text-to-CAD',
+ groupId: 'application',
+ argDefaultValues: {
+ method: 'newProject',
+ newProjectName: settings.projects.defaultProjectName.current,
+ },
+ },
+ })
}
}
useMenuListener(cb)
@@ -182,7 +202,7 @@ const Home = () => {
readWriteProjectDir={readWriteProjectDir}
className="col-start-2 -col-end-1"
/>
-