fix: big clean up
This commit is contained in:
@ -1,492 +0,0 @@
|
||||
import { useMachine } from '@xstate/react'
|
||||
import { createContext, useCallback, useEffect, useState } from 'react'
|
||||
import toast from 'react-hot-toast'
|
||||
import { useLocation, useNavigate, useSearchParams } from 'react-router-dom'
|
||||
import type { Actor, AnyStateMachine, Prop, StateFrom } from 'xstate'
|
||||
import { fromPromise } from 'xstate'
|
||||
|
||||
import { useLspContext } from '@src/components/LspProvider'
|
||||
import { useFileSystemWatcher } from '@src/hooks/useFileSystemWatcher'
|
||||
import { useProjectsLoader } from '@src/hooks/useProjectsLoader'
|
||||
import useStateMachineCommands from '@src/hooks/useStateMachineCommands'
|
||||
import { newKclFile } from '@src/lang/project'
|
||||
import { projectsCommandBarConfig } from '@src/lib/commandBarConfigs/projectsCommandConfig'
|
||||
import {
|
||||
CREATE_FILE_URL_PARAM,
|
||||
FILE_EXT,
|
||||
PROJECT_ENTRYPOINT,
|
||||
} from '@src/lib/constants'
|
||||
import {
|
||||
createNewProjectDirectory,
|
||||
listProjects,
|
||||
renameProjectDirectory,
|
||||
} from '@src/lib/desktop'
|
||||
import {
|
||||
doesProjectNameNeedInterpolated,
|
||||
getNextFileName,
|
||||
getNextProjectIndex,
|
||||
getUniqueProjectName,
|
||||
interpolateProjectNameWithIndex,
|
||||
} from '@src/lib/desktopFS'
|
||||
import { isDesktop } from '@src/lib/isDesktop'
|
||||
import { PATHS } from '@src/lib/paths'
|
||||
import type { Project } from '@src/lib/project'
|
||||
import { codeManager, kclManager } from '@src/lib/singletons'
|
||||
import { err } from '@src/lib/trap'
|
||||
import { useSettings } from '@src/lib/singletons'
|
||||
import { commandBarActor } from '@src/machines/commandBarMachine'
|
||||
import { projectsMachine } from '@src/machines/projectsMachine'
|
||||
|
||||
type MachineContext<T extends AnyStateMachine> = {
|
||||
state?: StateFrom<T>
|
||||
send: Prop<Actor<T>, 'send'>
|
||||
}
|
||||
|
||||
export const ProjectsMachineContext = createContext(
|
||||
{} as MachineContext<typeof projectsMachine>
|
||||
)
|
||||
|
||||
/**
|
||||
* Watches the project directory and provides project management-related commands,
|
||||
* like "Create project", "Open project", "Delete project", etc.
|
||||
*
|
||||
* If in the future we implement full-fledge project management in the web version,
|
||||
* we can unify these components but for now, we need this to be only for the desktop version.
|
||||
*/
|
||||
export const ProjectsContextProvider = ({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) => {
|
||||
return isDesktop() ? (
|
||||
<ProjectsContextDesktop>{children}</ProjectsContextDesktop>
|
||||
) : (
|
||||
<ProjectsContextWeb>{children}</ProjectsContextWeb>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* We need some of the functionality of the ProjectsContextProvider in the web version
|
||||
* but we can't perform file system operations in the browser,
|
||||
* so most of the behavior of this machine is stubbed out.
|
||||
*/
|
||||
const ProjectsContextWeb = ({ children }: { children: React.ReactNode }) => {
|
||||
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 settings = useSettings()
|
||||
|
||||
const [state, send, actor] = useMachine(
|
||||
projectsMachine.provide({
|
||||
actions: {
|
||||
navigateToProject: () => {},
|
||||
navigateToProjectIfNeeded: () => {},
|
||||
navigateToFile: () => {},
|
||||
toastSuccess: ({ event }) =>
|
||||
toast.success(
|
||||
('data' in event && typeof event.data === 'string' && event.data) ||
|
||||
('output' in event &&
|
||||
'message' in event.output &&
|
||||
typeof event.output.message === 'string' &&
|
||||
event.output.message) ||
|
||||
''
|
||||
),
|
||||
toastError: ({ event }) =>
|
||||
toast.error(
|
||||
('data' in event && typeof event.data === 'string' && event.data) ||
|
||||
('output' in event &&
|
||||
typeof event.output === 'string' &&
|
||||
event.output) ||
|
||||
''
|
||||
),
|
||||
},
|
||||
actors: {
|
||||
readProjects: fromPromise(async () => [] as Project[]),
|
||||
createProject: fromPromise(async () => ({
|
||||
message: 'not implemented on web',
|
||||
})),
|
||||
renameProject: fromPromise(async () => ({
|
||||
message: 'not implemented on web',
|
||||
oldName: '',
|
||||
newName: '',
|
||||
})),
|
||||
deleteProject: fromPromise(async () => ({
|
||||
message: 'not implemented on web',
|
||||
name: '',
|
||||
})),
|
||||
createFile: fromPromise(async ({ input }) => {
|
||||
// Browser version doesn't navigate, just overwrites the current file
|
||||
clearImportSearchParams()
|
||||
|
||||
const codeToWrite = newKclFile(
|
||||
input.code,
|
||||
settings.modeling.defaultUnit.current
|
||||
)
|
||||
if (err(codeToWrite)) return Promise.reject(codeToWrite)
|
||||
codeManager.updateCodeStateEditor(codeToWrite)
|
||||
await codeManager.writeToFile()
|
||||
await kclManager.executeCode()
|
||||
|
||||
return {
|
||||
message: 'File overwritten successfully',
|
||||
fileName: input.name,
|
||||
projectName: '',
|
||||
}
|
||||
}),
|
||||
},
|
||||
}),
|
||||
{
|
||||
input: {
|
||||
projects: [],
|
||||
defaultProjectName: settings.projects.defaultProjectName.current,
|
||||
defaultDirectory: settings.app.projectDirectory.current,
|
||||
hasListedProjects: false,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// register all project-related command palette commands
|
||||
useStateMachineCommands({
|
||||
machineId: 'projects',
|
||||
send,
|
||||
state,
|
||||
commandBarConfig: projectsCommandBarConfig,
|
||||
actor,
|
||||
onCancel: clearImportSearchParams,
|
||||
})
|
||||
|
||||
return (
|
||||
<ProjectsMachineContext.Provider
|
||||
value={{
|
||||
state,
|
||||
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 { onProjectOpen } = useLspContext()
|
||||
const settings = useSettings()
|
||||
const [projectsLoaderTrigger, setProjectsLoaderTrigger] = useState(0)
|
||||
const { projectPaths, projectsDir } = useProjectsLoader([
|
||||
projectsLoaderTrigger,
|
||||
])
|
||||
|
||||
const [state, send, actor] = useMachine(
|
||||
projectsMachine.provide({
|
||||
actions: {
|
||||
navigateToProject: ({ context, event }) => {
|
||||
const nameFromEventData =
|
||||
'data' in event &&
|
||||
event.data &&
|
||||
'name' in event.data &&
|
||||
event.data.name
|
||||
const nameFromOutputData =
|
||||
'output' in event &&
|
||||
event.output &&
|
||||
'name' in event.output &&
|
||||
event.output.name
|
||||
|
||||
const name = nameFromEventData || nameFromOutputData
|
||||
|
||||
if (name) {
|
||||
let projectPath =
|
||||
context.defaultDirectory + window.electron.path.sep + name
|
||||
onProjectOpen(
|
||||
{
|
||||
name,
|
||||
path: projectPath,
|
||||
},
|
||||
null
|
||||
)
|
||||
commandBarActor.send({ type: 'Close' })
|
||||
const newPathName = `${PATHS.FILE}/${encodeURIComponent(
|
||||
projectPath
|
||||
)}`
|
||||
navigate(newPathName)
|
||||
}
|
||||
},
|
||||
navigateToProjectIfNeeded: ({ event }) => {
|
||||
if (
|
||||
event.type.startsWith('xstate.done.actor.') &&
|
||||
'output' in event
|
||||
) {
|
||||
const isInAProject = location.pathname.startsWith(PATHS.FILE)
|
||||
const isInDeletedProject =
|
||||
event.type === 'xstate.done.actor.delete-project' &&
|
||||
isInAProject &&
|
||||
decodeURIComponent(location.pathname).includes(event.output.name)
|
||||
if (isInDeletedProject) {
|
||||
navigate(PATHS.HOME)
|
||||
return
|
||||
}
|
||||
|
||||
const isInRenamedProject =
|
||||
event.type === 'xstate.done.actor.rename-project' &&
|
||||
isInAProject &&
|
||||
decodeURIComponent(location.pathname).includes(
|
||||
event.output.oldName
|
||||
)
|
||||
|
||||
if (isInRenamedProject) {
|
||||
// TODO: In future, we can navigate to the new project path
|
||||
// directly, but we need to coordinate with
|
||||
// @lf94's useFileSystemWatcher in SettingsAuthProvider.tsx:224
|
||||
// Because it's beating us to the punch and updating the route
|
||||
// const newPathName = location.pathname.replace(
|
||||
// encodeURIComponent(event.output.oldName),
|
||||
// encodeURIComponent(event.output.newName)
|
||||
// )
|
||||
// navigate(newPathName)
|
||||
return
|
||||
}
|
||||
}
|
||||
},
|
||||
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) ||
|
||||
('output' in event &&
|
||||
'message' in event.output &&
|
||||
typeof event.output.message === 'string' &&
|
||||
event.output.message) ||
|
||||
''
|
||||
),
|
||||
toastError: ({ event }) =>
|
||||
toast.error(
|
||||
('data' in event && typeof event.data === 'string' && event.data) ||
|
||||
('output' in event &&
|
||||
typeof event.output === 'string' &&
|
||||
event.output) ||
|
||||
('error' in event &&
|
||||
event.error instanceof Error &&
|
||||
event.error.message) ||
|
||||
''
|
||||
),
|
||||
},
|
||||
actors: {
|
||||
readProjects: fromPromise(() => {
|
||||
return listProjects()
|
||||
}),
|
||||
createProject: fromPromise(async ({ input }) => {
|
||||
let name = (
|
||||
input && 'name' in input && input.name
|
||||
? input.name
|
||||
: settings.projects.defaultProjectName.current
|
||||
).trim()
|
||||
|
||||
const uniqueName = getUniqueProjectName(name, input.projects)
|
||||
await createNewProjectDirectory(uniqueName)
|
||||
|
||||
return {
|
||||
message: `Successfully created "${uniqueName}"`,
|
||||
name: uniqueName,
|
||||
}
|
||||
}),
|
||||
renameProject: fromPromise(async ({ input }) => {
|
||||
const {
|
||||
oldName,
|
||||
newName,
|
||||
defaultProjectName,
|
||||
defaultDirectory,
|
||||
projects,
|
||||
} = input
|
||||
let name = newName ? newName : defaultProjectName
|
||||
if (doesProjectNameNeedInterpolated(name)) {
|
||||
const nextIndex = getNextProjectIndex(name, projects)
|
||||
name = interpolateProjectNameWithIndex(name, nextIndex)
|
||||
}
|
||||
|
||||
// Toast an error if the project name is taken
|
||||
if (projects.find((p) => p.name === name)) {
|
||||
return Promise.reject(
|
||||
new Error(`Project with name "${name}" already exists`)
|
||||
)
|
||||
}
|
||||
|
||||
await renameProjectDirectory(
|
||||
window.electron.path.join(defaultDirectory, oldName),
|
||||
name
|
||||
)
|
||||
return {
|
||||
message: `Successfully renamed "${oldName}" to "${name}"`,
|
||||
oldName: oldName,
|
||||
newName: name,
|
||||
}
|
||||
}),
|
||||
deleteProject: fromPromise(async ({ input }) => {
|
||||
await window.electron.rm(
|
||||
window.electron.path.join(input.defaultDirectory, input.name),
|
||||
{
|
||||
recursive: true,
|
||||
}
|
||||
)
|
||||
return {
|
||||
message: `Successfully deleted "${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 needsInterpolated = doesProjectNameNeedInterpolated(projectName)
|
||||
if (needsInterpolated) {
|
||||
const nextIndex = getNextProjectIndex(projectName, input.projects)
|
||||
projectName = interpolateProjectNameWithIndex(
|
||||
projectName,
|
||||
nextIndex
|
||||
)
|
||||
}
|
||||
|
||||
// Create the project around the file if newProject
|
||||
let fileLoaded = false
|
||||
if (input.method === 'newProject') {
|
||||
await createNewProjectDirectory(projectName, input.code)
|
||||
fileLoaded = true
|
||||
message = `Project "${projectName}" created successfully with link contents`
|
||||
} else {
|
||||
message = `File "${fileName}" created successfully`
|
||||
}
|
||||
|
||||
// Create the file
|
||||
let baseDir = window.electron.join(
|
||||
settings.app.projectDirectory.current,
|
||||
projectName
|
||||
)
|
||||
const { name, path } = getNextFileName({
|
||||
entryName: fileName,
|
||||
baseDir,
|
||||
})
|
||||
|
||||
fileName = name
|
||||
if (!fileLoaded) {
|
||||
const codeToWrite = newKclFile(
|
||||
input.code,
|
||||
settings.modeling.defaultUnit.current
|
||||
)
|
||||
if (err(codeToWrite)) return Promise.reject(codeToWrite)
|
||||
await window.electron.writeFile(path, codeToWrite)
|
||||
}
|
||||
|
||||
// TODO: Return the project's file name if one was created.
|
||||
return {
|
||||
message,
|
||||
fileName,
|
||||
projectName,
|
||||
}
|
||||
}),
|
||||
},
|
||||
}),
|
||||
{
|
||||
input: {
|
||||
projects: projectPaths,
|
||||
defaultProjectName: settings.projects.defaultProjectName.current,
|
||||
defaultDirectory: settings.app.projectDirectory.current,
|
||||
hasListedProjects: false,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
useFileSystemWatcher(
|
||||
async () => {
|
||||
// Gotcha: Chokidar is buggy. It will emit addDir or add on files that did not get created.
|
||||
// This means while the application initialize and Chokidar initializes you cannot tell if
|
||||
// a directory or file is actually created or they are buggy signals. This means you must
|
||||
// ignore all signals during initialization because it is ambiguous. Once those signals settle
|
||||
// you can actually start listening to real signals.
|
||||
// If someone creates folders or files during initialization we ignore those events!
|
||||
if (!actor.getSnapshot().context.hasListedProjects) {
|
||||
return
|
||||
}
|
||||
return setProjectsLoaderTrigger(projectsLoaderTrigger + 1)
|
||||
},
|
||||
projectsDir ? [projectsDir] : []
|
||||
)
|
||||
|
||||
// Gotcha: Triggers listProjects() on chokidar changes
|
||||
// Gotcha: Load the projects when the projectDirectory changes.
|
||||
const projectDirectory = settings.app.projectDirectory.current
|
||||
useEffect(() => {
|
||||
send({ type: 'Read projects', data: {} })
|
||||
}, [projectPaths, projectDirectory])
|
||||
|
||||
// register all project-related command palette commands
|
||||
useStateMachineCommands({
|
||||
machineId: 'projects',
|
||||
send,
|
||||
state,
|
||||
commandBarConfig: projectsCommandBarConfig,
|
||||
actor,
|
||||
onCancel: clearImportSearchParams,
|
||||
})
|
||||
|
||||
return (
|
||||
<ProjectsMachineContext.Provider
|
||||
value={{
|
||||
state,
|
||||
send,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ProjectsMachineContext.Provider>
|
||||
)
|
||||
}
|
||||
@ -1,177 +1,6 @@
|
||||
import { CommandBarOverwriteWarning } from '@src/components/CommandBarOverwriteWarning'
|
||||
import type { StateMachineCommandSetConfig } from '@src/lib/commandTypes'
|
||||
import { isDesktop } from '@src/lib/isDesktop'
|
||||
import type { projectsMachine } from '@src/machines/projectsMachine'
|
||||
import type { Command, CommandArgumentOption } from '@src/lib/commandTypes'
|
||||
|
||||
export type ProjectsCommandSchema = {
|
||||
'Read projects': Record<string, unknown>
|
||||
'Create project': {
|
||||
name: string
|
||||
}
|
||||
'Open project': {
|
||||
name: string
|
||||
}
|
||||
'Delete project': {
|
||||
name: string
|
||||
}
|
||||
'Rename project': {
|
||||
oldName: string
|
||||
newName: string
|
||||
}
|
||||
'Import file from URL': {
|
||||
name: string
|
||||
code?: string
|
||||
method: 'newProject' | 'existingProject'
|
||||
projectName?: string
|
||||
}
|
||||
}
|
||||
|
||||
export const projectsCommandBarConfig: StateMachineCommandSetConfig<
|
||||
typeof projectsMachine,
|
||||
ProjectsCommandSchema
|
||||
> = {
|
||||
'Open project': {
|
||||
icon: 'arrowRight',
|
||||
description: 'Open a project',
|
||||
status: isDesktop() ? 'active' : 'inactive',
|
||||
args: {
|
||||
name: {
|
||||
inputType: 'options',
|
||||
required: true,
|
||||
options: (_, context) =>
|
||||
context?.projects.map((p) => ({
|
||||
name: p.name,
|
||||
value: p.name,
|
||||
})) || [],
|
||||
},
|
||||
},
|
||||
},
|
||||
'Create project': {
|
||||
icon: 'folderPlus',
|
||||
description: 'Create a project',
|
||||
status: isDesktop() ? 'active' : 'inactive',
|
||||
args: {
|
||||
name: {
|
||||
inputType: 'string',
|
||||
required: true,
|
||||
defaultValueFromContext: (context) => context.defaultProjectName,
|
||||
},
|
||||
},
|
||||
},
|
||||
'Delete project': {
|
||||
icon: 'close',
|
||||
description: 'Delete a project',
|
||||
status: isDesktop() ? 'active' : 'inactive',
|
||||
needsReview: true,
|
||||
reviewMessage: ({ argumentsToSubmit }) =>
|
||||
CommandBarOverwriteWarning({
|
||||
heading: 'Are you sure you want to delete?',
|
||||
message: `This will permanently delete the project "${argumentsToSubmit.name}" and all its contents.`,
|
||||
}),
|
||||
args: {
|
||||
name: {
|
||||
inputType: 'options',
|
||||
required: true,
|
||||
options: (_, context) =>
|
||||
context?.projects.map((p) => ({
|
||||
name: p.name,
|
||||
value: p.name,
|
||||
})) || [],
|
||||
},
|
||||
},
|
||||
},
|
||||
'Rename project': {
|
||||
icon: 'folder',
|
||||
description: 'Rename a project',
|
||||
needsReview: true,
|
||||
status: isDesktop() ? 'active' : 'inactive',
|
||||
args: {
|
||||
oldName: {
|
||||
inputType: 'options',
|
||||
required: true,
|
||||
options: (_, context) =>
|
||||
context?.projects.map((p) => ({
|
||||
name: p.name,
|
||||
value: p.name,
|
||||
})) || [],
|
||||
},
|
||||
newName: {
|
||||
inputType: 'string',
|
||||
required: true,
|
||||
defaultValueFromContext: (context) => context.defaultProjectName,
|
||||
},
|
||||
},
|
||||
},
|
||||
'Import file from URL': {
|
||||
icon: 'file',
|
||||
description: 'Create a file',
|
||||
needsReview: true,
|
||||
status: 'active',
|
||||
args: {
|
||||
method: {
|
||||
inputType: 'options',
|
||||
required: true,
|
||||
skip: true,
|
||||
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) =>
|
||||
isDesktop() &&
|
||||
commandsContext.argumentsToSubmit.method === 'existingProject',
|
||||
skip: true,
|
||||
options: (_, context) =>
|
||||
context?.projects.map((p) => ({
|
||||
name: p.name,
|
||||
value: p.name,
|
||||
})) || [],
|
||||
},
|
||||
name: {
|
||||
inputType: 'string',
|
||||
required: isDesktop(),
|
||||
skip: true,
|
||||
},
|
||||
code: {
|
||||
inputType: 'text',
|
||||
required: true,
|
||||
skip: true,
|
||||
valueSummary(value) {
|
||||
const lineCount = value?.trim().split('\n').length
|
||||
return `${lineCount} line${lineCount === 1 ? '' : 's'}`
|
||||
},
|
||||
},
|
||||
},
|
||||
reviewMessage(commandBarContext) {
|
||||
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.`
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
import {
|
||||
folderSnapshot,
|
||||
defaultProjectFolderNameSnapshot,
|
||||
|
||||
@ -9,6 +9,23 @@ import { SceneEntities } from '@src/clientSideScene/sceneEntities'
|
||||
import { SceneInfra } from '@src/clientSideScene/sceneInfra'
|
||||
import type { BaseUnit } from '@src/lib/settings/settingsTypes'
|
||||
|
||||
|
||||
import { useSelector } from '@xstate/react'
|
||||
import { createActor, setup, spawnChild } from 'xstate'
|
||||
|
||||
import { isDesktop } from '@src/lib/isDesktop'
|
||||
import { createSettings } from '@src/lib/settings/initialSettings'
|
||||
import { authMachine } from '@src/machines/authMachine'
|
||||
import type { EngineStreamActor } from '@src/machines/engineStreamMachine'
|
||||
import {
|
||||
engineStreamContextCreate,
|
||||
engineStreamMachine,
|
||||
} from '@src/machines/engineStreamMachine'
|
||||
import { ACTOR_IDS } from '@src/machines/machineConstants'
|
||||
import { settingsMachine } from '@src/machines/settingsMachine'
|
||||
import { systemIOMachineDesktop } from '@src/machines/systemIO/systemIOMachineDesktop'
|
||||
import { systemIOMachineWeb } from '@src/machines/systemIO/systemIOMachineWeb'
|
||||
|
||||
export const codeManager = new CodeManager()
|
||||
export const engineCommandManager = new EngineCommandManager()
|
||||
export const rustContext = new RustContext(engineCommandManager)
|
||||
@ -90,22 +107,6 @@ if (typeof window !== 'undefined') {
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
import { useSelector } from '@xstate/react'
|
||||
import { createActor, setup, spawnChild } from 'xstate'
|
||||
|
||||
import { isDesktop } from '@src/lib/isDesktop'
|
||||
import { createSettings } from '@src/lib/settings/initialSettings'
|
||||
import { authMachine } from '@src/machines/authMachine'
|
||||
import type { EngineStreamActor } from '@src/machines/engineStreamMachine'
|
||||
import {
|
||||
engineStreamContextCreate,
|
||||
engineStreamMachine,
|
||||
} from '@src/machines/engineStreamMachine'
|
||||
import { ACTOR_IDS } from '@src/machines/machineConstants'
|
||||
import { settingsMachine } from '@src/machines/settingsMachine'
|
||||
import { systemIOMachineDesktop } from '@src/machines/systemIO/systemIOMachineDesktop'
|
||||
import { systemIOMachineWeb } from '@src/machines/systemIO/systemIOMachineWeb'
|
||||
const { AUTH, SETTINGS, SYSTEM_IO, ENGINE_STREAM } = ACTOR_IDS
|
||||
const appMachineActors = {
|
||||
[AUTH]: authMachine,
|
||||
|
||||
@ -1,337 +0,0 @@
|
||||
import { assign, fromPromise, setup } from 'xstate'
|
||||
|
||||
import type { ProjectsCommandSchema } from '@src/lib/commandBarConfigs/projectsCommandConfig'
|
||||
import type { Project } from '@src/lib/project'
|
||||
import { isArray } from '@src/lib/utils'
|
||||
|
||||
export const projectsMachine = setup({
|
||||
types: {
|
||||
context: {} as {
|
||||
projects: Project[]
|
||||
defaultProjectName: string
|
||||
defaultDirectory: string
|
||||
hasListedProjects: boolean
|
||||
},
|
||||
events: {} as
|
||||
| { type: 'Read projects'; data: ProjectsCommandSchema['Read projects'] }
|
||||
| { type: 'Open project'; data: ProjectsCommandSchema['Open project'] }
|
||||
| {
|
||||
type: 'Rename project'
|
||||
data: ProjectsCommandSchema['Rename project']
|
||||
}
|
||||
| {
|
||||
type: 'Create project'
|
||||
data: ProjectsCommandSchema['Create project']
|
||||
}
|
||||
| {
|
||||
type: 'Delete project'
|
||||
data: ProjectsCommandSchema['Delete project']
|
||||
}
|
||||
| {
|
||||
type: 'Import file from URL'
|
||||
data: ProjectsCommandSchema['Import file from URL']
|
||||
}
|
||||
| { type: 'navigate'; data: { name: string } }
|
||||
| {
|
||||
type: 'xstate.done.actor.read-projects'
|
||||
output: Project[]
|
||||
}
|
||||
| {
|
||||
type: 'xstate.done.actor.delete-project'
|
||||
output: { message: string; name: string }
|
||||
}
|
||||
| {
|
||||
type: 'xstate.done.actor.create-project'
|
||||
output: { message: string; name: string }
|
||||
}
|
||||
| {
|
||||
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[]
|
||||
defaultProjectName: string
|
||||
defaultDirectory: string
|
||||
hasListedProjects: boolean
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
setProjects: assign({
|
||||
projects: ({ context, event }) =>
|
||||
'output' in event && isArray(event.output)
|
||||
? event.output
|
||||
: context.projects,
|
||||
}),
|
||||
setHasListedProjects: assign({
|
||||
hasListedProjects: () => true,
|
||||
}),
|
||||
toastSuccess: () => {},
|
||||
toastError: () => {},
|
||||
navigateToProject: () => {},
|
||||
navigateToProjectIfNeeded: () => {},
|
||||
navigateToFile: () => {},
|
||||
},
|
||||
actors: {
|
||||
readProjects: fromPromise(() => Promise.resolve([] as Project[])),
|
||||
createProject: fromPromise(
|
||||
(_: { input: { name: string; projects: Project[] } }) =>
|
||||
Promise.resolve({ message: '' })
|
||||
),
|
||||
renameProject: fromPromise(
|
||||
(_: {
|
||||
input: {
|
||||
oldName: string
|
||||
newName: string
|
||||
defaultProjectName: string
|
||||
defaultDirectory: string
|
||||
projects: Project[]
|
||||
}
|
||||
}) =>
|
||||
Promise.resolve({
|
||||
message: '',
|
||||
oldName: '',
|
||||
newName: '',
|
||||
})
|
||||
),
|
||||
deleteProject: fromPromise(
|
||||
(_: { input: { defaultDirectory: string; name: string } }) =>
|
||||
Promise.resolve({
|
||||
message: '',
|
||||
name: '',
|
||||
})
|
||||
),
|
||||
createFile: fromPromise(
|
||||
(_: {
|
||||
input: ProjectsCommandSchema['Import file from URL'] & {
|
||||
projects: Project[]
|
||||
}
|
||||
}) => Promise.resolve({ message: '', projectName: '', fileName: '' })
|
||||
),
|
||||
},
|
||||
guards: {
|
||||
'Has at least 1 project': ({ event }) => {
|
||||
if (event.type !== 'xstate.done.actor.read-projects') return false
|
||||
return event.output.length ? event.output.length >= 1 : false
|
||||
},
|
||||
},
|
||||
}).createMachine({
|
||||
/** @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',
|
||||
|
||||
context: ({ input }) => ({
|
||||
...input,
|
||||
}),
|
||||
|
||||
on: {
|
||||
assign: {
|
||||
actions: assign(({ event }) => ({
|
||||
...event.data,
|
||||
})),
|
||||
},
|
||||
|
||||
'Import file from URL': '.Creating file',
|
||||
},
|
||||
states: {
|
||||
'Has no projects': {
|
||||
on: {
|
||||
'Read projects': {
|
||||
target: 'Reading projects',
|
||||
},
|
||||
'Create project': {
|
||||
target: 'Creating project',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
'Has projects': {
|
||||
on: {
|
||||
'Read projects': {
|
||||
target: 'Reading projects',
|
||||
},
|
||||
|
||||
'Rename project': {
|
||||
target: 'Renaming project',
|
||||
},
|
||||
|
||||
'Create project': {
|
||||
target: 'Creating project',
|
||||
},
|
||||
|
||||
'Delete project': {
|
||||
target: 'Deleting project',
|
||||
},
|
||||
|
||||
'Open project': {
|
||||
target: 'Reading projects',
|
||||
actions: 'navigateToProject',
|
||||
reenter: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
'Creating project': {
|
||||
invoke: {
|
||||
id: 'create-project',
|
||||
src: 'createProject',
|
||||
input: ({ event, context }) => {
|
||||
if (
|
||||
event.type !== 'Create project' &&
|
||||
event.type !== 'Import file from URL'
|
||||
) {
|
||||
return {
|
||||
name: '',
|
||||
projects: context.projects,
|
||||
}
|
||||
}
|
||||
return {
|
||||
name: event.data.name,
|
||||
projects: context.projects,
|
||||
}
|
||||
},
|
||||
onDone: [
|
||||
{
|
||||
target: 'Reading projects',
|
||||
actions: ['toastSuccess', 'navigateToProject'],
|
||||
},
|
||||
],
|
||||
onError: [
|
||||
{
|
||||
target: 'Reading projects',
|
||||
actions: ['toastError'],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
'Renaming project': {
|
||||
invoke: {
|
||||
id: 'rename-project',
|
||||
src: 'renameProject',
|
||||
input: ({ event, context }) => {
|
||||
if (event.type !== 'Rename project') {
|
||||
// This is to make TS happy
|
||||
return {
|
||||
defaultProjectName: context.defaultProjectName,
|
||||
defaultDirectory: context.defaultDirectory,
|
||||
oldName: '',
|
||||
newName: '',
|
||||
projects: context.projects,
|
||||
}
|
||||
}
|
||||
return {
|
||||
defaultProjectName: context.defaultProjectName,
|
||||
defaultDirectory: context.defaultDirectory,
|
||||
oldName: event.data.oldName,
|
||||
newName: event.data.newName,
|
||||
projects: context.projects,
|
||||
}
|
||||
},
|
||||
onDone: [
|
||||
{
|
||||
target: '#Home machine.Reading projects',
|
||||
actions: ['toastSuccess', 'navigateToProjectIfNeeded'],
|
||||
},
|
||||
],
|
||||
onError: [
|
||||
{
|
||||
target: '#Home machine.Reading projects',
|
||||
actions: ['toastError'],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
'Deleting project': {
|
||||
invoke: {
|
||||
id: 'delete-project',
|
||||
src: 'deleteProject',
|
||||
input: ({ event, context }) => {
|
||||
if (event.type !== 'Delete project') {
|
||||
// This is to make TS happy
|
||||
return {
|
||||
defaultDirectory: context.defaultDirectory,
|
||||
name: '',
|
||||
}
|
||||
}
|
||||
return {
|
||||
defaultDirectory: context.defaultDirectory,
|
||||
name: event.data.name,
|
||||
}
|
||||
},
|
||||
onDone: [
|
||||
{
|
||||
actions: ['toastSuccess', 'navigateToProjectIfNeeded'],
|
||||
target: '#Home machine.Reading projects',
|
||||
},
|
||||
],
|
||||
onError: {
|
||||
actions: ['toastError'],
|
||||
target: '#Home machine.Has projects',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
'Reading projects': {
|
||||
invoke: {
|
||||
id: 'read-projects',
|
||||
src: 'readProjects',
|
||||
onDone: [
|
||||
{
|
||||
guard: 'Has at least 1 project',
|
||||
target: 'Has projects',
|
||||
actions: ['setProjects', 'setHasListedProjects'],
|
||||
},
|
||||
{
|
||||
target: 'Has no projects',
|
||||
actions: ['setProjects', 'setHasListedProjects'],
|
||||
},
|
||||
],
|
||||
onError: [
|
||||
{
|
||||
target: 'Has no projects',
|
||||
actions: ['toastError'],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
'Creating file': {
|
||||
invoke: {
|
||||
id: 'create-file',
|
||||
src: 'createFile',
|
||||
input: ({ event, context }) => {
|
||||
if (event.type !== 'Import file from URL') {
|
||||
return {
|
||||
code: '',
|
||||
name: '',
|
||||
method: 'existingProject',
|
||||
projects: context.projects,
|
||||
}
|
||||
}
|
||||
return {
|
||||
code: event.data.code || '',
|
||||
name: event.data.name,
|
||||
method: event.data.method,
|
||||
projectName: event.data.projectName,
|
||||
projects: context.projects,
|
||||
}
|
||||
},
|
||||
onDone: {
|
||||
target: 'Reading projects',
|
||||
actions: ['navigateToFile', 'toastSuccess'],
|
||||
},
|
||||
onError: {
|
||||
target: 'Reading projects',
|
||||
actions: 'toastError',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
@ -238,7 +238,7 @@ app.on('ready', (event, data) => {
|
||||
// Create the mainWindow
|
||||
mainWindow = createWindow()
|
||||
// Set menu application to null to avoid default electron menu
|
||||
// Menu.setApplicationMenu(null)
|
||||
Menu.setApplicationMenu(null)
|
||||
})
|
||||
|
||||
// For now there is no good reason to separate these out to another file(s)
|
||||
|
||||
Reference in New Issue
Block a user