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" /> -