[Feature]: Load external model becomes Add file to project, global application add file to project with home page update. (#6506)
* chore: saving off skeleton
* fix: saving skeleton
* chore: skeleton for loading projects from project directory path
* chore: cleaning up useless state transition to be an on event direct to action state
* fix: new structure for web vs desktop vs react machine provider code
* chore: saving off skeleton
* fix: skeleton logic for react? going to move it from a string to obj.string
* fix: trying to prevent error element unmount on global react components. This is bricking JS state
* fix: we are so back
* chore: implemented navigating to specfic KCL file
* chore: implementing renaming project
* chore: deleting project
* fix: auto fixes
* fix: old debug/testing file oops
* chore: generic create new file
* chore: skeleton for web create file provide
* chore: basic machine vitest... need to figure out how to get window.electron implemented in vitest?
* chore: save off progress before deleting other project implementation, a few missing features still
* chore: trying a different init skeleton? most likely will migrate
* chore: first attempt of purging projects context provider
* chore: enabling toast for some machine state
* chore: enabling more toast success and error
* chore: writing read write state to the system io based on the project path
* fix: tsc fixes
* fix: use file system watcher, navigate to project after creation via the requestProjectName
* chore: open project command, hooks vs snapshot context helpers
* chore: implemented open and create project for e2e testing. They are hard coded in poor spot for now.
* fix: codespell fixes
* chore: implementing more project commands
* chore: PR improvements for root.tsx
* chore: leaving comment about new Router.tsx layout
* fix: removing debugging code
* fix: rewriting component for readability
* fix: improving web initialization
* chore: implementing import file from url which is not actually that?
* fix: clearing search params on import file from url
* fix: fixed two e2e tests, forgot needsReview when making new command
* fix: fixing some import from url business logic to pass e2e tests
* chore: script for diffing circular deps +/-
* fix: formatting
* fix: massive fix for circular depsga!
* fix: trying to fix some errors and auto fmt
* fix: updating deps
* fix: removing debugging code
* fix: big clean up
* fix: more deletion
* fix: tsc cleanup
* fix: TSC TSC TSC TSC!
* fix: typo fix
* fix: clear query params on web only, desktop not required
* fix: removing unused code
* fmt
* Bring back `trap` removed in merge
* Use explicit types instead of `any`s on arg configs
* Add project commands directly to command palette
* fix: deleting debugging code, from PR review
* fix: this got added back(?)
* fix: using referred type
* fix: more PR clean up
* fix: big block comment for xstate architecture decision
* fix: more pr comment fixes
* fix: saving off logic, need a big cleanup because I hacked it together to get a POC
* fix: extra business?
* fix: merge conflict just added them back why dude
* fix: more PR comments
* fix: big ciruclar deps fix, commandBarActor in appActor
* chore: writing e2e test, still need to fix 3 bugs
* chore: adding more scenarios
* fix: formatting
* fix: fixing tsc errors
* chore: deleting the old text to cad and using the new application level one, almost there
* fix: prompt to edit works
* fix: large push to get 1 text to cad command... the usage is a little buggy with delete and navigate within /file
* fix: settings for highlight edges now works
* chore: adding another e2e test
* fix: cleaning up e2e tests and writing more of them
* fix: tsc type
* chore: more e2e improvements, unique project name  in text to cad
* chore: e2e tests should be good to go
* fix: gotcha comment
* fix: enabled web t2c, codespell fixes
* fix: fixing merge conflcits??
* feat: implemented load external for kcl samples
* feat: load external model from disk
* fix: trying to delete old stuff
* fix: all command trigger locations now have defaults for current project
* fix: gotcha comment for the future
* chore: hiding import file from url command, two separate commands for 3d and kcl file adding
* chore: commands are now add file to project, 3rd iteration
* fix: t2c in file menu fixed
* chore: updating file menu for new global actions
* fix: auto fixes
* fix: the command bar arg flow for web add kcl file seems backwards?
* chore: updated home layout, added create from kcl sample button
* chore: remapping some menu actions
* fix: fixing open dialog copy
* fix: an e2e test
* fix: fixed e2e tests
* fix: fixed e2e tests
* fix: auto fixes
* fix: pr clean up
* fix: removing console log
* fix: PR updates
* fix: the reviewed stage boolean required the expected state to change. Also I progressed the command bar too soon
* fix: no idea how this passed locally yesterday? I removed the {dir} unused but I need the function's logic but not the return value...
* fix: should be good to go?
---------
Co-authored-by: Frank Noirot <frankjohnson1993@gmail.com>
			
			
This commit is contained in:
		@ -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')
 | 
			
		||||
 | 
			
		||||
@ -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 ({
 | 
			
		||||
 | 
			
		||||
@ -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()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -144,7 +144,16 @@ export const CommandBar = () => {
 | 
			
		||||
            data-testid="command-bar"
 | 
			
		||||
          >
 | 
			
		||||
            {commandBarState.matches('Selecting command') ? (
 | 
			
		||||
              <CommandComboBox options={commands} />
 | 
			
		||||
              <CommandComboBox
 | 
			
		||||
                options={commands.filter((command) => {
 | 
			
		||||
                  return (
 | 
			
		||||
                    // By default everything is undefined
 | 
			
		||||
                    // If marked explicitly as false hide
 | 
			
		||||
                    command.hideFromSearch === undefined ||
 | 
			
		||||
                    command.hideFromSearch === false
 | 
			
		||||
                  )
 | 
			
		||||
                })}
 | 
			
		||||
              />
 | 
			
		||||
            ) : commandBarState.matches('Gathering arguments') ? (
 | 
			
		||||
              <CommandBarArgument stepBack={stepBack} />
 | 
			
		||||
            ) : (
 | 
			
		||||
 | 
			
		||||
@ -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<AnyStateMachine>) =>
 | 
			
		||||
@ -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 {
 | 
			
		||||
 | 
			
		||||
@ -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<KclSamplesManifestItem[]>(
 | 
			
		||||
    []
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  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(() => {
 | 
			
		||||
 | 
			
		||||
@ -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',
 | 
			
		||||
 | 
			
		||||
@ -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) => {
 | 
			
		||||
          <Menu.Item>
 | 
			
		||||
            <button
 | 
			
		||||
              onClick={() => {
 | 
			
		||||
                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,
 | 
			
		||||
                    },
 | 
			
		||||
                  },
 | 
			
		||||
                })
 | 
			
		||||
              }}
 | 
			
		||||
              className={styles.button}
 | 
			
		||||
            >
 | 
			
		||||
              <span>Load external model</span>
 | 
			
		||||
              <span>Add file to project</span>
 | 
			
		||||
            </button>
 | 
			
		||||
          </Menu.Item>
 | 
			
		||||
          <Menu.Item>
 | 
			
		||||
 | 
			
		||||
@ -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',
 | 
			
		||||
 | 
			
		||||
@ -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)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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<string>[] = []
 | 
			
		||||
          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]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -182,6 +182,7 @@ export function createProjectCommands({
 | 
			
		||||
    icon: 'file',
 | 
			
		||||
    description: 'Create a file',
 | 
			
		||||
    needsReview: true,
 | 
			
		||||
    hideFromSearch: true,
 | 
			
		||||
    onSubmit: (record) => {
 | 
			
		||||
      if (record) {
 | 
			
		||||
        systemIOActor.send({
 | 
			
		||||
 | 
			
		||||
@ -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<T extends AnyStateMachine> = Partial<{
 | 
			
		||||
  [EventType in EventFrom<T>['type']]: Record<string, any>
 | 
			
		||||
}>
 | 
			
		||||
@ -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<typeof commandBarMachine>,
 | 
			
		||||
            machineContext?: ContextFrom<T>
 | 
			
		||||
          ) => OutputType)
 | 
			
		||||
      filters: FiltersConfig
 | 
			
		||||
    }
 | 
			
		||||
  | {
 | 
			
		||||
      inputType: 'text'
 | 
			
		||||
 | 
			
		||||
@ -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<void>
 | 
			
		||||
    providedOptions: CommandArgumentOption<string>[]
 | 
			
		||||
  }
 | 
			
		||||
  specialPropsForInsertCommand: {
 | 
			
		||||
    providedOptions: CommandArgumentOption<string>[]
 | 
			
		||||
  }
 | 
			
		||||
@ -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',
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										7
									
								
								src/lib/kclSamples.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/lib/kclSamples.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,7 @@
 | 
			
		||||
import kclSamplesManifest from '@public/kcl-samples/manifest.json'
 | 
			
		||||
 | 
			
		||||
const kclSamplesManifestWithNoMultipleFiles = kclSamplesManifest.filter(
 | 
			
		||||
  (file) => !file.multipleFiles
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
export { kclSamplesManifest, kclSamplesManifestWithNoMultipleFiles }
 | 
			
		||||
@ -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 }),
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
@ -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,
 | 
			
		||||
                }
 | 
			
		||||
              },
 | 
			
		||||
            }),
 | 
			
		||||
 | 
			
		||||
@ -204,7 +204,7 @@ export const systemIOMachineDesktop = systemIOMachine.provide({
 | 
			
		||||
 | 
			
		||||
        return {
 | 
			
		||||
          message: 'File created successfully',
 | 
			
		||||
          fileName: input.requestedFileName,
 | 
			
		||||
          fileName: newFileName,
 | 
			
		||||
          projectName: newProjectName,
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
@ -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'
 | 
			
		||||
 | 
			
		||||
@ -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',
 | 
			
		||||
          })
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
@ -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({
 | 
			
		||||
 | 
			
		||||
@ -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 =
 | 
			
		||||
 | 
			
		||||
@ -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"
 | 
			
		||||
        />
 | 
			
		||||
        <aside className="row-start-2 -row-end-1 flex flex-col justify-between">
 | 
			
		||||
        <aside className="row-start-1 -row-end-1 flex flex-col justify-between">
 | 
			
		||||
          <ul className="flex flex-col">
 | 
			
		||||
            <li className="contents">
 | 
			
		||||
              <ActionButton
 | 
			
		||||
@ -233,6 +253,34 @@ const Home = () => {
 | 
			
		||||
                Generate with Text-to-CAD
 | 
			
		||||
              </ActionButton>
 | 
			
		||||
            </li>
 | 
			
		||||
            <li className="contents">
 | 
			
		||||
              <ActionButton
 | 
			
		||||
                Element="button"
 | 
			
		||||
                onClick={() =>
 | 
			
		||||
                  commandBarActor.send({
 | 
			
		||||
                    type: 'Find and select command',
 | 
			
		||||
                    data: {
 | 
			
		||||
                      groupId: 'application',
 | 
			
		||||
                      name: 'add-kcl-file-to-project',
 | 
			
		||||
                      argDefaultValues: {
 | 
			
		||||
                        source: 'kcl-samples',
 | 
			
		||||
                        method: 'newProject',
 | 
			
		||||
                        newProjectName:
 | 
			
		||||
                          settings.projects.defaultProjectName.current,
 | 
			
		||||
                      },
 | 
			
		||||
                    },
 | 
			
		||||
                  })
 | 
			
		||||
                }
 | 
			
		||||
                className={sidebarButtonClasses}
 | 
			
		||||
                iconStart={{
 | 
			
		||||
                  icon: 'importFile',
 | 
			
		||||
                  bgClassName: '!bg-transparent rounded-sm',
 | 
			
		||||
                }}
 | 
			
		||||
                data-testid="home-create-from-sample"
 | 
			
		||||
              >
 | 
			
		||||
                Create from a sample
 | 
			
		||||
              </ActionButton>
 | 
			
		||||
            </li>
 | 
			
		||||
          </ul>
 | 
			
		||||
          <ul className="flex flex-col">
 | 
			
		||||
            <li className="contents">
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user