Get query-triggered command working in browser too
This commit is contained in:
		@ -26,6 +26,7 @@ import { useCreateFileLinkQuery } from 'hooks/useCreateFileLinkQueryWatcher'
 | 
			
		||||
 | 
			
		||||
export function App() {
 | 
			
		||||
  const { project, file } = useLoaderData() as IndexLoaderData
 | 
			
		||||
  // Keep a lookout for a URL query string that invokes the 'import file from URL' command
 | 
			
		||||
  useCreateFileLinkQuery()
 | 
			
		||||
  useRefreshSettings(PATHS.FILE + 'SETTINGS')
 | 
			
		||||
  const navigate = useNavigate()
 | 
			
		||||
 | 
			
		||||
@ -4,6 +4,7 @@ import { useCommandsContext } from 'hooks/useCommandsContext'
 | 
			
		||||
import { Command } from 'lib/commandTypes'
 | 
			
		||||
import { useEffect, useState } from 'react'
 | 
			
		||||
import { CustomIcon } from './CustomIcon'
 | 
			
		||||
import { getActorNextEvents } from 'lib/utils'
 | 
			
		||||
 | 
			
		||||
function CommandComboBox({
 | 
			
		||||
  options,
 | 
			
		||||
@ -73,7 +74,8 @@ function CommandComboBox({
 | 
			
		||||
          <Combobox.Option
 | 
			
		||||
            key={option.groupId + option.name + (option.displayName || '')}
 | 
			
		||||
            value={option}
 | 
			
		||||
            className="flex items-center gap-4 px-4 py-1.5 first:mt-2 last:mb-2 ui-active:bg-primary/10 dark:ui-active:bg-chalkboard-90"
 | 
			
		||||
            className="flex items-center gap-4 px-4 py-1.5 first:mt-2 last:mb-2 ui-active:bg-primary/10 dark:ui-active:bg-chalkboard-90 ui-disabled:!text-chalkboard-50"
 | 
			
		||||
            disabled={optionIsDisabled(option)}
 | 
			
		||||
          >
 | 
			
		||||
            {'icon' in option && option.icon && (
 | 
			
		||||
              <CustomIcon name={option.icon} className="w-5 h-5" />
 | 
			
		||||
@ -96,3 +98,11 @@ function CommandComboBox({
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default CommandComboBox
 | 
			
		||||
 | 
			
		||||
function optionIsDisabled(option: Command): boolean {
 | 
			
		||||
  return (
 | 
			
		||||
    'machineActor' in option &&
 | 
			
		||||
    option.machineActor !== undefined &&
 | 
			
		||||
    !getActorNextEvents(option.machineActor.getSnapshot()).includes(option.name)
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -186,7 +186,6 @@ function ProjectMenuPopover({
 | 
			
		||||
        {
 | 
			
		||||
          id: 'share-link',
 | 
			
		||||
          Element: 'button',
 | 
			
		||||
          className: !isDesktop() ? 'hidden' : '',
 | 
			
		||||
          children: 'Share link to file',
 | 
			
		||||
          onClick: async () => {
 | 
			
		||||
            const shareUrl = createFileLink({
 | 
			
		||||
 | 
			
		||||
@ -3,11 +3,11 @@ import { useCommandsContext } from 'hooks/useCommandsContext'
 | 
			
		||||
import { useFileSystemWatcher } from 'hooks/useFileSystemWatcher'
 | 
			
		||||
import { useProjectsLoader } from 'hooks/useProjectsLoader'
 | 
			
		||||
import { projectsMachine } from 'machines/projectsMachine'
 | 
			
		||||
import { createContext, useEffect, useState } from 'react'
 | 
			
		||||
import { createContext, useCallback, useEffect, useState } from 'react'
 | 
			
		||||
import { Actor, AnyStateMachine, fromPromise, Prop, StateFrom } from 'xstate'
 | 
			
		||||
import { useLspContext } from './LspProvider'
 | 
			
		||||
import toast from 'react-hot-toast'
 | 
			
		||||
import { useLocation, useNavigate } from 'react-router-dom'
 | 
			
		||||
import { useLocation, useNavigate, useSearchParams } from 'react-router-dom'
 | 
			
		||||
import { PATHS } from 'lib/paths'
 | 
			
		||||
import {
 | 
			
		||||
  createNewProjectDirectory,
 | 
			
		||||
@ -18,11 +18,27 @@ import {
 | 
			
		||||
  getNextProjectIndex,
 | 
			
		||||
  interpolateProjectNameWithIndex,
 | 
			
		||||
  doesProjectNameNeedInterpolated,
 | 
			
		||||
  getNextFileName,
 | 
			
		||||
} from 'lib/desktopFS'
 | 
			
		||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
 | 
			
		||||
import useStateMachineCommands from 'hooks/useStateMachineCommands'
 | 
			
		||||
import { projectsCommandBarConfig } from 'lib/commandBarConfigs/projectsCommandConfig'
 | 
			
		||||
import { isDesktop } from 'lib/isDesktop'
 | 
			
		||||
import {
 | 
			
		||||
  CREATE_FILE_URL_PARAM,
 | 
			
		||||
  FILE_EXT,
 | 
			
		||||
  PROJECT_ENTRYPOINT,
 | 
			
		||||
} from 'lib/constants'
 | 
			
		||||
import { DeepPartial } from 'lib/types'
 | 
			
		||||
import { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
 | 
			
		||||
import { codeManager } from 'lib/singletons'
 | 
			
		||||
import {
 | 
			
		||||
  loadAndValidateSettings,
 | 
			
		||||
  projectConfigurationToSettingsPayload,
 | 
			
		||||
  saveSettings,
 | 
			
		||||
  setSettingsAtLevel,
 | 
			
		||||
} from 'lib/settings/settingsUtils'
 | 
			
		||||
import { Project } from 'lib/project'
 | 
			
		||||
 | 
			
		||||
type MachineContext<T extends AnyStateMachine> = {
 | 
			
		||||
  state?: StateFrom<T>
 | 
			
		||||
@ -44,47 +60,25 @@ export const ProjectsContextProvider = ({
 | 
			
		||||
  children,
 | 
			
		||||
}: {
 | 
			
		||||
  children: React.ReactNode
 | 
			
		||||
}) => {
 | 
			
		||||
  return isDesktop() ? (
 | 
			
		||||
    <ProjectsContextDesktop>{children}</ProjectsContextDesktop>
 | 
			
		||||
  ) : (
 | 
			
		||||
    <ProjectsContextWeb>{children}</ProjectsContextWeb>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const ProjectsContextWeb = ({ children }: { children: React.ReactNode }) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <ProjectsMachineContext.Provider
 | 
			
		||||
      value={{
 | 
			
		||||
        state: undefined,
 | 
			
		||||
        send: () => {},
 | 
			
		||||
      }}
 | 
			
		||||
    >
 | 
			
		||||
      {children}
 | 
			
		||||
    </ProjectsMachineContext.Provider>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const ProjectsContextDesktop = ({
 | 
			
		||||
  children,
 | 
			
		||||
}: {
 | 
			
		||||
  children: React.ReactNode
 | 
			
		||||
}) => {
 | 
			
		||||
  const navigate = useNavigate()
 | 
			
		||||
  const location = useLocation()
 | 
			
		||||
  const [searchParams, setSearchParams] = useSearchParams()
 | 
			
		||||
  const clearImportSearchParams = useCallback(() => {
 | 
			
		||||
    // Clear the search parameters related to the "Import file from URL" command
 | 
			
		||||
    // or we'll never be able cancel or submit it.
 | 
			
		||||
    searchParams.delete(CREATE_FILE_URL_PARAM)
 | 
			
		||||
    searchParams.delete('code')
 | 
			
		||||
    searchParams.delete('name')
 | 
			
		||||
    searchParams.delete('units')
 | 
			
		||||
    setSearchParams(searchParams)
 | 
			
		||||
  }, [searchParams, setSearchParams])
 | 
			
		||||
  const { commandBarSend } = useCommandsContext()
 | 
			
		||||
  const { onProjectOpen } = useLspContext()
 | 
			
		||||
  const {
 | 
			
		||||
    settings: { context: settings },
 | 
			
		||||
    settings: { context: settings, send: settingsSend },
 | 
			
		||||
  } = useSettingsAuthContext()
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    console.log(
 | 
			
		||||
      'project directory changed',
 | 
			
		||||
      settings.app.projectDirectory.current
 | 
			
		||||
    )
 | 
			
		||||
  }, [settings.app.projectDirectory.current])
 | 
			
		||||
 | 
			
		||||
  const [projectsLoaderTrigger, setProjectsLoaderTrigger] = useState(0)
 | 
			
		||||
  const { projectPaths, projectsDir } = useProjectsLoader([
 | 
			
		||||
    projectsLoaderTrigger,
 | 
			
		||||
@ -163,6 +157,31 @@ const ProjectsContextDesktop = ({
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        navigateToFile: ({ context, event }) => {
 | 
			
		||||
          if (event.type !== 'xstate.done.actor.create-file') return
 | 
			
		||||
          // For now, the browser version of create-file doesn't need to navigate
 | 
			
		||||
          // since it just overwrites the current file.
 | 
			
		||||
          if (!isDesktop()) return
 | 
			
		||||
          let projectPath = window.electron.join(
 | 
			
		||||
            context.defaultDirectory,
 | 
			
		||||
            event.output.projectName
 | 
			
		||||
          )
 | 
			
		||||
          let filePath = window.electron.join(
 | 
			
		||||
            projectPath,
 | 
			
		||||
            event.output.fileName
 | 
			
		||||
          )
 | 
			
		||||
          onProjectOpen(
 | 
			
		||||
            {
 | 
			
		||||
              name: event.output.projectName,
 | 
			
		||||
              path: projectPath,
 | 
			
		||||
            },
 | 
			
		||||
            null
 | 
			
		||||
          )
 | 
			
		||||
          const pathToNavigateTo = `${PATHS.FILE}/${encodeURIComponent(
 | 
			
		||||
            filePath
 | 
			
		||||
          )}`
 | 
			
		||||
          navigate(pathToNavigateTo)
 | 
			
		||||
        },
 | 
			
		||||
        toastSuccess: ({ event }) =>
 | 
			
		||||
          toast.success(
 | 
			
		||||
            ('data' in event && typeof event.data === 'string' && event.data) ||
 | 
			
		||||
@ -182,7 +201,10 @@ const ProjectsContextDesktop = ({
 | 
			
		||||
          ),
 | 
			
		||||
      },
 | 
			
		||||
      actors: {
 | 
			
		||||
        readProjects: fromPromise(() => listProjects()),
 | 
			
		||||
        readProjects: fromPromise(async () => {
 | 
			
		||||
          if (!isDesktop()) return [] as Project[]
 | 
			
		||||
          return listProjects()
 | 
			
		||||
        }),
 | 
			
		||||
        createProject: fromPromise(async ({ input }) => {
 | 
			
		||||
          let name = (
 | 
			
		||||
            input && 'name' in input && input.name
 | 
			
		||||
@ -238,6 +260,101 @@ const ProjectsContextDesktop = ({
 | 
			
		||||
            name: input.name,
 | 
			
		||||
          }
 | 
			
		||||
        }),
 | 
			
		||||
        createFile: fromPromise(async ({ input }) => {
 | 
			
		||||
          let projectName =
 | 
			
		||||
            (input.method === 'newProject' ? input.name : input.projectName) ||
 | 
			
		||||
            settings.projects.defaultProjectName.current
 | 
			
		||||
          let fileName =
 | 
			
		||||
            input.method === 'newProject'
 | 
			
		||||
              ? PROJECT_ENTRYPOINT
 | 
			
		||||
              : input.name.endsWith(FILE_EXT)
 | 
			
		||||
              ? input.name
 | 
			
		||||
              : input.name + FILE_EXT
 | 
			
		||||
          let message = 'File created successfully'
 | 
			
		||||
          const unitsConfiguration: DeepPartial<Configuration> = {
 | 
			
		||||
            settings: {
 | 
			
		||||
              project: {
 | 
			
		||||
                directory: settings.app.projectDirectory.current,
 | 
			
		||||
              },
 | 
			
		||||
              modeling: {
 | 
			
		||||
                base_unit: input.units,
 | 
			
		||||
              },
 | 
			
		||||
            },
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          if (isDesktop()) {
 | 
			
		||||
            const needsInterpolated =
 | 
			
		||||
              doesProjectNameNeedInterpolated(projectName)
 | 
			
		||||
            console.log(
 | 
			
		||||
              `The project name "${projectName}" needs interpolated: ${needsInterpolated}`
 | 
			
		||||
            )
 | 
			
		||||
            if (needsInterpolated) {
 | 
			
		||||
              const nextIndex = getNextProjectIndex(projectName, input.projects)
 | 
			
		||||
              projectName = interpolateProjectNameWithIndex(
 | 
			
		||||
                projectName,
 | 
			
		||||
                nextIndex
 | 
			
		||||
              )
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Create the project around the file if newProject
 | 
			
		||||
            if (input.method === 'newProject') {
 | 
			
		||||
              await createNewProjectDirectory(
 | 
			
		||||
                projectName,
 | 
			
		||||
                input.code,
 | 
			
		||||
                unitsConfiguration
 | 
			
		||||
              )
 | 
			
		||||
              message = `Project "${projectName}" created successfully with link contents`
 | 
			
		||||
            } else {
 | 
			
		||||
              let projectPath = window.electron.join(
 | 
			
		||||
                settings.app.projectDirectory.current,
 | 
			
		||||
                projectName
 | 
			
		||||
              )
 | 
			
		||||
 | 
			
		||||
              message = `File "${fileName}" created successfully`
 | 
			
		||||
              const existingConfiguration = await loadAndValidateSettings(
 | 
			
		||||
                projectPath
 | 
			
		||||
              )
 | 
			
		||||
              const settingsToSave = setSettingsAtLevel(
 | 
			
		||||
                existingConfiguration.settings,
 | 
			
		||||
                'project',
 | 
			
		||||
                projectConfigurationToSettingsPayload(unitsConfiguration)
 | 
			
		||||
              )
 | 
			
		||||
              await saveSettings(settingsToSave, projectPath)
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Create the file
 | 
			
		||||
            let baseDir = window.electron.join(
 | 
			
		||||
              settings.app.projectDirectory.current,
 | 
			
		||||
              projectName
 | 
			
		||||
            )
 | 
			
		||||
            const { name, path } = getNextFileName({
 | 
			
		||||
              entryName: fileName,
 | 
			
		||||
              baseDir,
 | 
			
		||||
            })
 | 
			
		||||
            fileName = name
 | 
			
		||||
 | 
			
		||||
            await window.electron.writeFile(path, input.code || '')
 | 
			
		||||
          } else {
 | 
			
		||||
            // Browser version doesn't navigate, just overwrites the current file
 | 
			
		||||
            clearImportSearchParams()
 | 
			
		||||
            codeManager.updateCodeStateEditor(input.code || '')
 | 
			
		||||
            await codeManager.writeToFile()
 | 
			
		||||
            message = 'File successfully overwritten with link contents'
 | 
			
		||||
            settingsSend({
 | 
			
		||||
              type: 'set.modeling.defaultUnit',
 | 
			
		||||
              data: {
 | 
			
		||||
                level: 'project',
 | 
			
		||||
                value: input.units,
 | 
			
		||||
              },
 | 
			
		||||
            })
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          return {
 | 
			
		||||
            message,
 | 
			
		||||
            fileName,
 | 
			
		||||
            projectName,
 | 
			
		||||
          }
 | 
			
		||||
        }),
 | 
			
		||||
      },
 | 
			
		||||
      guards: {
 | 
			
		||||
        'Has at least 1 project': ({ event }) => {
 | 
			
		||||
@ -267,6 +384,7 @@ const ProjectsContextDesktop = ({
 | 
			
		||||
    state,
 | 
			
		||||
    commandBarConfig: projectsCommandBarConfig,
 | 
			
		||||
    actor,
 | 
			
		||||
    onCancel: clearImportSearchParams,
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
import { base64ToString } from 'lib/base64'
 | 
			
		||||
import { CREATE_FILE_URL_PARAM, DEFAULT_FILE_NAME } from 'lib/constants'
 | 
			
		||||
import { useEffect } from 'react'
 | 
			
		||||
import { useLocation } from 'react-router-dom'
 | 
			
		||||
import { useSearchParams } from 'react-router-dom'
 | 
			
		||||
import { useCommandsContext } from './useCommandsContext'
 | 
			
		||||
import { useSettingsAuthContext } from './useSettingsAuthContext'
 | 
			
		||||
import { isDesktop } from 'lib/isDesktop'
 | 
			
		||||
@ -16,28 +16,28 @@ import { baseUnitsUnion } from 'lib/settings/settingsTypes'
 | 
			
		||||
 * URL parameters.
 | 
			
		||||
 */
 | 
			
		||||
export function useCreateFileLinkQuery() {
 | 
			
		||||
  const location = useLocation()
 | 
			
		||||
  const [searchParams] = useSearchParams()
 | 
			
		||||
  const { commandBarSend } = useCommandsContext()
 | 
			
		||||
  const { settings } = useSettingsAuthContext()
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const urlParams = new URLSearchParams(location.search)
 | 
			
		||||
    const createFileParam = urlParams.has(CREATE_FILE_URL_PARAM)
 | 
			
		||||
    const createFileParam = searchParams.has(CREATE_FILE_URL_PARAM)
 | 
			
		||||
 | 
			
		||||
    console.log('checking for createFileParam', {
 | 
			
		||||
      createFileParam,
 | 
			
		||||
      urlParams: [...urlParams.entries()],
 | 
			
		||||
      location,
 | 
			
		||||
      searchParams: [...searchParams.entries()],
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    if (createFileParam) {
 | 
			
		||||
      const params: FileLinkParams = {
 | 
			
		||||
        code: base64ToString(decodeURIComponent(urlParams.get('code') ?? '')),
 | 
			
		||||
        code: base64ToString(
 | 
			
		||||
          decodeURIComponent(searchParams.get('code') ?? '')
 | 
			
		||||
        ),
 | 
			
		||||
 | 
			
		||||
        name: urlParams.get('name') ?? DEFAULT_FILE_NAME,
 | 
			
		||||
        name: searchParams.get('name') ?? DEFAULT_FILE_NAME,
 | 
			
		||||
 | 
			
		||||
        units:
 | 
			
		||||
          (baseUnitsUnion.find((unit) => urlParams.get('units') === unit) ||
 | 
			
		||||
          (baseUnitsUnion.find((unit) => searchParams.get('units') === unit) ||
 | 
			
		||||
            settings.context.modeling.defaultUnit.default) ??
 | 
			
		||||
          settings.context.modeling.defaultUnit.current,
 | 
			
		||||
      }
 | 
			
		||||
@ -74,5 +74,5 @@ export function useCreateFileLinkQuery() {
 | 
			
		||||
        },
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
  }, [location.search])
 | 
			
		||||
  }, [searchParams])
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -14,7 +14,7 @@ export const useProjectsLoader = (deps?: [number]) => {
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    // Useless on web, until we get fake filesystems over there.
 | 
			
		||||
    if (!isDesktop) return
 | 
			
		||||
    if (!isDesktop()) return
 | 
			
		||||
 | 
			
		||||
    if (deps && deps[0] === lastTs) return
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,7 @@
 | 
			
		||||
import { UnitLength_type } from '@kittycad/lib/dist/types/src/models'
 | 
			
		||||
import { CommandBarOverwriteWarning } from 'components/CommandBarOverwriteWarning'
 | 
			
		||||
import { StateMachineCommandSetConfig } from 'lib/commandTypes'
 | 
			
		||||
import { isDesktop } from 'lib/isDesktop'
 | 
			
		||||
import { baseUnitLabels, baseUnitsUnion } from 'lib/settings/settingsTypes'
 | 
			
		||||
import { projectsMachine } from 'machines/projectsMachine'
 | 
			
		||||
 | 
			
		||||
@ -112,16 +113,27 @@ export const projectsCommandBarConfig: StateMachineCommandSetConfig<
 | 
			
		||||
        inputType: 'options',
 | 
			
		||||
        required: true,
 | 
			
		||||
        skip: true,
 | 
			
		||||
        options: [
 | 
			
		||||
          { name: 'New Project', value: 'newProject' },
 | 
			
		||||
          { name: 'Existing Project', value: 'existingProject' },
 | 
			
		||||
        ],
 | 
			
		||||
        options: isDesktop()
 | 
			
		||||
          ? [
 | 
			
		||||
              { name: 'New project', value: 'newProject' },
 | 
			
		||||
              { name: 'Existing project', value: 'existingProject' },
 | 
			
		||||
            ]
 | 
			
		||||
          : [{ name: 'Overwrite', value: 'existingProject' }],
 | 
			
		||||
        valueSummary(value) {
 | 
			
		||||
          return isDesktop()
 | 
			
		||||
            ? value === 'newProject'
 | 
			
		||||
              ? 'New project'
 | 
			
		||||
              : 'Existing project'
 | 
			
		||||
            : 'Overwrite'
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      // TODO: We can't get the currently-opened project to auto-populate here because
 | 
			
		||||
      // it's not available on projectMachine, but lower in fileMachine. Unify these.
 | 
			
		||||
      projectName: {
 | 
			
		||||
        inputType: 'options',
 | 
			
		||||
        required: (commandsContext) => commandsContext.argumentsToSubmit.method === 'existingProject',
 | 
			
		||||
        required: (commandsContext) =>
 | 
			
		||||
          isDesktop() &&
 | 
			
		||||
          commandsContext.argumentsToSubmit.method === 'existingProject',
 | 
			
		||||
        skip: true,
 | 
			
		||||
        options: [],
 | 
			
		||||
        optionsFromContext: (context) =>
 | 
			
		||||
@ -132,13 +144,16 @@ export const projectsCommandBarConfig: StateMachineCommandSetConfig<
 | 
			
		||||
      },
 | 
			
		||||
      name: {
 | 
			
		||||
        inputType: 'string',
 | 
			
		||||
        required: true,
 | 
			
		||||
        required: isDesktop(),
 | 
			
		||||
        skip: true,
 | 
			
		||||
      },
 | 
			
		||||
      code: {
 | 
			
		||||
        inputType: 'text',
 | 
			
		||||
        required: false,
 | 
			
		||||
        required: true,
 | 
			
		||||
        skip: true,
 | 
			
		||||
        valueSummary(value) {
 | 
			
		||||
          return value?.trim().split('\n').length + ' lines'
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      units: {
 | 
			
		||||
        inputType: 'options',
 | 
			
		||||
@ -151,7 +166,17 @@ export const projectsCommandBarConfig: StateMachineCommandSetConfig<
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    reviewMessage(commandBarContext) {
 | 
			
		||||
      return `Will add the contents from URL to a new ${commandBarContext.argumentsToSubmit.method === 'newProject' ? 'project with file main.kcl' : `file within the project "${commandBarContext.argumentsToSubmit.projectName}"`} named "${commandBarContext.argumentsToSubmit.name}", and set default units to "${commandBarContext.argumentsToSubmit.units}".`
 | 
			
		||||
      return isDesktop()
 | 
			
		||||
        ? `Will add the contents from URL to a new ${
 | 
			
		||||
            commandBarContext.argumentsToSubmit.method === 'newProject'
 | 
			
		||||
              ? 'project with file main.kcl'
 | 
			
		||||
              : `file within the project "${commandBarContext.argumentsToSubmit.projectName}"`
 | 
			
		||||
          } named "${
 | 
			
		||||
            commandBarContext.argumentsToSubmit.name
 | 
			
		||||
          }", and set default units to "${
 | 
			
		||||
            commandBarContext.argumentsToSubmit.units
 | 
			
		||||
          }".`
 | 
			
		||||
        : `Will overwrite the contents of the current file with the contents from the URL.`
 | 
			
		||||
    },
 | 
			
		||||
  }
 | 
			
		||||
  },
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,26 +1,23 @@
 | 
			
		||||
import { UnitLength_type } from '@kittycad/lib/dist/types/src/models'
 | 
			
		||||
import { CREATE_FILE_URL_PARAM, PROD_APP_URL } from './constants'
 | 
			
		||||
import { CREATE_FILE_URL_PARAM } from './constants'
 | 
			
		||||
import { stringToBase64 } from './base64'
 | 
			
		||||
 | 
			
		||||
export interface FileLinkParams {
 | 
			
		||||
    code: string
 | 
			
		||||
    name: string
 | 
			
		||||
    units: UnitLength_type
 | 
			
		||||
  code: string
 | 
			
		||||
  name: string
 | 
			
		||||
  units: UnitLength_type
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Given a file's code, name, and units, creates shareable link
 | 
			
		||||
 * TODO: make the app respect this link
 | 
			
		||||
 */
 | 
			
		||||
export function createFileLink({
 | 
			
		||||
  code,
 | 
			
		||||
  name,
 | 
			
		||||
  units,
 | 
			
		||||
}: FileLinkParams) {
 | 
			
		||||
export function createFileLink({ code, name, units }: FileLinkParams) {
 | 
			
		||||
  const origin = globalThis.window.location.origin
 | 
			
		||||
  return new URL(
 | 
			
		||||
    `/?${CREATE_FILE_URL_PARAM}&name=${encodeURIComponent(
 | 
			
		||||
      name
 | 
			
		||||
    )}&units=${units}&code=${encodeURIComponent(stringToBase64(code))}`,
 | 
			
		||||
    PROD_APP_URL
 | 
			
		||||
    origin
 | 
			
		||||
  ).href
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -104,7 +104,7 @@ export const fileLoader: LoaderFunction = async (
 | 
			
		||||
        return redirect(
 | 
			
		||||
          `${PATHS.FILE}/${encodeURIComponent(
 | 
			
		||||
            isDesktop() ? fallbackFile : params.id + '/' + PROJECT_ENTRYPOINT
 | 
			
		||||
          )}`
 | 
			
		||||
          )}${new URL(routerData.request.url).search || ''}`
 | 
			
		||||
        )
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -25,7 +25,10 @@ export const projectsMachine = setup({
 | 
			
		||||
          type: 'Delete project'
 | 
			
		||||
          data: ProjectsCommandSchema['Delete project']
 | 
			
		||||
        }
 | 
			
		||||
      | { type: 'Import file from URL'; data: ProjectsCommandSchema['Import file from URL'] }
 | 
			
		||||
      | {
 | 
			
		||||
          type: 'Import file from URL'
 | 
			
		||||
          data: ProjectsCommandSchema['Import file from URL']
 | 
			
		||||
        }
 | 
			
		||||
      | { type: 'navigate'; data: { name: string } }
 | 
			
		||||
      | {
 | 
			
		||||
          type: 'xstate.done.actor.read-projects'
 | 
			
		||||
@ -43,6 +46,10 @@ export const projectsMachine = setup({
 | 
			
		||||
          type: 'xstate.done.actor.rename-project'
 | 
			
		||||
          output: { message: string; oldName: string; newName: string }
 | 
			
		||||
        }
 | 
			
		||||
      | {
 | 
			
		||||
          type: 'xstate.done.actor.create-file'
 | 
			
		||||
          output: { message: string; projectName: string; fileName: string }
 | 
			
		||||
        }
 | 
			
		||||
      | { type: 'assign'; data: { [key: string]: any } },
 | 
			
		||||
    input: {} as {
 | 
			
		||||
      projects: Project[]
 | 
			
		||||
@ -61,6 +68,7 @@ export const projectsMachine = setup({
 | 
			
		||||
    toastError: () => {},
 | 
			
		||||
    navigateToProject: () => {},
 | 
			
		||||
    navigateToProjectIfNeeded: () => {},
 | 
			
		||||
    navigateToFile: () => {},
 | 
			
		||||
  },
 | 
			
		||||
  actors: {
 | 
			
		||||
    readProjects: fromPromise(() => Promise.resolve([] as Project[])),
 | 
			
		||||
@ -91,13 +99,20 @@ export const projectsMachine = setup({
 | 
			
		||||
          name: '',
 | 
			
		||||
        })
 | 
			
		||||
    ),
 | 
			
		||||
    createFile: fromPromise(
 | 
			
		||||
      (_: {
 | 
			
		||||
        input: ProjectsCommandSchema['Import file from URL'] & {
 | 
			
		||||
          projects: Project[]
 | 
			
		||||
        }
 | 
			
		||||
      }) => Promise.resolve({ message: '', projectName: '', fileName: '' })
 | 
			
		||||
    ),
 | 
			
		||||
  },
 | 
			
		||||
  guards: {
 | 
			
		||||
    'Has at least 1 project': () => false,
 | 
			
		||||
    'New project method is used': () => false,
 | 
			
		||||
  },
 | 
			
		||||
}).createMachine({
 | 
			
		||||
  /** @xstate-layout N4IgpgJg5mDOIC5QAkD2BbMACdBDAxgBYCWAdmAMS6yzFSkDaADALqKgAOqtALsaqXYgAHogAsAJgA0IAJ6IAjBIkA2AHQAOCUw0qNkjQE4xYjQF8zMtJhwES5NcmpZSqLBwBOqAFZh8PWAoAJTBcCHcvX39YZjYkEC5efkF40QQFFQBmAHY1Qwz9QwBWU0NMrRl5BGyxBTUVJlVM01rtBXNLEGtsPCIyMEdnVwifPwCKAGEPUJ5sT1H-WKFE4j4BITSMzMzNIsyJDSZMhSYmFWzKxAkTNSYxIuU9zOKT7IsrDB67fsHYEajxiEwv8xjFWMtuKtkhsrpJboZsioincFMdTIjLggVGJ1GImMVMg0JISjEV3l1PrY+g4nH95gDAiFSLgbPSxkt4is1ilQGlrhJ4YjkbU0RoMXJEIYkWoikV8hJinjdOdyd0qfYBrSQdFJtNcLNtTwOZxIdyYQh+YKkSjReKqqYBQoEQpsgiSi9MqrKb0Nb9DYEACJgAA2YANbMW4M5puhqVhAvxQptCnRKkxCleuw0Gm2+2aRVRXpsPp+Woj4wA8hwwKRDcaEjH1nGLXDE9aRSmxWmJVipWocmdsbL8kxC501SWHFMZmQoIaKBABAMyAA3VAAawG+D1swAtOX61zY7zENlEWoMkoatcdPoipjCWIZRIirozsPiYYi19qQNp-rZ3nMAPC8Dw1A4YN9QAM1QDx0DUbcZjAfdInZKMTSSJsT2qc9Lwka8lTvTFTB2WUMiyQwc3yPRv3VH4mRZQDywXJc1FXDcBmmZlMBQhYjXQhtMJ5ERT1wlQr0kQj7kxKiZTxM5s2zbQEVoycBgY9AmNQ-wKGA0DwMgngYLgtQuJZZCDwEo8sJEnD1Dwgjb2kntig0GUkWVfZDEMfFVO+Bwg1DPhSDnZjFwcdjNzUCAQzDCztP4uIMKhGy0jPezxPwySnPvHsTjlPIhyOHRsnwlM-N-NRArDLS+N0kDYIM6DYPgmKgvivjD0bYS+VbBF21RTs7UUUcimfRpXyYRF8LFCrfSBCBaoZFiItINcor1CBeIZLqhPNdKL0yxzs2cqoNDqdpSo0EoSoLOb6NCRaQv9FblzWjjTMe7bQQYBQksElKesUMRjFubRMllCHsm2a5MVldRDBfFpmj0IpxPuhwFqW0F6v0iDmpMzbvuiXbAfNFNQcaI5IaKaH9jETFsUMNRVDxOU5TxBUMcof8DSg4hQ1Js1mwyfCL2JYlakkA4GZcp16jEaGzylfMsm53UkKwfnBb+iE9pFlQDjUM8HiYV99At2WqhOO5+zPLQqbRyiyXJVwYvgeIJ38sA9bJ5td27KpdzG7zQ70VFslOBVim5v1hnLD3kuF7D2hOW5smRCQM2uhQHkZ6VtkRBFnmu0qFFjssEsTgHk9syR03xAUlWOVQ9gzyjY957H-F92u0hMJ8ryc86iUyYiCvxFNzldVvjFjjTu54XvjzrnIWZUfI0eRM2VCyK3JThe5mhdWnS5dj5i29qrYuC0KEuX1LxDxepnlOfYDghp0G+yOpngzN+Yajnno9Re1drJA3SC6HY4lvL6AVMiJEuUqgbzckfQ4+gcTnUJBYCwQA */
 | 
			
		||||
  /** @xstate-layout N4IgpgJg5mDOIC5QAkD2BbMACdBDAxgBYCWAdmAMS6yzFSkDaADALqKgAOqtALsaqXYgAHogAsAJgA0IAJ6IAjAHYAbADoArBJVMFTCQA4mTAMwmxAXwsy0mHARLkKASXRcATjywAzYgBtsb3cMLABVACUAGWY2JBAuXn5BONEESRl5BAUFFQM1HQUxJSYATmyFAyUTKxsMbDwiMjA1ZGosUlQsDmCAKzB8HlgKcLBcCC7e-sGYoQTiPgEhVIUy9SKSiQ0NEwkclRyMxGKJNRKlMRVDU23zpRqQW3qHJpa2jonUPoGhgGF3UZ42G6nymMzicwWyVAyzKJzECg0OiYGjERhK+kOWUMSlOGiUlTEJlMxRKBnuj3sjXIr1gHy+g2Go3GwPpsDBnG48ySS0QGj0+Q0JTOGkqBg2JhKmNRJy2OyUEn0Kml5LqlMczVatJZUyGI1IuDs2oG7PinMhPIQfKYAqFShF+PFkrkRwkYjU8OUShKKhMKg0pUs1geqoa6ppdJ1FD+AKBk2NrFmZu5KV5-L9tvtYokEsxelRagUCoMiMumyUdpVdlDL01Ee+FAAImAAoC6zwTRDk9DU9b08LRY7MWd1AZcoS-aoLiYFJWnlSNW0jQyAPIcMCkNsdpOLFOWtOC-sO7NOzLbGWFbY6acmFESWdql7R3B8UhQNsUCACZpkABuqAA1s0+D-M+YAALRLluiQ7t2WRMGIbryjeBgGBIEglNsCi5sY6hMOchQqEoCLFCh97VtST4vm+S4UGA7jBO4agcH4z7eKg7joGowExhBcbtgm4LblCIiKPBiHZiKqHoZhuaFhoBbGMYBhiPBCgStUQYUuRzR6gaZDUXxH5fmov4Ac0-z6pgvEgvGsQctBwnLGJahIZJaEYdOmKEfJuQSoRRKSRcZHPNSunoPp750QxTEsTwbEcWoFkGuBkECfZXIwSJcEIS5Ekoe5MnOggXqIaUqFiiUhIInemkhiFzRNi2EU0Z+1KmYBagQM2YCAtZ9JQRljmiTlrn5dJnlFShOLwcpZjKBs+KBrUVb1WojU9c1hlRexMWsexnFdS2KV8QN5q7laNqHlmOaTcpmjoWc8L6HoYrBfOagjGMm02QyrXfqQf4dSBEB9Tqp1dllebichUkeVhRXTiU+QecUKhCps4pvWGn0QN9rJGW1ANmYlTKg98DAKHZpoORanoyqh8oImU3pKJifJ5Ohdr7CYqjIvKWMvDjeORttjHMXtCXA2T0xpdTg20+W9MSIzgorIRXlpr6ORbPs+jwQLFEgVRPj+JQf0mUTHXcaBYG+AE4OZcsxbuts5hbCK+zFmImJgSc5zPV6BQVD6RQG80lERXblCi7tcX7VxRvgVHDtDQgaE4vK2gXKiTA6ItubmJo8IoqOylERUVhBh0XXwHEWn1YmNO7mBKg+2KpzK2IpIBUwBhqUtwYre9tbvEutfpWdsFFnkPpVJnQqz15Ozur36FoypREbGH4Zj438u7tO1qIu5qK5PBGyYjebpEkS44e9oVTbxHr5tnvk9ZeXajTuW+zaHNuzYRUmoeCEp-RKlUHJbeYVhYDDfhDVIxR5IGGnISQk6E+6EkxMUBQX9c66EMMg3Q5Zt7rWNkuOBjsjilDUAqQsZQzzgOkJNUk7o7SmF-tiXOUCmQwMGBQ1OF4TgVGzFUG8yIxQGDZiYPI4oqh+mPsrDSy05xhmfm+KO-CLT+iRucEUuwFQqWzMWXM2YTAuTtL6ZBylkRcMrkAA */
 | 
			
		||||
  id: 'Home machine',
 | 
			
		||||
 | 
			
		||||
  initial: 'Reading projects',
 | 
			
		||||
@ -114,13 +129,7 @@ export const projectsMachine = setup({
 | 
			
		||||
      target: '.Reading projects',
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    "Import file from URL": [{
 | 
			
		||||
      target: ".Creating project",
 | 
			
		||||
      guard: "New project method is used"
 | 
			
		||||
    }, {
 | 
			
		||||
      target: ".Reading projects",
 | 
			
		||||
      actions: "navigateToProject"
 | 
			
		||||
    }]
 | 
			
		||||
    'Import file from URL': '.Creating file',
 | 
			
		||||
  },
 | 
			
		||||
  states: {
 | 
			
		||||
    'Has no projects': {
 | 
			
		||||
@ -165,7 +174,10 @@ export const projectsMachine = setup({
 | 
			
		||||
        id: 'create-project',
 | 
			
		||||
        src: 'createProject',
 | 
			
		||||
        input: ({ event, context }) => {
 | 
			
		||||
          if (event.type !== 'Create project' && event.type !== 'Import file from URL') {
 | 
			
		||||
          if (
 | 
			
		||||
            event.type !== 'Create project' &&
 | 
			
		||||
            event.type !== 'Import file from URL'
 | 
			
		||||
          ) {
 | 
			
		||||
            return {
 | 
			
		||||
              name: '',
 | 
			
		||||
              projects: context.projects,
 | 
			
		||||
@ -280,7 +292,41 @@ export const projectsMachine = setup({
 | 
			
		||||
            actions: ['toastError'],
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      }
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    'Creating file': {
 | 
			
		||||
      invoke: {
 | 
			
		||||
        id: 'create-file',
 | 
			
		||||
        src: 'createFile',
 | 
			
		||||
        input: ({ event, context }) => {
 | 
			
		||||
          if (event.type !== 'Import file from URL') {
 | 
			
		||||
            return {
 | 
			
		||||
              code: '',
 | 
			
		||||
              name: '',
 | 
			
		||||
              units: 'mm',
 | 
			
		||||
              method: 'existingProject',
 | 
			
		||||
              projects: context.projects,
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
          return {
 | 
			
		||||
            code: event.data.code || '',
 | 
			
		||||
            name: event.data.name,
 | 
			
		||||
            units: event.data.units,
 | 
			
		||||
            method: event.data.method,
 | 
			
		||||
            projectName: event.data.projectName,
 | 
			
		||||
            projects: context.projects,
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        onDone: {
 | 
			
		||||
          target: 'Reading projects',
 | 
			
		||||
          actions: ['navigateToFile', 'toastSuccess'],
 | 
			
		||||
        },
 | 
			
		||||
        onError: {
 | 
			
		||||
          target: 'Reading projects',
 | 
			
		||||
          actions: 'toastError',
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
@ -53,7 +53,7 @@ if (require('electron-squirrel-startup')) {
 | 
			
		||||
 | 
			
		||||
const ZOO_STUDIO_PROTOCOL = 'zoo-studio'
 | 
			
		||||
 | 
			
		||||
/// Register our application to handle all "electron-fiddle://" protocols.
 | 
			
		||||
/// Register our application to handle all "zoo-studio://" protocols.
 | 
			
		||||
if (process.defaultApp) {
 | 
			
		||||
  if (process.argv.length >= 2) {
 | 
			
		||||
    app.setAsDefaultProtocolClient(ZOO_STUDIO_PROTOCOL, process.execPath, [
 | 
			
		||||
@ -90,6 +90,7 @@ const createWindow = (filePath?: string): BrowserWindow => {
 | 
			
		||||
  if (MAIN_WINDOW_VITE_DEV_SERVER_URL) {
 | 
			
		||||
    newWindow.loadURL(MAIN_WINDOW_VITE_DEV_SERVER_URL).catch(reportRejection)
 | 
			
		||||
  } else {
 | 
			
		||||
    console.log('Loading from file', filePath)
 | 
			
		||||
    getProjectPathAtStartup(filePath)
 | 
			
		||||
      .then(async (projectPath) => {
 | 
			
		||||
        const startIndex = path.join(
 | 
			
		||||
@ -320,6 +321,7 @@ const getProjectPathAtStartup = async (
 | 
			
		||||
    // macOS: open-url events that were received before the app is ready
 | 
			
		||||
    const getOpenUrls: string[] = (global as any).getOpenUrls
 | 
			
		||||
    if (getOpenUrls && getOpenUrls.length > 0) {
 | 
			
		||||
      console.log('getOpenUrls', getOpenUrls)
 | 
			
		||||
      projectPath = getOpenUrls[0] // We only do one project at a
 | 
			
		||||
    }
 | 
			
		||||
    // Reset this so we don't accidentally use it again.
 | 
			
		||||
@ -389,6 +391,8 @@ function registerStartupListeners() {
 | 
			
		||||
  ) {
 | 
			
		||||
    event.preventDefault()
 | 
			
		||||
 | 
			
		||||
    console.log('open-url', url)
 | 
			
		||||
 | 
			
		||||
    // If we have a mainWindow, lets open another window.
 | 
			
		||||
    if (mainWindow) {
 | 
			
		||||
      createWindow(url)
 | 
			
		||||
 | 
			
		||||
@ -28,6 +28,7 @@ import { useCreateFileLinkQuery } from 'hooks/useCreateFileLinkQueryWatcher'
 | 
			
		||||
// This route only opens in the desktop context for now,
 | 
			
		||||
// as defined in Router.tsx, so we can use the desktop APIs and types.
 | 
			
		||||
const Home = () => {
 | 
			
		||||
  // Keep a lookout for a URL query string that invokes the 'import file from URL' command
 | 
			
		||||
  useCreateFileLinkQuery()
 | 
			
		||||
  const { state, send } = useProjectsContext()
 | 
			
		||||
  const { commandBarSend } = useCommandsContext()
 | 
			
		||||
@ -207,7 +208,7 @@ const Home = () => {
 | 
			
		||||
          </p>
 | 
			
		||||
          <p>
 | 
			
		||||
            <Link
 | 
			
		||||
              to="/?create-file&name=Untitled-1.kcl&units=mm&code=c2tldGNoMDAxID0gc3RhcnRTa2V0Y2hPbignWFknKQogIHw%2BIGNpcmNsZSh7CiAgICAgICBjZW50ZXI6IFstMTUuNTEsIDE1LjMzXSwKICAgICAgIHJhZGl1czogMTIuMTkKICAgICB9LCAlKQpleHRydXNpb25EaXN0YW5jZSA9IDEyCmV4dHJ1ZGUwMDEgPSBleHRydWRlKGV4dHJ1c2lvbkRpc3RhbmNlLCBza2V0Y2gwMDEpCnNrZXRjaDAwMiA9IHN0YXJ0U2tldGNoT24oZXh0cnVkZTAwMSwgJ0VORCcpCiAgfD4gY2lyY2xlKHsKICAgICAgIGNlbnRlcjogWy0xOC42OSwgMjAuMjNdLAogICAgICAgcmFkaXVzOiA0LjgzCiAgICAgfSwgJSkKZXh0cnVkZTAwMiA9IGV4dHJ1ZGUoLWV4dHJ1c2lvbkRpc3RhbmNlLCBza2V0Y2gwMDIpCgo%3D"
 | 
			
		||||
              to="/?create-file&name=Untitled-1.kcl&units=in&code=c2tldGNoMDAxID0gc3RhcnRTa2V0Y2hPbignWFknKQogIHw%2BIGNpcmNsZSh7CiAgICAgICBjZW50ZXI6IFstMTUuNTEsIDE1LjMzXSwKICAgICAgIHJhZGl1czogMTIuMTkKICAgICB9LCAlKQpleHRydXNpb25EaXN0YW5jZSA9IDEyCmV4dHJ1ZGUwMDEgPSBleHRydWRlKGV4dHJ1c2lvbkRpc3RhbmNlLCBza2V0Y2gwMDEpCnNrZXRjaDAwMiA9IHN0YXJ0U2tldGNoT24oZXh0cnVkZTAwMSwgJ0VORCcpCiAgfD4gY2lyY2xlKHsKICAgICAgIGNlbnRlcjogWy0xOC42OSwgMjAuMjNdLAogICAgICAgcmFkaXVzOiA0LjgzCiAgICAgfSwgJSkKZXh0cnVkZTAwMiA9IGV4dHJ1ZGUoLWV4dHJ1c2lvbkRpc3RhbmNlLCBza2V0Y2gwMDIpCgo%3D"
 | 
			
		||||
              className="text-primary underline underline-offset-2"
 | 
			
		||||
            >
 | 
			
		||||
              Go to a test create-file link
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user