Get query-triggered command working in browser too

This commit is contained in:
Frank Noirot
2024-10-10 18:55:31 -04:00
parent 96b06247a4
commit d58a147b7d
12 changed files with 283 additions and 82 deletions

View File

@ -26,6 +26,7 @@ import { useCreateFileLinkQuery } from 'hooks/useCreateFileLinkQueryWatcher'
export function App() { export function App() {
const { project, file } = useLoaderData() as IndexLoaderData const { project, file } = useLoaderData() as IndexLoaderData
// Keep a lookout for a URL query string that invokes the 'import file from URL' command
useCreateFileLinkQuery() useCreateFileLinkQuery()
useRefreshSettings(PATHS.FILE + 'SETTINGS') useRefreshSettings(PATHS.FILE + 'SETTINGS')
const navigate = useNavigate() const navigate = useNavigate()

View File

@ -4,6 +4,7 @@ import { useCommandsContext } from 'hooks/useCommandsContext'
import { Command } from 'lib/commandTypes' import { Command } from 'lib/commandTypes'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { CustomIcon } from './CustomIcon' import { CustomIcon } from './CustomIcon'
import { getActorNextEvents } from 'lib/utils'
function CommandComboBox({ function CommandComboBox({
options, options,
@ -73,7 +74,8 @@ function CommandComboBox({
<Combobox.Option <Combobox.Option
key={option.groupId + option.name + (option.displayName || '')} key={option.groupId + option.name + (option.displayName || '')}
value={option} 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 && ( {'icon' in option && option.icon && (
<CustomIcon name={option.icon} className="w-5 h-5" /> <CustomIcon name={option.icon} className="w-5 h-5" />
@ -96,3 +98,11 @@ function CommandComboBox({
} }
export default CommandComboBox export default CommandComboBox
function optionIsDisabled(option: Command): boolean {
return (
'machineActor' in option &&
option.machineActor !== undefined &&
!getActorNextEvents(option.machineActor.getSnapshot()).includes(option.name)
)
}

View File

@ -186,7 +186,6 @@ function ProjectMenuPopover({
{ {
id: 'share-link', id: 'share-link',
Element: 'button', Element: 'button',
className: !isDesktop() ? 'hidden' : '',
children: 'Share link to file', children: 'Share link to file',
onClick: async () => { onClick: async () => {
const shareUrl = createFileLink({ const shareUrl = createFileLink({

View File

@ -3,11 +3,11 @@ import { useCommandsContext } from 'hooks/useCommandsContext'
import { useFileSystemWatcher } from 'hooks/useFileSystemWatcher' import { useFileSystemWatcher } from 'hooks/useFileSystemWatcher'
import { useProjectsLoader } from 'hooks/useProjectsLoader' import { useProjectsLoader } from 'hooks/useProjectsLoader'
import { projectsMachine } from 'machines/projectsMachine' 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 { Actor, AnyStateMachine, fromPromise, Prop, StateFrom } from 'xstate'
import { useLspContext } from './LspProvider' import { useLspContext } from './LspProvider'
import toast from 'react-hot-toast' 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 { PATHS } from 'lib/paths'
import { import {
createNewProjectDirectory, createNewProjectDirectory,
@ -18,11 +18,27 @@ import {
getNextProjectIndex, getNextProjectIndex,
interpolateProjectNameWithIndex, interpolateProjectNameWithIndex,
doesProjectNameNeedInterpolated, doesProjectNameNeedInterpolated,
getNextFileName,
} from 'lib/desktopFS' } from 'lib/desktopFS'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import useStateMachineCommands from 'hooks/useStateMachineCommands' import useStateMachineCommands from 'hooks/useStateMachineCommands'
import { projectsCommandBarConfig } from 'lib/commandBarConfigs/projectsCommandConfig' import { projectsCommandBarConfig } from 'lib/commandBarConfigs/projectsCommandConfig'
import { isDesktop } from 'lib/isDesktop' 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> = { type MachineContext<T extends AnyStateMachine> = {
state?: StateFrom<T> state?: StateFrom<T>
@ -44,47 +60,25 @@ export const ProjectsContextProvider = ({
children, children,
}: { }: {
children: React.ReactNode 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 navigate = useNavigate()
const location = useLocation() 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 { commandBarSend } = useCommandsContext()
const { onProjectOpen } = useLspContext() const { onProjectOpen } = useLspContext()
const { const {
settings: { context: settings }, settings: { context: settings, send: settingsSend },
} = useSettingsAuthContext() } = useSettingsAuthContext()
useEffect(() => {
console.log(
'project directory changed',
settings.app.projectDirectory.current
)
}, [settings.app.projectDirectory.current])
const [projectsLoaderTrigger, setProjectsLoaderTrigger] = useState(0) const [projectsLoaderTrigger, setProjectsLoaderTrigger] = useState(0)
const { projectPaths, projectsDir } = useProjectsLoader([ const { projectPaths, projectsDir } = useProjectsLoader([
projectsLoaderTrigger, 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 }) => toastSuccess: ({ event }) =>
toast.success( toast.success(
('data' in event && typeof event.data === 'string' && event.data) || ('data' in event && typeof event.data === 'string' && event.data) ||
@ -182,7 +201,10 @@ const ProjectsContextDesktop = ({
), ),
}, },
actors: { actors: {
readProjects: fromPromise(() => listProjects()), readProjects: fromPromise(async () => {
if (!isDesktop()) return [] as Project[]
return listProjects()
}),
createProject: fromPromise(async ({ input }) => { createProject: fromPromise(async ({ input }) => {
let name = ( let name = (
input && 'name' in input && input.name input && 'name' in input && input.name
@ -238,6 +260,101 @@ const ProjectsContextDesktop = ({
name: input.name, 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: { guards: {
'Has at least 1 project': ({ event }) => { 'Has at least 1 project': ({ event }) => {
@ -267,6 +384,7 @@ const ProjectsContextDesktop = ({
state, state,
commandBarConfig: projectsCommandBarConfig, commandBarConfig: projectsCommandBarConfig,
actor, actor,
onCancel: clearImportSearchParams,
}) })
return ( return (

View File

@ -1,7 +1,7 @@
import { base64ToString } from 'lib/base64' import { base64ToString } from 'lib/base64'
import { CREATE_FILE_URL_PARAM, DEFAULT_FILE_NAME } from 'lib/constants' import { CREATE_FILE_URL_PARAM, DEFAULT_FILE_NAME } from 'lib/constants'
import { useEffect } from 'react' import { useEffect } from 'react'
import { useLocation } from 'react-router-dom' import { useSearchParams } from 'react-router-dom'
import { useCommandsContext } from './useCommandsContext' import { useCommandsContext } from './useCommandsContext'
import { useSettingsAuthContext } from './useSettingsAuthContext' import { useSettingsAuthContext } from './useSettingsAuthContext'
import { isDesktop } from 'lib/isDesktop' import { isDesktop } from 'lib/isDesktop'
@ -16,28 +16,28 @@ import { baseUnitsUnion } from 'lib/settings/settingsTypes'
* URL parameters. * URL parameters.
*/ */
export function useCreateFileLinkQuery() { export function useCreateFileLinkQuery() {
const location = useLocation() const [searchParams] = useSearchParams()
const { commandBarSend } = useCommandsContext() const { commandBarSend } = useCommandsContext()
const { settings } = useSettingsAuthContext() const { settings } = useSettingsAuthContext()
useEffect(() => { useEffect(() => {
const urlParams = new URLSearchParams(location.search) const createFileParam = searchParams.has(CREATE_FILE_URL_PARAM)
const createFileParam = urlParams.has(CREATE_FILE_URL_PARAM)
console.log('checking for createFileParam', { console.log('checking for createFileParam', {
createFileParam, createFileParam,
urlParams: [...urlParams.entries()], searchParams: [...searchParams.entries()],
location,
}) })
if (createFileParam) { if (createFileParam) {
const params: FileLinkParams = { 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: units:
(baseUnitsUnion.find((unit) => urlParams.get('units') === unit) || (baseUnitsUnion.find((unit) => searchParams.get('units') === unit) ||
settings.context.modeling.defaultUnit.default) ?? settings.context.modeling.defaultUnit.default) ??
settings.context.modeling.defaultUnit.current, settings.context.modeling.defaultUnit.current,
} }
@ -74,5 +74,5 @@ export function useCreateFileLinkQuery() {
}, },
}) })
} }
}, [location.search]) }, [searchParams])
} }

View File

@ -14,7 +14,7 @@ export const useProjectsLoader = (deps?: [number]) => {
useEffect(() => { useEffect(() => {
// Useless on web, until we get fake filesystems over there. // Useless on web, until we get fake filesystems over there.
if (!isDesktop) return if (!isDesktop()) return
if (deps && deps[0] === lastTs) return if (deps && deps[0] === lastTs) return

View File

@ -1,6 +1,7 @@
import { UnitLength_type } from '@kittycad/lib/dist/types/src/models' import { UnitLength_type } from '@kittycad/lib/dist/types/src/models'
import { CommandBarOverwriteWarning } from 'components/CommandBarOverwriteWarning' import { CommandBarOverwriteWarning } from 'components/CommandBarOverwriteWarning'
import { StateMachineCommandSetConfig } from 'lib/commandTypes' import { StateMachineCommandSetConfig } from 'lib/commandTypes'
import { isDesktop } from 'lib/isDesktop'
import { baseUnitLabels, baseUnitsUnion } from 'lib/settings/settingsTypes' import { baseUnitLabels, baseUnitsUnion } from 'lib/settings/settingsTypes'
import { projectsMachine } from 'machines/projectsMachine' import { projectsMachine } from 'machines/projectsMachine'
@ -112,16 +113,27 @@ export const projectsCommandBarConfig: StateMachineCommandSetConfig<
inputType: 'options', inputType: 'options',
required: true, required: true,
skip: true, skip: true,
options: [ options: isDesktop()
{ name: 'New Project', value: 'newProject' }, ? [
{ name: 'Existing Project', value: 'existingProject' }, { 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 // 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. // it's not available on projectMachine, but lower in fileMachine. Unify these.
projectName: { projectName: {
inputType: 'options', inputType: 'options',
required: (commandsContext) => commandsContext.argumentsToSubmit.method === 'existingProject', required: (commandsContext) =>
isDesktop() &&
commandsContext.argumentsToSubmit.method === 'existingProject',
skip: true, skip: true,
options: [], options: [],
optionsFromContext: (context) => optionsFromContext: (context) =>
@ -132,13 +144,16 @@ export const projectsCommandBarConfig: StateMachineCommandSetConfig<
}, },
name: { name: {
inputType: 'string', inputType: 'string',
required: true, required: isDesktop(),
skip: true, skip: true,
}, },
code: { code: {
inputType: 'text', inputType: 'text',
required: false, required: true,
skip: true, skip: true,
valueSummary(value) {
return value?.trim().split('\n').length + ' lines'
},
}, },
units: { units: {
inputType: 'options', inputType: 'options',
@ -151,7 +166,17 @@ export const projectsCommandBarConfig: StateMachineCommandSetConfig<
}, },
}, },
reviewMessage(commandBarContext) { 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.`
},
}, },
}
} }

View File

@ -1,5 +1,5 @@
import { UnitLength_type } from '@kittycad/lib/dist/types/src/models' 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' import { stringToBase64 } from './base64'
export interface FileLinkParams { export interface FileLinkParams {
@ -12,15 +12,12 @@ export interface FileLinkParams {
* Given a file's code, name, and units, creates shareable link * Given a file's code, name, and units, creates shareable link
* TODO: make the app respect this link * TODO: make the app respect this link
*/ */
export function createFileLink({ export function createFileLink({ code, name, units }: FileLinkParams) {
code, const origin = globalThis.window.location.origin
name,
units,
}: FileLinkParams) {
return new URL( return new URL(
`/?${CREATE_FILE_URL_PARAM}&name=${encodeURIComponent( `/?${CREATE_FILE_URL_PARAM}&name=${encodeURIComponent(
name name
)}&units=${units}&code=${encodeURIComponent(stringToBase64(code))}`, )}&units=${units}&code=${encodeURIComponent(stringToBase64(code))}`,
PROD_APP_URL origin
).href ).href
} }

View File

@ -104,7 +104,7 @@ export const fileLoader: LoaderFunction = async (
return redirect( return redirect(
`${PATHS.FILE}/${encodeURIComponent( `${PATHS.FILE}/${encodeURIComponent(
isDesktop() ? fallbackFile : params.id + '/' + PROJECT_ENTRYPOINT isDesktop() ? fallbackFile : params.id + '/' + PROJECT_ENTRYPOINT
)}` )}${new URL(routerData.request.url).search || ''}`
) )
} }

View File

@ -25,7 +25,10 @@ export const projectsMachine = setup({
type: 'Delete project' type: 'Delete project'
data: ProjectsCommandSchema['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: 'navigate'; data: { name: string } }
| { | {
type: 'xstate.done.actor.read-projects' type: 'xstate.done.actor.read-projects'
@ -43,6 +46,10 @@ export const projectsMachine = setup({
type: 'xstate.done.actor.rename-project' type: 'xstate.done.actor.rename-project'
output: { message: string; oldName: string; newName: string } 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 } }, | { type: 'assign'; data: { [key: string]: any } },
input: {} as { input: {} as {
projects: Project[] projects: Project[]
@ -61,6 +68,7 @@ export const projectsMachine = setup({
toastError: () => {}, toastError: () => {},
navigateToProject: () => {}, navigateToProject: () => {},
navigateToProjectIfNeeded: () => {}, navigateToProjectIfNeeded: () => {},
navigateToFile: () => {},
}, },
actors: { actors: {
readProjects: fromPromise(() => Promise.resolve([] as Project[])), readProjects: fromPromise(() => Promise.resolve([] as Project[])),
@ -91,13 +99,20 @@ export const projectsMachine = setup({
name: '', name: '',
}) })
), ),
createFile: fromPromise(
(_: {
input: ProjectsCommandSchema['Import file from URL'] & {
projects: Project[]
}
}) => Promise.resolve({ message: '', projectName: '', fileName: '' })
),
}, },
guards: { guards: {
'Has at least 1 project': () => false, 'Has at least 1 project': () => false,
'New project method is used': () => false, 'New project method is used': () => false,
}, },
}).createMachine({ }).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', id: 'Home machine',
initial: 'Reading projects', initial: 'Reading projects',
@ -114,13 +129,7 @@ export const projectsMachine = setup({
target: '.Reading projects', target: '.Reading projects',
}, },
"Import file from URL": [{ 'Import file from URL': '.Creating file',
target: ".Creating project",
guard: "New project method is used"
}, {
target: ".Reading projects",
actions: "navigateToProject"
}]
}, },
states: { states: {
'Has no projects': { 'Has no projects': {
@ -165,7 +174,10 @@ export const projectsMachine = setup({
id: 'create-project', id: 'create-project',
src: 'createProject', src: 'createProject',
input: ({ event, context }) => { 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 { return {
name: '', name: '',
projects: context.projects, projects: context.projects,
@ -280,7 +292,41 @@ export const projectsMachine = setup({
actions: ['toastError'], 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',
},
},
}, },
}, },
}) })

View File

@ -53,7 +53,7 @@ if (require('electron-squirrel-startup')) {
const ZOO_STUDIO_PROTOCOL = 'zoo-studio' 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.defaultApp) {
if (process.argv.length >= 2) { if (process.argv.length >= 2) {
app.setAsDefaultProtocolClient(ZOO_STUDIO_PROTOCOL, process.execPath, [ app.setAsDefaultProtocolClient(ZOO_STUDIO_PROTOCOL, process.execPath, [
@ -90,6 +90,7 @@ const createWindow = (filePath?: string): BrowserWindow => {
if (MAIN_WINDOW_VITE_DEV_SERVER_URL) { if (MAIN_WINDOW_VITE_DEV_SERVER_URL) {
newWindow.loadURL(MAIN_WINDOW_VITE_DEV_SERVER_URL).catch(reportRejection) newWindow.loadURL(MAIN_WINDOW_VITE_DEV_SERVER_URL).catch(reportRejection)
} else { } else {
console.log('Loading from file', filePath)
getProjectPathAtStartup(filePath) getProjectPathAtStartup(filePath)
.then(async (projectPath) => { .then(async (projectPath) => {
const startIndex = path.join( const startIndex = path.join(
@ -320,6 +321,7 @@ const getProjectPathAtStartup = async (
// macOS: open-url events that were received before the app is ready // macOS: open-url events that were received before the app is ready
const getOpenUrls: string[] = (global as any).getOpenUrls const getOpenUrls: string[] = (global as any).getOpenUrls
if (getOpenUrls && getOpenUrls.length > 0) { if (getOpenUrls && getOpenUrls.length > 0) {
console.log('getOpenUrls', getOpenUrls)
projectPath = getOpenUrls[0] // We only do one project at a projectPath = getOpenUrls[0] // We only do one project at a
} }
// Reset this so we don't accidentally use it again. // Reset this so we don't accidentally use it again.
@ -389,6 +391,8 @@ function registerStartupListeners() {
) { ) {
event.preventDefault() event.preventDefault()
console.log('open-url', url)
// If we have a mainWindow, lets open another window. // If we have a mainWindow, lets open another window.
if (mainWindow) { if (mainWindow) {
createWindow(url) createWindow(url)

View File

@ -28,6 +28,7 @@ import { useCreateFileLinkQuery } from 'hooks/useCreateFileLinkQueryWatcher'
// This route only opens in the desktop context for now, // This route only opens in the desktop context for now,
// as defined in Router.tsx, so we can use the desktop APIs and types. // as defined in Router.tsx, so we can use the desktop APIs and types.
const Home = () => { const Home = () => {
// Keep a lookout for a URL query string that invokes the 'import file from URL' command
useCreateFileLinkQuery() useCreateFileLinkQuery()
const { state, send } = useProjectsContext() const { state, send } = useProjectsContext()
const { commandBarSend } = useCommandsContext() const { commandBarSend } = useCommandsContext()
@ -207,7 +208,7 @@ const Home = () => {
</p> </p>
<p> <p>
<Link <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" className="text-primary underline underline-offset-2"
> >
Go to a test create-file link Go to a test create-file link